[PM-2170] Update collections component (#6794)

* PM-2170 Updated Collections to use Component Library

* PM-2170 Removed some extra space

* PM-2170 Fix typo

* PM-2170 Refresh vault when saving

* PM-2170 Fix PR comments

* PM-2170 Refactor to use CollectionsDialogResult to fix lint error

* PM-2170 Refactor subtitle

* PM-4788 Fix dismiss of modal

* PM-2170 Fix PR comments
This commit is contained in:
Carlos Gonçalves 2024-04-16 15:47:12 +01:00 committed by GitHub
parent 0765240886
commit 62ed7e5abc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 165 additions and 95 deletions

View File

@ -1,64 +1,52 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle"> <form (ngSubmit)="submit()">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <bit-dialog>
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise"> <span bitDialogTitle>
<div class="modal-header"> {{ "collections" | i18n }}
<h1 class="modal-title" id="collectionsTitle"> <small *ngIf="cipher">{{ cipher.name }}</small>
{{ "collections" | i18n }} </span>
<small *ngIf="cipher">{{ cipher.name }}</small> <ng-container bitDialogContent>
</h1> <p>{{ "collectionsDesc" | i18n }}</p>
<button <div class="tw-flex">
type="button" <label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{
class="close" "collections" | i18n
data-dismiss="modal" }}</label>
appA11yTitle="{{ 'close' | i18n }}" <div class="tw-ml-auto tw-flex" *ngIf="collections && collections.length">
> <button bitLink type="button" (click)="selectAll(true)" class="tw-px-2">
<span aria-hidden="true">&times;</span> {{ "selectAll" | i18n }}
</button> </button>
</div> <button bitLink type="button" (click)="selectAll(false)" class="tw-px-2">
<div class="modal-body"> {{ "unselectAll" | i18n }}
<p>{{ "collectionsDesc" | i18n }}</p> </button>
<div class="d-flex">
<h3>{{ "collections" | i18n }}</h3>
<div class="ml-auto d-flex" *ngIf="collections && collections.length">
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
{{ "selectAll" | i18n }}
</button>
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
{{ "unselectAll" | i18n }}
</button>
</div>
</div> </div>
<div *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
<table class="table table-hover table-list mb-0" *ngIf="collections && collections.length">
<tbody>
<tr *ngFor="let c of collections; let i = index" (click)="check(c)">
<td class="table-list-checkbox">
<input
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
/>
</td>
<td>
{{ c.name }}
</td>
</tr>
</tbody>
</table>
</div> </div>
<div class="modal-footer"> <div *ngIf="!collections || !collections.length">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> {{ "noCollectionsInList" | i18n }}
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }}
</button>
</div> </div>
</form> <bit-table *ngIf="collections && collections.length">
</div> <ng-template body>
</div> <tr bitRow *ngFor="let c of collections; let i = index" (click)="check(c)">
<td bitCell>
<input
type="checkbox"
bitCheckbox
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
appStopProp
[disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)"
/>
{{ c.name }}
</td>
</tr>
</ng-template>
</bit-table>
</ng-container>
<ng-container bitDialogFooter>
<button bitButton buttonType="primary" type="submit">
{{ "save" | i18n }}
</button>
<button bitButton bitDialogClose buttonType="secondary" type="button">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -1,4 +1,5 @@
import { Component, OnDestroy } from "@angular/core"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, OnDestroy, Inject } from "@angular/core";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -8,6 +9,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService } from "@bitwarden/components";
@Component({ @Component({
selector: "app-vault-collections", selector: "app-vault-collections",
@ -21,6 +23,8 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
cipherService: CipherService, cipherService: CipherService,
organizationSerivce: OrganizationService, organizationSerivce: OrganizationService,
logService: LogService, logService: LogService,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: CollectionsDialogParams,
) { ) {
super( super(
collectionService, collectionService,
@ -30,10 +34,16 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
organizationSerivce, organizationSerivce,
logService, logService,
); );
this.cipherId = params?.cipherId;
} }
ngOnDestroy() { override async submit(): Promise<boolean> {
this.selectAll(false); const success = await super.submit();
if (success) {
this.dialogRef.close(CollectionsDialogResult.Saved);
return true;
}
return false;
} }
check(c: CollectionView, select?: boolean) { check(c: CollectionView, select?: boolean) {
@ -46,4 +56,31 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
selectAll(select: boolean) { selectAll(select: boolean) {
this.collections.forEach((c) => this.check(c, select)); this.collections.forEach((c) => this.check(c, select));
} }
ngOnDestroy() {
this.selectAll(false);
}
}
export interface CollectionsDialogParams {
cipherId: string;
}
export enum CollectionsDialogResult {
Saved = "saved",
}
/**
* Strongly typed helper to open a Collections dialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Optional configuration for the dialog
*/
export function openIndividualVaultCollectionsDialog(
dialogService: DialogService,
config?: DialogConfig<CollectionsDialogParams>,
) {
return dialogService.open<CollectionsDialogResult, CollectionsDialogParams>(
CollectionsComponent,
config,
);
} }

View File

@ -86,7 +86,7 @@ import {
BulkShareDialogResult, BulkShareDialogResult,
openBulkShareDialog, openBulkShareDialog,
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component"; } from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
import { CollectionsComponent } from "./collections.component"; import { openIndividualVaultCollectionsDialog } from "./collections.component";
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component"; import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
import { ShareComponent } from "./share.component"; import { ShareComponent } from "./share.component";
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component"; import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
@ -568,17 +568,7 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
async editCipherCollections(cipher: CipherView) { async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef( openIndividualVaultCollectionsDialog(this.dialogService, { data: { cipherId: cipher.id } });
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
},
);
} }
async addCipher() { async addCipher() {

View File

@ -1,4 +1,5 @@
import { Component } from "@angular/core"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
@ -11,8 +12,13 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherCollectionsRequest } from "@bitwarden/common/vault/models/request/cipher-collections.request"; import { CipherCollectionsRequest } from "@bitwarden/common/vault/models/request/cipher-collections.request";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService } from "@bitwarden/components";
import { CollectionsComponent as BaseCollectionsComponent } from "../individual-vault/collections.component"; import {
CollectionsComponent as BaseCollectionsComponent,
CollectionsDialogResult,
} from "../individual-vault/collections.component";
@Component({ @Component({
selector: "app-org-vault-collections", selector: "app-org-vault-collections",
@ -29,6 +35,8 @@ export class CollectionsComponent extends BaseCollectionsComponent {
organizationService: OrganizationService, organizationService: OrganizationService,
private apiService: ApiService, private apiService: ApiService,
logService: LogService, logService: LogService,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams,
) { ) {
super( super(
collectionService, collectionService,
@ -37,8 +45,14 @@ export class CollectionsComponent extends BaseCollectionsComponent {
cipherService, cipherService,
organizationService, organizationService,
logService, logService,
dialogRef,
params,
); );
this.allowSelectNone = true; this.allowSelectNone = true;
this.collectionIds = params?.collectionIds;
this.collections = params?.collections;
this.organization = params?.organization;
this.cipherId = params?.cipherId;
} }
protected async loadCipher() { protected async loadCipher() {
@ -79,3 +93,25 @@ export class CollectionsComponent extends BaseCollectionsComponent {
} }
} }
} }
export interface OrgVaultCollectionsDialogParams {
collectionIds: string[];
collections: CollectionView[];
organization: Organization;
cipherId: string;
}
/**
* Strongly typed helper to open a Collections dialog
* @param dialogService Instance of the dialog service that will be used to open the dialog
* @param config Optional configuration for the dialog
*/
export function openOrgVaultCollectionsDialog(
dialogService: DialogService,
config?: DialogConfig<OrgVaultCollectionsDialogParams>,
) {
return dialogService.open<CollectionsDialogResult, OrgVaultCollectionsDialogParams>(
CollectionsComponent,
config,
);
}

View File

@ -75,6 +75,7 @@ import {
BulkDeleteDialogResult, BulkDeleteDialogResult,
openBulkDeleteDialog, openBulkDeleteDialog,
} from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component"; } from "../individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
import { CollectionsDialogResult } from "../individual-vault/collections.component";
import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service"; import { RoutedVaultFilterBridgeService } from "../individual-vault/vault-filter/services/routed-vault-filter-bridge.service";
import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service"; import { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service";
import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function"; import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function";
@ -95,7 +96,7 @@ import {
BulkCollectionsDialogComponent, BulkCollectionsDialogComponent,
BulkCollectionsDialogResult, BulkCollectionsDialogResult,
} from "./bulk-collections-dialog"; } from "./bulk-collections-dialog";
import { CollectionsComponent } from "./collections.component"; import { openOrgVaultCollectionsDialog } from "./collections.component";
import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
const BroadcasterSubscriptionId = "OrgVaultComponent"; const BroadcasterSubscriptionId = "OrgVaultComponent";
@ -711,21 +712,37 @@ export class VaultComponent implements OnInit, OnDestroy {
} else { } else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$); collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
} }
const [modal] = await this.modalService.openViewRef( const dialog = openOrgVaultCollectionsDialog(this.dialogService, {
CollectionsComponent, data: {
this.collectionsModalRef, collectionIds: cipher.collectionIds,
(comp) => { collections: collections.filter((c) => !c.readOnly && c.id != Unassigned),
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; organization: this.organization,
comp.collectionIds = cipher.collectionIds; cipherId: cipher.id,
comp.collections = collections;
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
}, },
); });
/**
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled;
comp.collectionIds = cipher.collectionIds;
comp.collections = collections;
comp.organization = this.organization;
comp.cipherId = cipher.id;
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
},
);
*/
if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) {
await this.refresh();
}
} }
async addCipher() { async addCipher() {

View File

@ -59,7 +59,7 @@ export class CollectionsComponent implements OnInit {
} }
} }
async submit() { async submit(): Promise<boolean> {
const selectedCollectionIds = this.collections const selectedCollectionIds = this.collections
.filter((c) => { .filter((c) => {
if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) {
@ -75,7 +75,7 @@ export class CollectionsComponent implements OnInit {
this.i18nService.t("errorOccurred"), this.i18nService.t("errorOccurred"),
this.i18nService.t("selectOneCollection"), this.i18nService.t("selectOneCollection"),
); );
return; return false;
} }
this.cipherDomain.collectionIds = selectedCollectionIds; this.cipherDomain.collectionIds = selectedCollectionIds;
try { try {
@ -83,8 +83,10 @@ export class CollectionsComponent implements OnInit {
await this.formPromise; await this.formPromise;
this.onSavedCollections.emit(); this.onSavedCollections.emit();
this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem")); this.platformUtilsService.showToast("success", null, this.i18nService.t("editedItem"));
return true;
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
return false;
} }
} }