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

[PM-8381] Assign collections (#9854)

* initial add of assign collections component

* grab cipherId from query params

* add organization selection for moving a cipher

* add multi-select for collections

* add transfer of cipher to an organization

* temp: do not show assign collections while a cipher already has an organization

* account for initial collections for a cipher

* block assign-collections route with feature flag

* replace hardcoded string with i18n

* separate out async calls to switchMap to avoid async subscribe

* use local cipher rather than decrypting again

* use anchor for better semantics

* migrate form submission to bitSubmit directive

* swap to "assign" rather than "save"

* integrate with base AssignCollections component

* clean up messages file

* remove unneeded takeUntilDestroyed

* remove unneeded bitFormButton

* remove unused translations

* lint fix

* refactor assign-collections component to take in a button reference

- This allows consuming components to not have to worry about loading/disabled states
- The base AssignCollections component will change the submit button when supplied
This commit is contained in:
Nick Krantz
2024-07-18 08:53:53 -05:00
committed by GitHub
parent cebbb9486e
commit 9bfd838da6
10 changed files with 205 additions and 22 deletions

View File

@@ -3544,12 +3544,6 @@
"contactYourOrgAdmin": { "contactYourOrgAdmin": {
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
}, },
"itemDetails": {
"message": "Item details"
},
"itemName": {
"message": "Item name"
},
"additionalInformation": { "additionalInformation": {
"message": "Additional information" "message": "Additional information"
}, },
@@ -3643,6 +3637,24 @@
"loading": { "loading": {
"message": "Loading" "message": "Loading"
}, },
"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"
}
}
},
"addField": { "addField": {
"message": "Add field" "message": "Add field"
}, },
@@ -3726,6 +3738,46 @@
} }
} }
}, },
"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"
}
}
},
"successfullyAssignedCollections": {
"message": "Successfully assigned collections"
},
"nothingSelected": {
"message": "You have not selected anything."
},
"movedItemsToOrg": {
"message": "Selected items moved to $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
"example": "Company Name"
}
}
},
"reorderFieldDown":{ "reorderFieldDown":{
"message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$",
"placeholders": { "placeholders": {

View File

@@ -177,6 +177,9 @@ export const routerTransition = trigger("routerTransition", [
transition("tabs => account-security", inSlideLeft), transition("tabs => account-security", inSlideLeft),
transition("account-security => tabs", outSlideRight), transition("account-security => tabs", outSlideRight),
transition("tabs => assign-collections", inSlideLeft),
transition("assign-collections => tabs", outSlideRight),
// Vault settings // Vault settings
transition("tabs => vault-settings", inSlideLeft), transition("tabs => vault-settings", inSlideLeft),
transition("vault-settings => tabs", outSlideRight), transition("vault-settings => tabs", outSlideRight),

View File

@@ -71,6 +71,7 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
@@ -408,6 +409,12 @@ const routes: Routes = [
}, },
], ],
}, },
{
path: "assign-collections",
component: AssignCollections,
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")],
data: { state: "assign-collections" },
},
...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, {
path: "about", path: "about",
canActivate: [AuthGuard], canActivate: [AuthGuard],

View File

@@ -0,0 +1,31 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'assignCollections' | i18n" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<bit-card>
<assign-collections
*ngIf="params"
[params]="params"
[submitBtn]="assignSubmitButton"
(onCollectionAssign)="navigateBack()"
></assign-collections>
</bit-card>
<popup-footer slot="footer">
<button
#assignSubmitButton
bitButton
form="assign_collections_form"
buttonType="primary"
type="submit"
>
{{ "assign" | i18n }}
</button>
<a bitButton buttonType="secondary" (click)="navigateBack()">
{{ "cancel" | i18n }}
</a>
</popup-footer>
</popup-page>

View File

@@ -0,0 +1,81 @@
import { CommonModule, Location } from "@angular/common";
import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ReactiveFormsModule } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { Observable, combineLatest, first, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
ButtonModule,
CardComponent,
SelectModule,
FormFieldModule,
AsyncActionsModule,
} from "@bitwarden/components";
import { AssignCollectionsComponent, CollectionAssignmentParams } from "@bitwarden/vault";
import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
@Component({
standalone: true,
selector: "app-assign-collections",
templateUrl: "./assign-collections.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CommonModule,
JslibModule,
SelectModule,
FormFieldModule,
AssignCollectionsComponent,
CardComponent,
ReactiveFormsModule,
PopupPageComponent,
PopupHeaderComponent,
PopupFooterComponent,
PopOutComponent,
],
})
export class AssignCollections {
/** Params needed to populate the assign collections component */
params: CollectionAssignmentParams;
constructor(
private location: Location,
private collectionService: CollectionService,
private cipherService: CipherService,
route: ActivatedRoute,
) {
const $cipher: Observable<CipherView> = route.queryParams.pipe(
switchMap(({ cipherId }) => this.cipherService.get(cipherId)),
switchMap((cipherDomain) =>
this.cipherService
.getKeyForCipherKeyDecryption(cipherDomain)
.then(cipherDomain.decrypt.bind(cipherDomain)),
),
);
combineLatest([$cipher, this.collectionService.decryptedCollections$])
.pipe(takeUntilDestroyed(), first())
.subscribe(([cipherView, collections]) => {
this.params = {
ciphers: [cipherView],
organizationId: (cipherView?.organizationId as OrganizationId) ?? null,
availableCollections: collections.filter((c) => !c.readOnly),
};
});
}
/** Navigates the user back to the previous screen */
navigateBack() {
this.location.back();
}
}

