1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 23:33:31 +00:00

[PM-18766] Changes to use the new Assign Collections Component in Desktop (#14180)

* Initial changes to use the new Assign Collections Component in Desktop

* Renaming component properly and adding the missing messages.json entries

* Adding an option in right click menu to assign to collections

* lint fix

* prettier

* updates so that the feature flag being on will show the new assign collections dialog

* lint fix

* set collections property after updating cipher collections

* update revision date from server response in shareManyWithServer

* Removing changes from non-feature flagged files, fixing the refresh issue

* return CipherResponse instead of Record

* adding in the master password reprompt check if they try and share

---------

Co-authored-by: bnagawiecki <107435978+bnagawiecki@users.noreply.github.com>
Co-authored-by: jaasen-livefront <jaasen@livefront.com>
This commit is contained in:
cd-bitwarden
2025-06-17 14:26:30 -04:00
committed by GitHub
parent e8e61d2796
commit 82877e9b97
6 changed files with 279 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
import { CalloutModule, DialogModule } from "@bitwarden/components";
import { AssignCollectionsComponent } from "@bitwarden/vault";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginModule } from "../auth/login/login.module";
@@ -55,6 +56,7 @@ import { SharedModule } from "./shared/shared.module";
DeleteAccountComponent,
UserVerificationComponent,
NavComponent,
AssignCollectionsComponent,
VaultV2Component,
],
declarations: [

View File

@@ -3812,5 +3812,139 @@
"message": "Learn more about SSH agent",
"description": "Two part message",
"example": "Store your keys and connect with the SSH agent for fast, encrypted authentication. Learn more about SSH agent"
},
"assignToCollections": {
"message": "Assign to collections"
},
"assignToTheseCollections": {
"message": "Assign to these collections"
},
"bulkCollectionAssignmentDialogDescriptionSingular": {
"message": "Only organization members with access to these collections will be able to see the item."
},
"bulkCollectionAssignmentDialogDescriptionPlural": {
"message": "Only organization members with access to these collections will be able to see the items."
},
"noCollectionsAssigned": {
"message": "No collections have been assigned"
},
"assign": {
"message": "Assign"
},
"bulkCollectionAssignmentDialogDescription": {
"message": "Only organization members with access to these collections will be able to see the items."
},
"bulkCollectionAssignmentWarning": {
"message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.",
"placeholders": {
"total_count": {
"content": "$1",
"example": "10"
},
"readonly_count": {
"content": "$2"
}
}
},
"selectCollectionsToAssign": {
"message": "Select collections to assign"
},
"personalItemsTransferWarning": {
"message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to the selected organization. You will no longer own these items.",
"placeholders": {
"personal_items_count": {
"content": "$1",
"example": "2 items"
}
}
},
"personalItemsWithOrgTransferWarning": {
"message": "$PERSONAL_ITEMS_COUNT$ will be permanently transferred to $ORG$. You will no longer own these items.",
"placeholders": {
"personal_items_count": {
"content": "$1",
"example": "2 items"
},
"org": {
"content": "$2",
"example": "Organization name"
}
}
},
"personalItemTransferWarningSingular": {
"message": "1 item will be permanently transferred to the selected organization. You will no longer own this item."
},
"personalItemWithOrgTransferWarningSingular": {
"message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.",
"placeholders": {
"org": {
"content": "$1",
"example": "Organization name"
}
}
},
"successfullyAssignedCollections": {
"message": "Successfully assigned collections"
},
"nothingSelected": {
"message": "You have not selected anything."
},
"itemsMovedToOrg": {
"message": "Items moved to $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
"example": "Company Name"
}
}
},
"itemMovedToOrg": {
"message": "Item moved to $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
"example": "Company Name"
}
}
},
"movedItemsToOrg": {
"message": "Selected items moved to $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
"example": "Company Name"
}
},
"personalItemsTransferWarningPlural": {
"message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.",
"placeholders": {
"personal_items_count": {
"content": "$1",
"example": "2"
}
}
},
"personalItemWithOrgTransferWarningSingular": {
"message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.",
"placeholders": {
"org": {
"content": "$1",
"example": "Organization name"
}
}
},
"personalItemsWithOrgTransferWarningPlural": {
"message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.",
"placeholders": {
"personal_items_count": {
"content": "$1",
"example": "2"
},
"org": {
"content": "$2",
"example": "Organization name"
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ "assignToCollections" | i18n }}
<span class="tw-text-sm tw-normal-case tw-text-muted">
{{ editableItemCount | pluralize: ("item" | i18n) : ("items" | i18n) }}
</span>
</span>
<div bitDialogContent>
<assign-collections
[params]="params"
[submitBtn]="assignSubmitButton"
(onCollectionAssign)="onCollectionAssign($event)"
(editableItemCountChange)="editableItemCount = $event"
></assign-collections>
</div>
<ng-container bitDialogFooter>
<button
#assignSubmitButton
form="assign_collections_form"
type="submit"
bitButton
bitFormButton
buttonType="primary"
>
{{ "assign" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" bitDialogClose>
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,36 @@
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { PluralizePipe } from "@bitwarden/angular/pipes/pluralize.pipe";
import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
AssignCollectionsComponent,
CollectionAssignmentParams,
CollectionAssignmentResult,
} from "@bitwarden/vault";
@Component({
standalone: true,
templateUrl: "./assign-collections-desktop.component.html",
imports: [AssignCollectionsComponent, PluralizePipe, DialogModule, ButtonModule, JslibModule],
})
export class AssignCollectionsDesktopComponent {
protected editableItemCount: number;
constructor(
@Inject(DIALOG_DATA) public params: CollectionAssignmentParams,
private dialogRef: DialogRef<CollectionAssignmentResult>,
) {}
protected async onCollectionAssign(result: CollectionAssignmentResult) {
this.dialogRef.close(result);
}
static open(dialogService: DialogService, config: DialogConfig<CollectionAssignmentParams>) {
return dialogService.open<CollectionAssignmentResult, CollectionAssignmentParams>(
AssignCollectionsDesktopComponent,
config,
);
}
}

View File

@@ -0,0 +1 @@
export * from "./assign-collections-desktop.component";

View File

@@ -9,7 +9,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom } from "rxjs";
import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observable } from "rxjs";
import { filter, map, take } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
@@ -19,6 +19,8 @@ import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/vie
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
@@ -57,6 +59,7 @@ import {
CipherFormMode,
CipherFormModule,
CipherViewComponent,
CollectionAssignmentResult,
DecryptionFailureDialogComponent,
DefaultChangeLoginPasswordService,
DefaultCipherFormConfigService,
@@ -69,6 +72,7 @@ import { DesktopCredentialGenerationService } from "../../../services/desktop-ci
import { DesktopPremiumUpgradePromptService } from "../../../services/desktop-premium-upgrade-prompt.service";
import { invokeMenu, RendererMenuItem } from "../../../utils";
import { AssignCollectionsDesktopComponent } from "./assign-collections";
import { ItemFooterComponent } from "./item-footer.component";
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
@@ -142,6 +146,11 @@ export class VaultV2Component implements OnInit, OnDestroy {
config: CipherFormConfig | null = null;
isSubmitting = false;
private organizations$: Observable<Organization[]> = this.accountService.activeAccount$.pipe(
map((a) => a?.id),
switchMap((id) => this.organizationService.organizations$(id)),
);
protected canAccessAttachments$ = this.accountService.activeAccount$.pipe(
filter((account): account is Account => !!account),
switchMap((account) =>
@@ -151,6 +160,8 @@ export class VaultV2Component implements OnInit, OnDestroy {
private modal: ModalRef | null = null;
private componentIsDestroyed$ = new Subject<boolean>();
private allOrganizations: Organization[] = [];
private allCollections: CollectionView[] = [];
constructor(
private route: ActivatedRoute,
@@ -176,6 +187,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
private formConfigService: CipherFormConfigService,
private premiumUpgradePromptService: PremiumUpgradePromptService,
private collectionService: CollectionService,
private organizationService: OrganizationService,
private folderService: FolderService,
) {}
@@ -312,6 +324,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
});
});
}
this.organizations$.pipe(takeUntil(this.componentIsDestroyed$)).subscribe((orgs) => {
this.allOrganizations = orgs;
});
this.collectionService.decryptedCollections$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((collections) => {
this.allCollections = collections;
});
}
ngOnDestroy() {
@@ -420,6 +442,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
},
});
}
if (cipher.canAssignToCollections) {
menu.push({
label: this.i18nService.t("assignToCollections"),
click: () =>
this.functionWithChangeDetection(async () => {
await this.shareCipher(cipher);
}),
});
}
}
switch (cipher.type) {
@@ -531,6 +563,36 @@ export class VaultV2Component implements OnInit, OnDestroy {
await this.go().catch(() => {});
}
async shareCipher(cipher: CipherView) {
if (!cipher) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("nothingSelected"),
});
return;
}
if (!(await this.passwordReprompt(cipher))) {
return;
}
const availableCollections = this.getAvailableCollections(cipher);
const dialog = AssignCollectionsDesktopComponent.open(this.dialogService, {
data: {
ciphers: [cipher],
organizationId: cipher.organizationId as OrganizationId,
availableCollections,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === CollectionAssignmentResult.Saved) {
await this.savedCipher(cipher);
}
}
async addCipher(type: CipherType) {
if (this.action === "add") {
return;
@@ -603,6 +665,16 @@ export class VaultV2Component implements OnInit, OnDestroy {
await this.go().catch(() => {});
}
private getAvailableCollections(cipher: CipherView): CollectionView[] {
const orgId = cipher.organizationId;
if (!orgId || orgId === "MyVault") {
return [];
}
const organization = this.allOrganizations.find((o) => o.id === orgId);
return this.allCollections.filter((c) => c.organizationId === organization?.id && !c.readOnly);
}
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
if (vaultFilter.status === "favorites") {
return "searchFavorites";