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 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 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
8eda6e3
Merge branch 'main' into vault/pm-12571/folder-dialog
gbubemismith Jan 14, 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 @@ -9,11 +9,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 @@ -64,6 +64,6 @@ export class NewItemDropdownV2Component implements OnInit {
}

openFolderDialog() {
this.dialogService.open(AddEditFolderDialogComponent);
AddEditFolderDialogComponent.open(this.dialogService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
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 @@ -24,8 +24,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 @@ -34,14 +34,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 @@ -64,7 +65,7 @@ describe("FoldersV2Component", () => {
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
},
})
.overrideProvider(DialogService, { useValue: { open } })
.overrideProvider(DialogService, { useValue: mockDialogService })
.compileComponents();

fixture = TestBed.createComponent(FoldersV2Component);
Expand Down Expand Up @@ -97,9 +98,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 @@ -110,6 +109,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 @@ -11,18 +11,14 @@ import {
DialogService,
IconButtonModule,
} from "@bitwarden/components";
import { VaultIcons } from "@bitwarden/vault";
import { AddEditFolderDialogComponent, VaultIcons } from "@bitwarden/vault";

import { ItemGroupComponent } from "../../../../../../libs/components/src/item/item-group.component";
import { ItemModule } from "../../../../../../libs/components/src/item/item.module";
import { NoItemsModule } from "../../../../../../libs/components/src/no-items/no-items.module";
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 @@ -67,8 +63,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 });
}
}
17 changes: 8 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 @@ -73,6 +73,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,
DefaultCipherFormConfigService,
Expand Down Expand Up @@ -113,7 +115,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 @@ -569,20 +570,18 @@ 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: { folder },
});

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 @@ -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 @@ 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 @@ -79,14 +82,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 @@ -96,7 +98,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 @@ -111,21 +113,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.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId!.id);
const folder = await this.folderService.encrypt(this.folder, userKey);
await this.folderApiService.save(folder);

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 @@ -147,18 +149,25 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
await this.folderApiService.delete(this.folder.id);
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 @@ -16,5 +16,6 @@ export { DownloadAttachmentComponent } from "./components/download-attachment/do
export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component";
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 * from "./components/add-edit-folder-dialog/add-edit-folder-dialog.component";

export * as VaultIcons from "./icons";
Loading