Skip to content

Commit

Permalink
[PM-12571][PM-13807] Add/Edit Folder Dialog (#12487)
Browse files Browse the repository at this point in the history
* move `add-edit-folder` component to `angular/vault/components` so it can be consumed by other platforms

* add edit/add folder copy to web app copy

* add extension refresh folder dialog to individual vault

* adding folder delete message to the web

* add deletion result for add/edit folder dialog

* allow editing folder from web

* fix strict types for changed files

* update tests

* remove border class so hover state shows

* revert changes to new-item-dropdown-v2

* migrate `AddEditFolderDialogComponent` to `libs/vault` package

* add Created enum type

* add static open method for folder dialog

* add fullName to `FolderFilter` type

* save the full name of a folder before splitting it into parts

* use the full name of the folder filter when available

* use a shallow copy to edit the folder's full name

---------

Co-authored-by: SmithThe4th <[email protected]>
  • Loading branch information
nick-livefront and gbubemismith authored Feb 4, 2025
1 parent a9f24b6 commit aa024b4
Show file tree
Hide file tree
Showing 11 changed files with 86 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";

import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component";

export interface NewItemInitialValues {
folderId?: string;
Expand Down Expand Up @@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit {
}

openFolderDialog() {
this.dialogService.open(AddEditFolderDialogComponent);
AddEditFolderDialogComponent.open(this.dialogService);
}
}
19 changes: 9 additions & 10 deletions apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DialogService } from "@bitwarden/components";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";

import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { AddEditFolderDialogComponent } from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";

import { FoldersV2Component } from "./folders-v2.component";

Expand All @@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component";
template: `<ng-content></ng-content>`,
})
class MockPopupHeaderComponent {
@Input() pageTitle: string;
@Input() backAction: () => void;
@Input() pageTitle: string = "";
@Input() backAction: () => void = () => {};
}

@Component({
Expand All @@ -37,14 +37,15 @@ class MockPopupHeaderComponent {
template: `<ng-content></ng-content>`,
})
class MockPopupFooterComponent {
@Input() pageTitle: string;
@Input() pageTitle: string = "";
}

describe("FoldersV2Component", () => {
let component: FoldersV2Component;
let fixture: ComponentFixture<FoldersV2Component>;
const folderViews$ = new BehaviorSubject<FolderView[]>([]);
const open = jest.fn();
const open = jest.spyOn(AddEditFolderDialogComponent, "open");
const mockDialogService = { open: jest.fn() };

beforeEach(async () => {
open.mockClear();
Expand All @@ -68,7 +69,7 @@ describe("FoldersV2Component", () => {
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
},
})
.overrideProvider(DialogService, { useValue: { open } })
.overrideProvider(DialogService, { useValue: mockDialogService })
.compileComponents();

fixture = TestBed.createComponent(FoldersV2Component);
Expand Down Expand Up @@ -101,9 +102,7 @@ describe("FoldersV2Component", () => {

editButton.triggerEventHandler("click");

expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, {
data: { editFolderConfig: { folder } },
});
expect(open).toHaveBeenCalledWith(mockDialogService, { editFolderConfig: { folder } });
});

it("opens add dialog for new folder when there are no folders", () => {
Expand All @@ -114,6 +113,6 @@ describe("FoldersV2Component", () => {

addButton.triggerEventHandler("click");

expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} });
expect(open).toHaveBeenCalledWith(mockDialogService, {});
});
});
10 changes: 2 additions & 8 deletions apps/browser/src/vault/popup/settings/folders-v2.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
DialogService,
IconButtonModule,
} from "@bitwarden/components";
import { VaultIcons } from "@bitwarden/vault";
import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault";

// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
Expand All @@ -27,10 +27,6 @@ import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogData,
} from "../components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component";

