1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +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,64 +1,52 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="collectionsTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="modal-header">
<h1 class="modal-title" id="collectionsTitle">
{{ "collections" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</h1>
<button
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>
<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>
<form (ngSubmit)="submit()">
<bit-dialog>
<span bitDialogTitle>
{{ "collections" | i18n }}
<small *ngIf="cipher">{{ cipher.name }}</small>
</span>
<ng-container bitDialogContent>
<p>{{ "collectionsDesc" | i18n }}</p>
<div class="tw-flex">
<label class="tw-mb-1 tw-block tw-font-semibold tw-text-main">{{
"collections" | i18n
}}</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 }}
</button>
<button bitLink type="button" (click)="selectAll(false)" class="tw-px-2">
{{ "unselectAll" | i18n }}
</button>
</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 class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<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 *ngIf="!collections || !collections.length">
{{ "noCollectionsInList" | i18n }}
</div>
</form>
</div>
</div>
<bit-table *ngIf="collections && collections.length">
<ng-template body>
<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 { 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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { DialogService } from "@bitwarden/components";
@Component({
selector: "app-vault-collections",
@ -21,6 +23,8 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
cipherService: CipherService,
organizationSerivce: OrganizationService,
logService: LogService,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: CollectionsDialogParams,
) {
super(
collectionService,
@ -30,10 +34,16 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
organizationSerivce,
logService,
);
this.cipherId = params?.cipherId;
}
ngOnDestroy() {
this.selectAll(false);
override async submit(): Promise<boolean> {
const success = await super.submit();
if (success) {
this.dialogRef.close(CollectionsDialogResult.Saved);
return true;
}
return false;
}
check(c: CollectionView, select?: boolean) {
@ -46,4 +56,31 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On
selectAll(select: boolean) {
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,
openBulkShareDialog,
} 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 { ShareComponent } from "./share.component";
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
@ -568,17 +568,7 @@ export class VaultComponent implements OnInit, OnDestroy {
}
async editCipherCollections(cipher: CipherView) {
const [modal] = await this.modalService.openViewRef(
CollectionsComponent,
this.collectionsModalRef,
(comp) => {
comp.cipherId = cipher.id;
comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => {
modal.close();
this.refresh();
});
},
);
openIndividualVaultCollectionsDialog(this.dialogService, { data: { cipherId: cipher.id } });
}
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 { 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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
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({
selector: "app-org-vault-collections",
@ -29,6 +35,8 @@ export class CollectionsComponent extends BaseCollectionsComponent {
organizationService: OrganizationService,
private apiService: ApiService,
logService: LogService,
protected dialogRef: DialogRef,
@Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams,
) {
super(
collectionService,
@ -37,8 +45,14 @@ export class CollectionsComponent extends BaseCollectionsComponent {
cipherService,
organizationService,
logService,
dialogRef,
params,
);
this.allowSelectNone = true;
this.collectionIds = params?.collectionIds;
this.collections = params?.collections;
this.organization = params?.organization;
this.cipherId = params?.cipherId;
}
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,
openBulkDeleteDialog,
} 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 { RoutedVaultFilterService } from "../individual-vault/vault-filter/services/routed-vault-filter.service";
import { createFilterFunction } from "../individual-vault/vault-filter/shared/models/filter-function";
@ -95,7 +96,7 @@ import {
BulkCollectionsDialogComponent,
BulkCollectionsDialogResult,
} from "./bulk-collections-dialog";
import { CollectionsComponent } from "./collections.component";
import { openOrgVaultCollectionsDialog } from "./collections.component";
import { VaultFilterComponent } from "./vault-filter/vault-filter.component";
const BroadcasterSubscriptionId = "OrgVaultComponent";
@ -711,21 +712,37 @@ export class VaultComponent implements OnInit, OnDestroy {
} else {
collections = await firstValueFrom(this.allCollectionsWithoutUnassigned$);
}
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();
});
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(
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() {

View File

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