diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts
index d57b1d2fe3..db3fff04bb 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts
@@ -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;
@@ -72,6 +72,6 @@ export class NewItemDropdownV2Component implements OnInit {
}
openFolderDialog() {
- this.dialogService.open(AddEditFolderDialogComponent);
+ AddEditFolderDialogComponent.open(this.dialogService);
}
}
diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts
index 9c202e26fe..6689f5a6c6 100644
--- a/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/settings/folders-v2.component.spec.ts
@@ -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";
@@ -27,8 +27,8 @@ import { FoldersV2Component } from "./folders-v2.component";
template: ``,
})
class MockPopupHeaderComponent {
- @Input() pageTitle: string;
- @Input() backAction: () => void;
+ @Input() pageTitle: string = "";
+ @Input() backAction: () => void = () => {};
}
@Component({
@@ -37,14 +37,15 @@ class MockPopupHeaderComponent {
template: ``,
})
class MockPopupFooterComponent {
- @Input() pageTitle: string;
+ @Input() pageTitle: string = "";
}
describe("FoldersV2Component", () => {
let component: FoldersV2Component;
let fixture: ComponentFixture;
const folderViews$ = new BehaviorSubject([]);
- const open = jest.fn();
+ const open = jest.spyOn(AddEditFolderDialogComponent, "open");
+ const mockDialogService = { open: jest.fn() };
beforeEach(async () => {
open.mockClear();
@@ -68,7 +69,7 @@ describe("FoldersV2Component", () => {
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
},
})
- .overrideProvider(DialogService, { useValue: { open } })
+ .overrideProvider(DialogService, { useValue: mockDialogService })
.compileComponents();
fixture = TestBed.createComponent(FoldersV2Component);
@@ -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", () => {
@@ -114,6 +113,6 @@ describe("FoldersV2Component", () => {
addButton.triggerEventHandler("click");
- expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} });
+ expect(open).toHaveBeenCalledWith(mockDialogService, {});
});
});
diff --git a/apps/browser/src/vault/popup/settings/folders-v2.component.ts b/apps/browser/src/vault/popup/settings/folders-v2.component.ts
index 8abc3f906c..bf25bb25f0 100644
--- a/apps/browser/src/vault/popup/settings/folders-v2.component.ts
+++ b/apps/browser/src/vault/popup/settings/folders-v2.component.ts
@@ -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
@@ -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,
@@ -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(AddEditFolderDialogComponent, {
- data: { editFolderConfig },
- });
+ AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig });
}
}
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
index 9b24f95e2e..03dfa92d0b 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts
@@ -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);
});
diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts
index 10888aea13..69d85c2a63 100644
--- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts
+++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts
@@ -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 };
diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts
index 7215c98020..950c1d7773 100644
--- a/apps/web/src/app/vault/individual-vault/vault.component.ts
+++ b/apps/web/src/app/vault/individual-vault/vault.component.ts
@@ -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,
@@ -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";
@@ -607,20 +608,24 @@ export class VaultComponent implements OnInit, OnDestroy {
await this.filterComponent.filters?.organizationFilter?.action(orgNode);
}
- addFolder = async (): Promise => {
- openFolderAddEditDialog(this.dialogService);
+ addFolder = (): void => {
+ AddEditFolderDialogComponent.open(this.dialogService);
};
editFolder = async (folder: FolderFilter): Promise => {
- 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",
diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json
index 3034678466..ef19916323 100644
--- a/apps/web/src/locales/en/messages.json
+++ b/apps/web/src/locales/en/messages.json
@@ -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"
diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.html
similarity index 96%
rename from apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html
rename to libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.html
index 0e6dbf2442..4869714332 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.html
+++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.html
@@ -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"
diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts
similarity index 95%
rename from apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts
rename to libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts
index cbec790303..93db390d14 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts
+++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.spec.ts
@@ -17,6 +17,7 @@ import { KeyService } from "@bitwarden/key-management";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogData,
+ AddEditFolderDialogResult,
} from "./add-edit-folder-dialog.component";
describe("AddEditFolderDialogComponent", () => {
@@ -115,7 +116,7 @@ describe("AddEditFolderDialogComponent", () => {
expect(showToast).toHaveBeenCalledWith({
message: "editedFolder",
- title: null,
+ title: "",
variant: "success",
});
});
@@ -125,7 +126,7 @@ describe("AddEditFolderDialogComponent", () => {
await component.submit();
- expect(close).toHaveBeenCalled();
+ expect(close).toHaveBeenCalledWith(AddEditFolderDialogResult.Created);
});
it("logs error if saving fails", async () => {
@@ -161,7 +162,7 @@ describe("AddEditFolderDialogComponent", () => {
expect(encrypt).toHaveBeenCalledWith(
{
- ...dialogData.editFolderConfig.folder,
+ ...dialogData.editFolderConfig!.folder,
name: "Edited Folder",
},
"",
@@ -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);
});
});
});
diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts
similarity index 78%
rename from apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts
rename to libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts
index a50403cea2..362063ff34 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/add-edit-folder-dialog/add-edit-folder-dialog.component.ts
+++ b/libs/vault/src/components/add-edit-folder-dialog/add-edit-folder-dialog.component.ts
@@ -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 {
@@ -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 };
@@ -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],
@@ -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,
@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 {
@@ -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;
}
@@ -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);
}
@@ -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(
+ AddEditFolderDialogComponent,
+ { data },
+ );
}
}
diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts
index 23143fa230..e1a1b0ebd0 100644
--- a/libs/vault/src/index.ts
+++ b/libs/vault/src/index.ts
@@ -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";