Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-12571][PM-13807] Add/Edit Folder Dialog #12487

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8efb18d
move `add-edit-folder` component to `angular/vault/components` so it โ€ฆ
nick-livefront Dec 19, 2024
91a0fbf
add edit/add folder copy to web app copy
nick-livefront Dec 19, 2024
57d85d8
add extension refresh folder dialog to individual vault
nick-livefront Dec 19, 2024
b4ed747
adding folder delete message to the web
nick-livefront Dec 19, 2024
30024fe
add deletion result for add/edit folder dialog
nick-livefront Dec 19, 2024
9c89ccd
allow editing folder from web
nick-livefront Dec 19, 2024
14e9c25
fix strict types for changed files
nick-livefront Dec 19, 2024
2a840a1
update tests
nick-livefront Dec 19, 2024
e41a15e
remove border class so hover state shows
nick-livefront Dec 19, 2024
2d7b2cc
revert changes to new-item-dropdown-v2
nick-livefront Dec 19, 2024
54ae50e
migrate `AddEditFolderDialogComponent` to `libs/vault` package
nick-livefront Dec 20, 2024
b20827e
add Created enum type
nick-livefront Dec 20, 2024
1b442f6
add static open method for folder dialog
nick-livefront Dec 20, 2024
976e1c2
Merge branch 'main' into vault/pm-12571/folder-dialog
nick-livefront Jan 2, 2025
c414bfb
Merge branch 'main' of https://github.com/bitwarden/clients into vaulโ€ฆ
nick-livefront Jan 9, 2025
706c388
add fullName to `FolderFilter` type
nick-livefront Jan 10, 2025
b16fe41
save the full name of a folder before splitting it into parts
nick-livefront Jan 10, 2025
70bd316
use the full name of the folder filter when available
nick-livefront Jan 10, 2025
7da6bf9
Merge branch 'main' into vault/pm-12571/folder-dialog
nick-livefront Jan 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
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 @@
}

openFolderDialog() {
this.dialogService.open(AddEditFolderDialogComponent);
AddEditFolderDialogComponent.open(this.dialogService);

Check warning on line 75 in apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts#L75

Added line #L75 was not covered by tests
}
}
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 @@ -270,6 +270,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 @@ -10,5 +10,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 };
22 changes: 13 additions & 9 deletions apps/web/src/app/vault/individual-vault/vault.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@
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 @@ -115,7 +117,6 @@
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 @@ -601,20 +602,23 @@
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
}

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

Check warning on line 606 in apps/web/src/app/vault/individual-vault/vault.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/vault/individual-vault/vault.component.ts#L605-L606

Added lines #L605 - L606 were not covered by tests
};

editFolder = async (folder: FolderFilter): Promise<void> => {
const dialog = openFolderAddEditDialog(this.dialogService, {
data: {
folderId: folder.id,
},
// If the filter has a fullName populated
if (folder.fullName) {
folder.name = folder.fullName;

Check warning on line 612 in apps/web/src/app/vault/individual-vault/vault.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/vault/individual-vault/vault.component.ts#L612

Added line #L612 was not covered by tests
}

const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {

Check warning on line 615 in apps/web/src/app/vault/individual-vault/vault.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/vault/individual-vault/vault.component.ts#L615

Added line #L615 was not covered by tests
editFolderConfig: { folder },
});

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

Check warning on line 619 in apps/web/src/app/vault/individual-vault/vault.component.ts

View check run for this annotation

Codecov / codecov/patch

apps/web/src/app/vault/individual-vault/vault.component.ts#L619

Added line #L619 was not covered by tests

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 @@ -425,6 +425,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 @@
} 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 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 @@
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 @@
}

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 @@
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 @@

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>(

Check warning on line 170 in libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts

View check run for this annotation

Codecov / codecov/patch

libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts#L170

Added line #L170 was not covered by tests
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,5 +17,6 @@ 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";
Loading