1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-22 16:29:09 +01:00

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

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,6 +712,16 @@ export class VaultComponent implements OnInit, OnDestroy {
} else { } else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$); collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
} }
const dialog = openOrgVaultCollectionsDialog(this.dialogService, {
data: {
collectionIds: cipher.collectionIds,
collections: collections.filter((c) => !c.readOnly && c.id != Unassigned),
organization: this.organization,
cipherId: cipher.id,
},
});
/**
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(
CollectionsComponent, CollectionsComponent,
this.collectionsModalRef, this.collectionsModalRef,
@ -726,6 +737,12 @@ export class VaultComponent implements OnInit, OnDestroy {
}); });
}, },
); );
*/
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;
} }
} }