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";