1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 15:23:33 +00:00

[EC-424] top level vault (#4267)

* [EC-424] remove cog menu and header hr

* [EC-424] change "Add item" to "New item"

* [EC-424] include text for "New item"

* [EC-424] add new item dropdown to org vault
- add parent collection to dialog params

* [EC-14] show Add Item if missing permissions
This commit is contained in:
Jake Fink
2022-12-20 10:16:45 -05:00
committed by GitHub
parent 8585b0e1eb
commit 260580237a
8 changed files with 75 additions and 235 deletions

View File

@@ -36,6 +36,7 @@ export interface CollectionDialogParams {
collectionId?: string; collectionId?: string;
organizationId: string; organizationId: string;
initialTab?: CollectionDialogTabType; initialTab?: CollectionDialogTabType;
parentCollectionId?: string;
} }
export enum CollectionDialogResult { export enum CollectionDialogResult {
@@ -133,6 +134,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
}); });
} else { } else {
this.nestOptions = collections; this.nestOptions = collections;
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
this.formGroup.patchValue({ parent: parent?.name ?? null });
} }
this.loading = false; this.loading = false;

View File

@@ -16,7 +16,7 @@
</div> </div>
</div> </div>
<div class="col-9"> <div class="col-9">
<div class="page-header d-flex"> <div class="tw-mb-4 tw-flex">
<h1> <h1>
{{ "vaultItems" | i18n }} {{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise"> <small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
@@ -30,20 +30,44 @@
</ng-container> </ng-container>
</small> </small>
</h1> </h1>
<div class="ml-auto d-flex"> <div *ngIf="!activeFilter.isDeleted" class="ml-auto d-flex">
<app-vault-bulk-actions <div *ngIf="organization.canCreateNewCollections" class="dropdown mr-2" appListDropdown>
[vaultItemsComponent]="vaultItemsComponent" <button
[deleted]="activeFilter.isDeleted" class="btn"
[organization]="organization" bitButton
> buttonType="primary"
</app-vault-bulk-actions> type="button"
data-toggle="dropdown"
id="newItemDropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'new' | i18n }}"
>
{{ "new" | i18n }}<i class="bwi bwi-angle-down tw-ml-2" aria-hidden="true"></i>
</button>
<div
id="dropdown"
class="dropdown-menu dropdown-menu-right tw-mt-2"
aria-labelledby="newItemDropdown"
>
<button class="dropdown-item" appStopClick (click)="addCipher()">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>
{{ "item" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="addCollection()">
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</div>
</div>
<button <button
*ngIf="!organization?.canCreateNewCollections"
type="button" type="button"
class="btn btn-outline-primary btn-sm ml-auto" bitButton
buttonType="primary"
(click)="addCipher()" (click)="addCipher()"
*ngIf="!activeFilter.isDeleted"
> >
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }} <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ import {
ViewContainerRef, ViewContainerRef,
} from "@angular/core"; } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router"; import { ActivatedRoute, Params, Router } from "@angular/router";
import { combineLatest, firstValueFrom, Subject } from "rxjs"; import { combineLatest, firstValueFrom, lastValueFrom, Subject } from "rxjs";
import { first, switchMap, takeUntil } from "rxjs/operators"; import { first, switchMap, takeUntil } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
@@ -22,10 +22,15 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction";
import { Organization } from "@bitwarden/common/models/domain/organization"; import { Organization } from "@bitwarden/common/models/domain/organization";
import { CipherView } from "@bitwarden/common/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { VaultFilterService } from "../../vault/vault-filter/services/abstractions/vault-filter.service"; import { VaultFilterService } from "../../vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model"; import { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model";
import { EntityEventsComponent } from "../manage/entity-events.component"; import { EntityEventsComponent } from "../manage/entity-events.component";
import {
CollectionDialogResult,
openCollectionDialog,
} from "../shared/components/collection-dialog";
import { AddEditComponent } from "./add-edit.component"; import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component"; import { AttachmentsComponent } from "./attachments.component";
@@ -66,6 +71,7 @@ export class VaultComponent implements OnInit, OnDestroy {
private syncService: SyncService, private syncService: SyncService,
private i18nService: I18nService, private i18nService: I18nService,
private modalService: ModalService, private modalService: ModalService,
private dialogService: DialogService,
private messagingService: MessagingService, private messagingService: MessagingService,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
private ngZone: NgZone, private ngZone: NgZone,
@@ -160,6 +166,21 @@ export class VaultComponent implements OnInit, OnDestroy {
this.vaultItemsComponent.search(200); this.vaultItemsComponent.search(200);
} }
async addCollection() {
const dialog = openCollectionDialog(this.dialogService, {
data: {
organizationId: this.organization?.id,
parentCollectionId: this.activeFilter.collectionId,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
this.vaultItemsComponent.actionPromise = this.vaultItemsComponent.refresh();
await this.vaultItemsComponent.actionPromise;
this.vaultItemsComponent.actionPromise = null;
}
}
async editCipherAttachments(cipher: CipherView) { async editCipherAttachments(cipher: CipherView) {
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) { if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId }); this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });

View File

@@ -103,7 +103,6 @@ import { ToolsComponent } from "../tools/tools.component";
import { AddEditCustomFieldsComponent } from "../vault/add-edit-custom-fields.component"; import { AddEditCustomFieldsComponent } from "../vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/add-edit.component"; import { AddEditComponent } from "../vault/add-edit.component";
import { AttachmentsComponent } from "../vault/attachments.component"; import { AttachmentsComponent } from "../vault/attachments.component";
import { BulkActionsComponent } from "../vault/bulk-actions.component";
import { CollectionsComponent } from "../vault/collections.component"; import { CollectionsComponent } from "../vault/collections.component";
import { FolderAddEditComponent } from "../vault/folder-add-edit.component"; import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
import { ShareComponent } from "../vault/share.component"; import { ShareComponent } from "../vault/share.component";
@@ -130,7 +129,6 @@ import { SharedModule } from "./shared.module";
ApiKeyComponent, ApiKeyComponent,
AttachmentsComponent, AttachmentsComponent,
BillingSyncKeyComponent, BillingSyncKeyComponent,
BulkActionsComponent,
ChangeEmailComponent, ChangeEmailComponent,
ChangeKdfComponent, ChangeKdfComponent,
ChangePasswordComponent, ChangePasswordComponent,
@@ -237,7 +235,6 @@ import { SharedModule } from "./shared.module";
AdjustStorageComponent, AdjustStorageComponent,
ApiKeyComponent, ApiKeyComponent,
AttachmentsComponent, AttachmentsComponent,
BulkActionsComponent,
ChangeEmailComponent, ChangeEmailComponent,
ChangeKdfComponent, ChangeKdfComponent,
ChangePasswordComponent, ChangePasswordComponent,

View File

@@ -1,50 +0,0 @@
<div class="dropdown mr-2" appListDropdown>
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button
class="dropdown-item"
appStopClick
(click)="bulkMove()"
*ngIf="!deleted && !organization"
>
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "moveSelected" | i18n }}
</button>
<button
class="dropdown-item"
appStopClick
(click)="bulkShare()"
*ngIf="!deleted && !organization"
>
<i class="bwi bwi-fw bwi-arrow-circle-right" aria-hidden="true"></i>
{{ "moveSelectedToOrg" | i18n }}
</button>
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
<i class="bwi bwi-fw bwi-undo" aria-hidden="true"></i>
{{ "restoreSelected" | i18n }}
</button>
<button class="dropdown-item text-danger" (click)="bulkDelete()">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
{{ (deleted ? "permanentlyDeleteSelected" : "deleteSelected") | i18n }}
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i>
{{ "selectAll" | i18n }}
</button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i>
{{ "unselectAll" | i18n }}
</button>
</div>
</div>