@Component({
standalone: true,
Expand Down Expand Up @@ -78,8 +74,6 @@ export class FoldersV2Component {
// If a folder is provided, the edit variant should be shown
const editFolderConfig = folder ? { folder } : undefined;

this.dialogService.open<unknown, AddEditFolderDialogData>(AddEditFolderDialogComponent, {
data: { editFolderConfig },
});
AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
folderCopy.id = f.id;
folderCopy.revisionDate = f.revisionDate;
folderCopy.icon = "bwi-folder";
folderCopy.fullName = f.name; // save full folder name before separating it into parts
const parts = f.name != null ? f.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter) : [];
ServiceUtils.nestedTraverse(nodes, 0, parts, folderCopy, null, NestingDelimiter);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@ export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: str
export type CollectionFilter = CollectionAdminView & {
icon: string;
};
export type FolderFilter = FolderView & { icon: string };
export type FolderFilter = FolderView & {
icon: string;
/**
* Full folder name.
*
* Used for when the folder `name` property is be separated into parts.
*/
fullName?: string;
};
export type OrganizationFilter = Organization & { icon: string; hideOptions?: boolean };
21 changes: 13 additions & 8 deletions apps/web/src/app/vault/individual-vault/vault.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import { DialogService, Icons, ToastService } from "@bitwarden/components";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogResult,
CipherFormConfig,
CollectionAssignmentResult,
DecryptionFailureDialogComponent,
Expand Down Expand Up @@ -118,7 +120,6 @@ import {
BulkMoveDialogResult,
openBulkMoveDialog,
} from "./bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component";
import { FolderAddEditDialogResult, openFolderAddEditDialog } from "./folder-add-edit.component";
import { VaultBannersComponent } from "./vault-banners/vault-banners.component";
import { VaultFilterComponent } from "./vault-filter/components/vault-filter.component";
import { VaultFilterService } from "./vault-filter/services/abstractions/vault-filter.service";
Expand Down Expand Up @@ -607,20 +608,24 @@ export class VaultComponent implements OnInit, OnDestroy {
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
}

addFolder = async (): Promise<void> => {
openFolderAddEditDialog(this.dialogService);
addFolder = (): void => {
AddEditFolderDialogComponent.open(this.dialogService);
};

editFolder = async (folder: FolderFilter): Promise<void> => {
const dialog = openFolderAddEditDialog(this.dialogService, {
data: {
folderId: folder.id,
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
editFolderConfig: {
// Shallow copy is used so the original folder object is not modified
folder: {
...folder,
name: folder.fullName ?? folder.name, // If the filter has a fullName populated, use that as the editable name
},
},
});

const result = await lastValueFrom(dialog.closed);
const result = await lastValueFrom(dialogRef.closed);

if (result === FolderAddEditDialogResult.Deleted) {
if (result === AddEditFolderDialogResult.Deleted) {
await this.router.navigate([], {
queryParams: { folderId: null },
queryParamsHandling: "merge",
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,18 @@
"editFolder": {
"message": "Edit folder"
},
"newFolder": {
"message": "New folder"
},
"folderName": {
"message": "Folder name"
},
"folderHintText": {
"message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums"
},
"deleteFolderPermanently": {
"message": "Are you sure you want to permanently delete this folder?"
},
"baseDomain": {
"message": "Base domain",
"description": "Domain name. Example: website.com"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
*ngIf="variant === 'edit'"
type="button"
buttonType="danger"
class="tw-border-0 tw-ml-auto"
class="tw-ml-auto"
bitIconButton="bwi-trash"
[appA11yTitle]="'deleteFolder' | i18n"
[bitAction]="deleteFolder"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { KeyService } from "@bitwarden/key-management";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogData,
AddEditFolderDialogResult,
} from "./add-edit-folder-dialog.component";

describe("AddEditFolderDialogComponent", () => {
Expand Down Expand Up @@ -115,7 +116,7 @@ describe("AddEditFolderDialogComponent", () => {

expect(showToast).toHaveBeenCalledWith({
message: "editedFolder",
title: null,
title: "",
variant: "success",
});
});
Expand All @@ -125,7 +126,7 @@ describe("AddEditFolderDialogComponent", () => {

await component.submit();

expect(close).toHaveBeenCalled();
expect(close).toHaveBeenCalledWith(AddEditFolderDialogResult.Created);
});

it("logs error if saving fails", async () => {
Expand Down Expand Up @@ -161,7 +162,7 @@ describe("AddEditFolderDialogComponent", () => {

expect(encrypt).toHaveBeenCalledWith(
{
...dialogData.editFolderConfig.folder,
...dialogData.editFolderConfig!.folder,
name: "Edited Folder",
},
"",
Expand All @@ -174,9 +175,10 @@ describe("AddEditFolderDialogComponent", () => {
expect(deleteFolder).toHaveBeenCalledWith(folderView.id, "");
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
title: "",
message: "deletedFolder",
});
expect(close).toHaveBeenCalledWith(AddEditFolderDialogResult.Deleted);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import {
Expand Down Expand Up @@ -35,6 +33,11 @@ import {
} from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";

export enum AddEditFolderDialogResult {
Created = "created",
Deleted = "deleted",
}

export type AddEditFolderDialogData = {
/** When provided, dialog will display edit folder variant */
editFolderConfig?: { folder: FolderView };
Expand All @@ -56,12 +59,12 @@ export type AddEditFolderDialogData = {
],
})
export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
@ViewChild(BitSubmitDirective) private bitSubmit: BitSubmitDirective;
@ViewChild("submitBtn") private submitBtn: ButtonComponent;
@ViewChild(BitSubmitDirective) private bitSubmit?: BitSubmitDirective;
@ViewChild("submitBtn") private submitBtn?: ButtonComponent;

folder: FolderView;
folder: FolderView = new FolderView();

variant: "add" | "edit";
variant: "add" | "edit" = "add";

folderForm = this.formBuilder.group({
name: ["", Validators.required],
Expand All @@ -80,14 +83,13 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
private i18nService: I18nService,
private logService: LogService,
private dialogService: DialogService,
private dialogRef: DialogRef,
private dialogRef: DialogRef<AddEditFolderDialogResult>,
@Inject(DIALOG_DATA) private data?: AddEditFolderDialogData,
) {}

ngOnInit(): void {
this.variant = this.data?.editFolderConfig ? "edit" : "add";

if (this.variant === "edit") {
if (this.data?.editFolderConfig) {
this.variant = "edit";
this.folderForm.controls.name.setValue(this.data.editFolderConfig.folder.name);
this.folder = this.data.editFolderConfig.folder;
} else {
Expand All @@ -97,7 +99,7 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
}

ngAfterViewInit(): void {
this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => {
this.bitSubmit?.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => {
if (!this.submitBtn) {
return;
}
Expand All @@ -112,21 +114,21 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
return;
}

this.folder.name = this.folderForm.controls.name.value;
this.folder.name = this.folderForm.controls.name.value ?? "";

try {
const activeUserId = await firstValueFrom(this.activeUserId$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId!);
const folder = await this.folderService.encrypt(this.folder, userKey);
await this.folderApiService.save(folder, activeUserId);
await this.folderApiService.save(folder, activeUserId!);

this.toastService.showToast({
variant: "success",
title: null,
title: "",
message: this.i18nService.t("editedFolder"),
});

this.close();
this.close(AddEditFolderDialogResult.Created);
} catch (e) {
this.logService.error(e);
}
Expand All @@ -146,21 +148,28 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {

try {
const activeUserId = await firstValueFrom(this.activeUserId$);
await this.folderApiService.delete(this.folder.id, activeUserId);
await this.folderApiService.delete(this.folder.id, activeUserId!);
this.toastService.showToast({
variant: "success",
title: null,
title: "",
message: this.i18nService.t("deletedFolder"),
});
} catch (e) {
this.logService.error(e);
}

this.close();
this.close(AddEditFolderDialogResult.Deleted);
};

/** Close the dialog */
private close() {
this.dialogRef.close();
private close(result: AddEditFolderDialogResult) {
this.dialogRef.close(result);
}

static open(dialogService: DialogService, data?: AddEditFolderDialogData) {
return dialogService.open<AddEditFolderDialogResult, AddEditFolderDialogData>(
AddEditFolderDialogComponent,
{ data },
);
}
}
1 change: 1 addition & 0 deletions libs/vault/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { PasswordHistoryViewComponent } from "./components/password-history-view
export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component";
export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component";
export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component";
export * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";

export * as VaultIcons from "./icons";

Expand Down

0 comments on commit aa024b4

Please sign in to comment.