mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-02 18:17:46 +01:00
[PM-7883] Fix Collection Dialog Component (#9088)
* [PM-7883] Cleanup/refactor collection-dialog.component - Add new limitNestedCollections option - Remove redundant calls to collectionService and collectionAdminService - Adjust deleted parent logic to account for users that cannot ViewAllCollections * [PM-7883] Ensure collection management setting is considered when limiting nested collections in the org vault
This commit is contained in:
parent
7f91e84456
commit
b4f4818635
@ -3,7 +3,6 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angula
|
|||||||
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
|
import { AbstractControl, FormBuilder, Validators } from "@angular/forms";
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
from,
|
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
of,
|
||||||
@ -23,7 +22,6 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
|
||||||
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
import { CollectionResponse } from "@bitwarden/common/vault/models/response/collection.response";
|
||||||
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
|
||||||
import { BitValidators, DialogService } from "@bitwarden/components";
|
import { BitValidators, DialogService } from "@bitwarden/components";
|
||||||
@ -56,7 +54,10 @@ export interface CollectionDialogParams {
|
|||||||
initialTab?: CollectionDialogTabType;
|
initialTab?: CollectionDialogTabType;
|
||||||
parentCollectionId?: string;
|
parentCollectionId?: string;
|
||||||
showOrgSelector?: boolean;
|
showOrgSelector?: boolean;
|
||||||
collectionIds?: string[];
|
/**
|
||||||
|
* Flag to limit the nested collections to only those the user has explicit CanManage access too.
|
||||||
|
*/
|
||||||
|
limitNestedCollections?: boolean;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +86,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
protected tabIndex: CollectionDialogTabType;
|
protected tabIndex: CollectionDialogTabType;
|
||||||
protected loading = true;
|
protected loading = true;
|
||||||
protected organization?: Organization;
|
protected organization?: Organization;
|
||||||
protected collection?: CollectionView;
|
protected collection?: CollectionAdminView;
|
||||||
protected nestOptions: CollectionView[] = [];
|
protected nestOptions: CollectionView[] = [];
|
||||||
protected accessItems: AccessItemView[] = [];
|
protected accessItems: AccessItemView[] = [];
|
||||||
protected deletedParentName: string | undefined;
|
protected deletedParentName: string | undefined;
|
||||||
@ -107,7 +108,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private groupService: GroupService,
|
private groupService: GroupService,
|
||||||
private collectionAdminService: CollectionAdminService,
|
private collectionAdminService: CollectionAdminService,
|
||||||
private collectionService: CollectionService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private organizationUserService: OrganizationUserService,
|
||||||
@ -124,7 +124,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
this.showOrgSelector = true;
|
this.showOrgSelector = true;
|
||||||
this.formGroup.controls.selectedOrg.valueChanges
|
this.formGroup.controls.selectedOrg.valueChanges
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((id) => this.loadOrg(id, this.params.collectionIds));
|
.subscribe((id) => this.loadOrg(id));
|
||||||
this.organizations$ = this.organizationService.organizations$.pipe(
|
this.organizations$ = this.organizationService.organizations$.pipe(
|
||||||
first(),
|
first(),
|
||||||
map((orgs) =>
|
map((orgs) =>
|
||||||
@ -138,11 +138,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
// Opened from the org vault
|
// Opened from the org vault
|
||||||
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
this.formGroup.patchValue({ selectedOrg: this.params.organizationId });
|
||||||
await this.loadOrg(this.params.organizationId, this.params.collectionIds);
|
await this.loadOrg(this.params.organizationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadOrg(orgId: string, collectionIds: string[]) {
|
async loadOrg(orgId: string) {
|
||||||
const organization$ = this.organizationService
|
const organization$ = this.organizationService
|
||||||
.get$(orgId)
|
.get$(orgId)
|
||||||
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
.pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||||
@ -158,28 +158,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
combineLatest({
|
combineLatest({
|
||||||
organization: organization$,
|
organization: organization$,
|
||||||
collections: this.collectionAdminService.getAll(orgId),
|
collections: this.collectionAdminService.getAll(orgId),
|
||||||
collectionDetails: this.params.collectionId
|
|
||||||
? from(this.collectionAdminService.get(orgId, this.params.collectionId))
|
|
||||||
: of(null),
|
|
||||||
groups: groups$,
|
groups: groups$,
|
||||||
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
|
// Collection(s) needed to map readonlypermission for (potential) access selector disabled state
|
||||||
users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }),
|
users: this.organizationUserService.getAllUsers(orgId, { includeCollections: true }),
|
||||||
collection: this.params.collectionId
|
|
||||||
? this.collectionService.get(this.params.collectionId)
|
|
||||||
: of(null),
|
|
||||||
flexibleCollectionsV1: this.flexibleCollectionsV1Enabled$,
|
flexibleCollectionsV1: this.flexibleCollectionsV1Enabled$,
|
||||||
})
|
})
|
||||||
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
.pipe(takeUntil(this.formGroup.controls.selectedOrg.valueChanges), takeUntil(this.destroy$))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({ organization, collections: allCollections, groups, users, flexibleCollectionsV1 }) => {
|
||||||
organization,
|
|
||||||
collections,
|
|
||||||
collectionDetails,
|
|
||||||
groups,
|
|
||||||
users,
|
|
||||||
collection,
|
|
||||||
flexibleCollectionsV1,
|
|
||||||
}) => {
|
|
||||||
this.organization = organization;
|
this.organization = organization;
|
||||||
this.accessItems = [].concat(
|
this.accessItems = [].concat(
|
||||||
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
|
groups.map((group) => mapGroupToAccessItemView(group, this.collectionId)),
|
||||||
@ -189,37 +175,48 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
|||||||
// Force change detection to update the access selector's items
|
// Force change detection to update the access selector's items
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
|
|
||||||
if (collectionIds) {
|
this.nestOptions = this.params.limitNestedCollections
|
||||||
collections = collections.filter((c) => collectionIds.includes(c.id));
|
? allCollections.filter((c) => c.manage)
|
||||||
}
|
: allCollections;
|
||||||
|
|
||||||
if (this.params.collectionId) {
|
if (this.params.collectionId) {
|
||||||
this.collection = collections.find((c) => c.id === this.collectionId);
|
this.collection = allCollections.find((c) => c.id === this.collectionId);
|
||||||
this.nestOptions = collections.filter((c) => c.id !== this.collectionId);
|
// Ensure we don't allow nesting the current collection within itself
|
||||||
|
this.nestOptions = this.nestOptions.filter((c) => c.id !== this.collectionId);
|
||||||
|
|
||||||
if (!this.collection) {
|
if (!this.collection) {
|
||||||
throw new Error("Could not find collection to edit.");
|
throw new Error("Could not find collection to edit.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, parent } = parseName(this.collection);
|
// Parse the name to find its parent name
|
||||||
if (parent !== undefined && !this.nestOptions.find((c) => c.name === parent)) {
|
const { name, parent: parentName } = parseName(this.collection);
|
||||||
this.deletedParentName = parent;
|
|
||||||
|
// Determine if the user can see/select the parent collection
|
||||||
|
if (parentName !== undefined) {
|
||||||
|
if (
|
||||||
|
this.organization.canViewAllCollections &&
|
||||||
|
!allCollections.find((c) => c.name === parentName)
|
||||||
|
) {
|
||||||
|
// The user can view all collections, but the parent was not found -> assume it has been deleted
|
||||||
|
this.deletedParentName = parentName;
|
||||||
|
} else if (!this.nestOptions.find((c) => c.name === parentName)) {
|
||||||
|
// We cannot find the current parent collection in our list of options, so add a placeholder
|
||||||
|
this.nestOptions.unshift({ name: parentName } as CollectionView);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessSelections = mapToAccessSelections(collectionDetails);
|
const accessSelections = mapToAccessSelections(this.collection);
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
name,
|
name,
|
||||||
externalId: this.collection.externalId,
|
externalId: this.collection.externalId,
|
||||||
parent,
|
parent: parentName,
|
||||||
access: accessSelections,
|
access: accessSelections,
|
||||||
});
|
});
|
||||||
this.collection.manage = collection?.manage ?? false; // Get manage flag from sync data collection
|
|
||||||
this.showDeleteButton =
|
this.showDeleteButton =
|
||||||
!this.dialogReadonly &&
|
!this.dialogReadonly &&
|
||||||
this.collection.canDelete(organization, flexibleCollectionsV1);
|
this.collection.canDelete(organization, flexibleCollectionsV1);
|
||||||
} else {
|
} else {
|
||||||
this.nestOptions = collections;
|
const parent = this.nestOptions.find((c) => c.id === this.params.parentCollectionId);
|
||||||
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
|
|
||||||
const currentOrgUserId = users.data.find(
|
const currentOrgUserId = users.data.find(
|
||||||
(u) => u.userId === this.organization?.userId,
|
(u) => u.userId === this.organization?.userId,
|
||||||
)?.id;
|
)?.id;
|
||||||
|
@ -650,7 +650,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
.sort(Utils.getSortFunction(this.i18nService, "name"))[0].id,
|
.sort(Utils.getSortFunction(this.i18nService, "name"))[0].id,
|
||||||
parentCollectionId: this.filter.collectionId,
|
parentCollectionId: this.filter.collectionId,
|
||||||
showOrgSelector: true,
|
showOrgSelector: true,
|
||||||
collectionIds: this.allCollections.map((c) => c.id),
|
limitNestedCollections: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await lastValueFrom(dialog.closed);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
@ -666,7 +666,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
|
async editCollection(c: CollectionView, tab: CollectionDialogTabType): Promise<void> {
|
||||||
const dialog = openCollectionDialog(this.dialogService, {
|
const dialog = openCollectionDialog(this.dialogService, {
|
||||||
data: { collectionId: c?.id, organizationId: c.organizationId, initialTab: tab },
|
data: {
|
||||||
|
collectionId: c?.id,
|
||||||
|
organizationId: c.organizationId,
|
||||||
|
initialTab: tab,
|
||||||
|
limitNestedCollections: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await lastValueFrom(dialog.closed);
|
const result = await lastValueFrom(dialog.closed);
|
||||||
|
@ -1175,6 +1175,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
data: {
|
data: {
|
||||||
organizationId: this.organization?.id,
|
organizationId: this.organization?.id,
|
||||||
parentCollectionId: this.selectedCollection?.node.id,
|
parentCollectionId: this.selectedCollection?.node.id,
|
||||||
|
limitNestedCollections: !this.organization.canEditAnyCollection(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1198,6 +1201,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
organizationId: this.organization?.id,
|
organizationId: this.organization?.id,
|
||||||
initialTab: tab,
|
initialTab: tab,
|
||||||
readonly: readonly,
|
readonly: readonly,
|
||||||
|
limitNestedCollections: !this.organization.canEditAnyCollection(
|
||||||
|
this.flexibleCollectionsV1Enabled,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user