mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-12 00:41:29 +01:00
[PM-12571][PM-13807] Add/Edit Folder Dialog (#12487)
* 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 <gsmith@bitwarden.com>
This commit is contained in:
parent
a9f24b6d24
commit
aa024b419c
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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: `<ng-content></ng-content>`,
|
||||
})
|
||||
class MockPopupHeaderComponent {
|
||||
@Input() pageTitle: string;
|
||||
@Input() backAction: () => void;
|
||||
@Input() pageTitle: string = "";
|
||||
@Input() backAction: () => void = () => {};
|
||||
}
|
||||
|
||||
@Component({
|
||||
@ -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();
|
||||
@ -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, {});
|
||||
});
|
||||
});
|
||||
|
@ -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<unknown, AddEditFolderDialogData>(AddEditFolderDialogComponent, {
|
||||
data: { editFolderConfig },
|
||||
});
|
||||
AddEditFolderDialogComponent.open(this.dialogService, { editFolderConfig });
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 };
|
||||
|
@ -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<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",
|
||||
|
@ -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"
|
||||
|
@ -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"
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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<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 {
|
||||
@ -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<AddEditFolderDialogResult, AddEditFolderDialogData>(
|
||||
AddEditFolderDialogComponent,
|
||||
{ data },
|
||||
);
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user