[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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 205 additions and 22 deletions

View File

@ -3544,12 +3544,6 @@
"contactYourOrgAdmin": {
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
},
"itemDetails": {
"message": "Item details"
},
"itemName": {
"message": "Item name"
},
"additionalInformation": {
"message": "Additional information"
},
@ -3643,6 +3637,24 @@
"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": {
"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":{
"message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$",
"placeholders": {

View File

@ -177,6 +177,9 @@ export const routerTransition = trigger("routerTransition", [
transition("tabs => account-security", inSlideLeft),
transition("account-security => tabs", outSlideRight),
transition("tabs => assign-collections", inSlideLeft),
transition("assign-collections => tabs", outSlideRight),
// Vault settings
transition("tabs => vault-settings", inSlideLeft),
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 { ViewComponent } from "../vault/popup/components/vault/view.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 { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.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, {
path: "about",
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()">
{{ "clone" | i18n }}
</a>
<button type="button" bitMenuItem>
<a
routerLink="/assign-collections"
[queryParams]="{ cipherId: this.cipher.id }"
type="button"
bitMenuItem
>
{{ "assignCollections" | i18n }}
</button>
</a>
</ng-container>
</bit-menu>
</bit-item-action>

View File

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

View File

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

View File

@ -1,7 +1,7 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" id="assign_collections_form">
<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">
<p>
{{ "bulkCollectionAssignmentWarning" | i18n: totalItemCount : readonlyItemCount }}

View File

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