View File

@@ -1,162 +0,0 @@
import { Component, Input } from "@angular/core";
import { lastValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType";
import { Organization } from "@bitwarden/common/models/domain/organization";
import { DialogService } from "@bitwarden/components";
import {
BulkDeleteDialogResult,
openBulkDeleteDialog,
} from "./bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component";
import {
BulkMoveDialogResult,
openBulkMoveDialog,
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
import {
BulkRestoreDialogResult,
openBulkRestoreDialog,
} from "./bulk-action-dialogs/bulk-restore-dialog/bulk-restore-dialog.component";
import {
BulkShareDialogResult,
openBulkShareDialog,
} from "./bulk-action-dialogs/bulk-share-dialog/bulk-share-dialog.component";
import { VaultItemsComponent } from "./vault-items.component";
@Component({
selector: "app-vault-bulk-actions",
templateUrl: "bulk-actions.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class BulkActionsComponent {
@Input() vaultItemsComponent: VaultItemsComponent;
@Input() deleted: boolean;
@Input() organization: Organization;
constructor(
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private dialogService: DialogService,
private passwordRepromptService: PasswordRepromptService
) {}
async bulkDelete() {
if (!(await this.promptPassword())) {
return;
}
const selectedCipherIds = this.vaultItemsComponent.selectedCipherIds;
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkDeleteDialog(this.dialogService, {
data: {
permanent: this.deleted,
cipherIds: selectedCipherIds,
organization: this.organization,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkDeleteDialogResult.Deleted) {
await this.vaultItemsComponent.refresh();
}
}
async bulkRestore() {
if (!(await this.promptPassword())) {
return;
}
const selectedCipherIds = this.vaultItemsComponent.selectedCipherIds;
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkRestoreDialog(this.dialogService, {
data: {
cipherIds: selectedCipherIds,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkRestoreDialogResult.Restored) {
this.vaultItemsComponent.refresh();
}
}
async bulkShare() {
if (!(await this.promptPassword())) {
return;
}
const selectedCiphers = this.vaultItemsComponent.selectedCiphers;
if (selectedCiphers.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkShareDialog(this.dialogService, { data: { ciphers: selectedCiphers } });
const result = await lastValueFrom(dialog.closed);
if (result === BulkShareDialogResult.Shared) {
this.vaultItemsComponent.refresh();
}
}
async bulkMove() {
if (!(await this.promptPassword())) {
return;
}
const selectedCipherIds = this.vaultItemsComponent.selectedCipherIds;
if (selectedCipherIds.length === 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("nothingSelected")
);
return;
}
const dialog = openBulkMoveDialog(this.dialogService, {
data: { cipherIds: selectedCipherIds },
});
const result = await lastValueFrom(dialog.closed);
if (result === BulkMoveDialogResult.Moved) {
this.vaultItemsComponent.refresh();
}
}
selectAll(select: boolean) {
this.vaultItemsComponent.checkAll(select);
}
private async promptPassword() {
const selectedCiphers = this.vaultItemsComponent.selectedCiphers;
const notProtected = !selectedCiphers.find(
(cipher) => cipher.reprompt !== CipherRepromptType.None
);
return notProtected || (await this.passwordRepromptService.showPasswordPrompt());
}
}

View File

@@ -17,7 +17,7 @@
</div> </div>
</div> </div>
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }"> <div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
<div class="page-header d-flex"> <div class="tw-mb-4 tw-flex">
<h1> <h1>
{{ "vaultItems" | i18n }} {{ "vaultItems" | i18n }}
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise"> <small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
@@ -32,18 +32,14 @@
</small> </small>
</h1> </h1>
<div class="ml-auto d-flex"> <div class="ml-auto d-flex">
<app-vault-bulk-actions
[vaultItemsComponent]="vaultItemsComponent"
[deleted]="activeFilter.isDeleted"
>
</app-vault-bulk-actions>
<button <button
type="button" type="button"
class="btn btn-outline-primary btn-sm" bitButton
buttonType="primary"
(click)="addCipher()" (click)="addCipher()"
*ngIf="!activeFilter.isDeleted" *ngIf="!activeFilter.isDeleted"
> >
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "addItem" | i18n }} <i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>{{ "newItem" | i18n }}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -386,6 +386,9 @@
"select": { "select": {
"message": "Select" "message": "Select"
}, },
"newItem": {
"message": "New item"
},
"addItem": { "addItem": {
"message": "Add item" "message": "Add item"
}, },
@@ -395,6 +398,14 @@
"viewItem": { "viewItem": {
"message": "View item" "message": "View item"
}, },
"new":
{
"message": "New",
"description": "for adding new items"
},
"item": {
"message": "Item"
},
"ex": { "ex": {
"message": "ex.", "message": "ex.",
"description": "Short abbreviation for 'example'." "description": "Short abbreviation for 'example'."