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:
@@ -36,6 +36,7 @@ export interface CollectionDialogParams {
|
||||
collectionId?: string;
|
||||
organizationId: string;
|
||||
initialTab?: CollectionDialogTabType;
|
||||
parentCollectionId?: string;
|
||||
}
|
||||
|
||||
export enum CollectionDialogResult {
|
||||
@@ -133,6 +134,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
} else {
|
||||
this.nestOptions = collections;
|
||||
const parent = collections.find((c) => c.id === this.params.parentCollectionId);
|
||||
this.formGroup.patchValue({ parent: parent?.name ?? null });
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="page-header d-flex">
|
||||
<div class="tw-mb-4 tw-flex">
|
||||
<h1>
|
||||
{{ "vaultItems" | i18n }}
|
||||
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
|
||||
@@ -30,20 +30,44 @@
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions
|
||||
[vaultItemsComponent]="vaultItemsComponent"
|
||||
[deleted]="activeFilter.isDeleted"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<div *ngIf="!activeFilter.isDeleted" class="ml-auto d-flex">
|
||||
<div *ngIf="organization.canCreateNewCollections" class="dropdown mr-2" appListDropdown>
|
||||
<button
|
||||
class="btn"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
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
|
||||
*ngIf="!organization?.canCreateNewCollections"
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm ml-auto"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ViewContainerRef,
|
||||
} from "@angular/core";
|
||||
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 { 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 { Organization } from "@bitwarden/common/models/domain/organization";
|
||||
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 { VaultFilter } from "../../vault/vault-filter/shared/models/vault-filter.model";
|
||||
import { EntityEventsComponent } from "../manage/entity-events.component";
|
||||
import {
|
||||
CollectionDialogResult,
|
||||
openCollectionDialog,
|
||||
} from "../shared/components/collection-dialog";
|
||||
|
||||
import { AddEditComponent } from "./add-edit.component";
|
||||
import { AttachmentsComponent } from "./attachments.component";
|
||||
@@ -66,6 +71,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private syncService: SyncService,
|
||||
private i18nService: I18nService,
|
||||
private modalService: ModalService,
|
||||
private dialogService: DialogService,
|
||||
private messagingService: MessagingService,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private ngZone: NgZone,
|
||||
@@ -160,6 +166,21 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
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) {
|
||||
if (this.organization.maxStorageGb == null || this.organization.maxStorageGb === 0) {
|
||||
this.messagingService.send("upgradeOrganization", { organizationId: cipher.organizationId });
|
||||
|
||||
@@ -103,7 +103,6 @@ import { ToolsComponent } from "../tools/tools.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/attachments.component";
|
||||
import { BulkActionsComponent } from "../vault/bulk-actions.component";
|
||||
import { CollectionsComponent } from "../vault/collections.component";
|
||||
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
||||
import { ShareComponent } from "../vault/share.component";
|
||||
@@ -130,7 +129,6 @@ import { SharedModule } from "./shared.module";
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BillingSyncKeyComponent,
|
||||
BulkActionsComponent,
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
@@ -237,7 +235,6 @@ import { SharedModule } from "./shared.module";
|
||||
AdjustStorageComponent,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BulkActionsComponent,
|
||||
ChangeEmailComponent,
|
||||
ChangeKdfComponent,
|
||||
ChangePasswordComponent,
|
||||
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{ 'col-6': isShowingCards, 'col-9': !isShowingCards }">
|
||||
<div class="page-header d-flex">
|
||||
<div class="tw-mb-4 tw-flex">
|
||||
<h1>
|
||||
{{ "vaultItems" | i18n }}
|
||||
<small #actionSpinner [appApiAction]="vaultItemsComponent.actionPromise">
|
||||
@@ -32,18 +32,14 @@
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions
|
||||
[vaultItemsComponent]="vaultItemsComponent"
|
||||
[deleted]="activeFilter.isDeleted"
|
||||
>
|
||||
</app-vault-bulk-actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
(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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -386,6 +386,9 @@
|
||||
"select": {
|
||||
"message": "Select"
|
||||
},
|
||||
"newItem": {
|
||||
"message": "New item"
|
||||
},
|
||||
"addItem": {
|
||||
"message": "Add item"
|
||||
},
|
||||
@@ -395,6 +398,14 @@
|
||||
"viewItem": {
|
||||
"message": "View item"
|
||||
},
|
||||
"new":
|
||||
{
|
||||
"message": "New",
|
||||
"description": "for adding new items"
|
||||
},
|
||||
"item": {
|
||||
"message": "Item"
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
|
||||
Reference in New Issue
Block a user