[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:
parent
cebbb9486e
commit
9bfd838da6
|
@ -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": {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,8 +17,6 @@ import { SharedModule } from "../../../shared";
|
|||
standalone: true,
|
||||
})
|
||||
export class AssignCollectionsWebComponent {
|
||||
protected loading = false;
|
||||
protected disabled = false;
|
||||
protected editableItemCount: number;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue