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:
@@ -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": {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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()">
|
<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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user