View File

@@ -28,9 +28,14 @@
<a routerLink="" bitMenuItem (click)="clone()"> <a routerLink="" bitMenuItem (click)="clone()">
{{ "clone" | i18n }} {{ "clone" | i18n }}
</a> </a>
<button type="button" bitMenuItem> <a
routerLink="/assign-collections"
[queryParams]="{ cipherId: this.cipher.id }"
type="button"
bitMenuItem
>
{{ "assignCollections" | i18n }} {{ "assignCollections" | i18n }}
</button> </a>
</ng-container> </ng-container>
</bit-menu> </bit-menu>
</bit-item-action> </bit-item-action>

View File

@@ -9,8 +9,7 @@
<div bitDialogContent> <div bitDialogContent>
<assign-collections <assign-collections
[params]="params" [params]="params"
(formDisabled)="disabled = $event" [submitBtn]="assignSubmitButton"
(formLoading)="loading = $event"
(onCollectionAssign)="onCollectionAssign($event)" (onCollectionAssign)="onCollectionAssign($event)"
(editableItemCountChange)="editableItemCount = $event" (editableItemCountChange)="editableItemCount = $event"
></assign-collections> ></assign-collections>
@@ -18,8 +17,7 @@
<ng-container bitDialogFooter> <ng-container bitDialogFooter>
<button <button
[disabled]="disabled" #assignSubmitButton
[loading]="loading"
form="assign_collections_form" form="assign_collections_form"
type="submit" type="submit"
bitButton bitButton

View File

@@ -17,8 +17,6 @@ import { SharedModule } from "../../../shared";
standalone: true, standalone: true,
}) })
export class AssignCollectionsWebComponent { export class AssignCollectionsWebComponent {
protected loading = false;
protected disabled = false;
protected editableItemCount: number; protected editableItemCount: number;
constructor( constructor(

View File

@@ -1,7 +1,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" id="assign_collections_form"> <form [formGroup]="formGroup" [bitSubmit]="submit" id="assign_collections_form">
<p>{{ "bulkCollectionAssignmentDialogDescription" | i18n }}</p> <p>{{ "bulkCollectionAssignmentDialogDescription" | i18n }}</p>
<ul class="tw-list-disc tw-pl-5 tw-space-y-2"> <ul class="tw-list-none tw-pl-5 tw-space-y-2">
<li *ngIf="readonlyItemCount > 0"> <li *ngIf="readonlyItemCount > 0">
<p> <p>
{{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }} {{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }}

View File

@@ -28,6 +28,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v
import { import {
AsyncActionsModule, AsyncActionsModule,
BitSubmitDirective, BitSubmitDirective,
ButtonComponent,
ButtonModule, ButtonModule,
DialogModule, DialogModule,
FormFieldModule, FormFieldModule,
@@ -86,11 +87,10 @@ export class AssignCollectionsComponent implements OnInit {
@Input() params: CollectionAssignmentParams; @Input() params: CollectionAssignmentParams;
@Output() /**
formLoading = new EventEmitter<boolean>(); * Submit button instance that will be disabled or marked as loading when the form is submitting.
*/
@Output() @Input() submitBtn?: ButtonComponent;
formDisabled = new EventEmitter<boolean>();
@Output() @Output()
editableItemCountChange = new EventEmitter<number>(); editableItemCountChange = new EventEmitter<number>();
@@ -177,11 +177,19 @@ export class AssignCollectionsComponent implements OnInit {
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => {
this.formLoading.emit(loading); if (!this.submitBtn) {
return;
}
this.submitBtn.loading = loading;
}); });
this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => {
this.formDisabled.emit(disabled); if (!this.submitBtn) {
return;
}
this.submitBtn.disabled = disabled;
}); });
} }