1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-8204] V2 Folder View (#10423)

* add no folders icon to icon library

* add/edit folder contained within a dialog

* add/edit folder dialog contained new item dropdown

* browser refresh folders page component

* swap in v2 folder component for extension refresh

* add copy for all folder related changes
This commit is contained in:
Nick Krantz
2024-08-08 13:24:49 -05:00
committed by GitHub
parent d2c4c4cad4
commit 28a2014e69
13 changed files with 759 additions and 9 deletions

View File

@@ -304,6 +304,24 @@
"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"
},
"noFoldersAdded": {
"message": "No folders added"
},
"createFoldersToOrganize": {
"message": "Create folders to organize your vault items"
},
"deleteFolderPermanently":{
"message": "Are you sure you want to permanently delete this folder?"
},
"deleteFolder": {
"message": "Delete folder"
},

View File

@@ -81,6 +81,7 @@ import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attac
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
@@ -303,12 +304,11 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "vault-settings" },
}),
{
...extensionRefreshSwap(FoldersComponent, FoldersV2Component, {
path: "folders",
component: FoldersComponent,
canActivate: [authGuard],
data: { state: "folders" },
},
}),
{
path: "add-folder",
component: FolderAddEditComponent,

View File

@@ -0,0 +1,41 @@
<form [formGroup]="folderForm" [bitSubmit]="submit" id="add-edit-folder">
<bit-dialog>
<span bitDialogTitle>
{{ (variant === "add" ? "newFolder" : "editFolder") | i18n }}
</span>
<div bitDialogContent>
<bit-form-field disableMargin>
<bit-label>{{ "folderName" | i18n }}</bit-label>
<input bitInput id="folderName" formControlName="name" type="text" />
<bit-hint>
{{ "folderHintText" | i18n }}
</bit-hint>
</bit-form-field>
</div>
<div bitDialogFooter class="tw-flex tw-gap-2 tw-w-full">
<button
#submitBtn
bitButton
buttonType="primary"
type="submit"
form="add-edit-folder"
[disabled]="folderForm.invalid"
>
{{ "save" | i18n }}
</button>
<button bitButton bitDialogClose buttonType="secondary" type="button">
{{ "cancel" | i18n }}
</button>
<button
*ngIf="variant === 'edit'"
type="button"
buttonType="danger"
class="tw-border-0 tw-ml-auto"
bitIconButton="bwi-trash"
[appA11yTitle]="'deleteFolder' | i18n"
[bitAction]="deleteFolder"
></button>
</div>
</bit-dialog>
</form>

View File

@@ -0,0 +1,157 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { Folder } from "@bitwarden/common/vault/models/domain/folder";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DialogService, ToastService } from "@bitwarden/components";
import {
AddEditFolderDialogComponent,
AddEditFolderDialogData,
} from "./add-edit-folder-dialog.component";
describe("AddEditFolderDialogComponent", () => {
let component: AddEditFolderDialogComponent;
let fixture: ComponentFixture<AddEditFolderDialogComponent>;
const dialogData = {} as AddEditFolderDialogData;
const folder = new Folder();
const encrypt = jest.fn().mockResolvedValue(folder);
const save = jest.fn().mockResolvedValue(null);
const deleteFolder = jest.fn().mockResolvedValue(null);
const openSimpleDialog = jest.fn().mockResolvedValue(true);
const error = jest.fn();
const close = jest.fn();
const showToast = jest.fn();
const dialogRef = {
close,
};
beforeEach(async () => {
encrypt.mockClear();
save.mockClear();
deleteFolder.mockClear();
error.mockClear();
close.mockClear();
showToast.mockClear();
await TestBed.configureTestingModule({
imports: [AddEditFolderDialogComponent, NoopAnimationsModule],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: FolderService, useValue: { encrypt } },
{ provide: FolderApiServiceAbstraction, useValue: { save, delete: deleteFolder } },
{ provide: LogService, useValue: { error } },
{ provide: ToastService, useValue: { showToast } },
{ provide: DIALOG_DATA, useValue: dialogData },
{ provide: DialogRef, useValue: dialogRef },
],
})
.overrideProvider(DialogService, { useValue: { openSimpleDialog } })
.compileComponents();
fixture = TestBed.createComponent(AddEditFolderDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
describe("new folder", () => {
it("requires a folder name", async () => {
await component.submit();
expect(encrypt).not.toHaveBeenCalled();
component.folderForm.controls.name.setValue("New Folder");
await component.submit();
expect(encrypt).toHaveBeenCalled();
});
it("submits a new folder view", async () => {
component.folderForm.controls.name.setValue("New Folder");
await component.submit();
const newFolder = new FolderView();
newFolder.name = "New Folder";
expect(encrypt).toHaveBeenCalledWith(newFolder);
expect(save).toHaveBeenCalled();
});
it("shows success toast after saving", async () => {
component.folderForm.controls.name.setValue("New Folder");
await component.submit();
expect(showToast).toHaveBeenCalledWith({
message: "editedFolder",
title: null,
variant: "success",
});
});
it("closes the dialog after saving", async () => {
component.folderForm.controls.name.setValue("New Folder");
await component.submit();
expect(close).toHaveBeenCalled();
});
it("logs error if saving fails", async () => {
const errorObj = new Error("Failed to save folder");
save.mockRejectedValue(errorObj);
component.folderForm.controls.name.setValue("New Folder");
await component.submit();
expect(error).toHaveBeenCalledWith(errorObj);
});
});
describe("editing folder", () => {
const folderView = new FolderView();
folderView.id = "1";
folderView.name = "Folder 1";
beforeEach(() => {
dialogData.editFolderConfig = { folder: folderView };
component.ngOnInit();
});
it("populates form with folder name", () => {
expect(component.folderForm.controls.name.value).toBe("Folder 1");
});
it("submits the updated folder", async () => {
component.folderForm.controls.name.setValue("Edited Folder");
await component.submit();
expect(encrypt).toHaveBeenCalledWith({
...dialogData.editFolderConfig.folder,
name: "Edited Folder",
});
});
it("deletes the folder", async () => {
await component.deleteFolder();
expect(deleteFolder).toHaveBeenCalledWith(folderView.id);
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,
message: "deletedFolder",
});
});
});
});

View File

@@ -0,0 +1,155 @@
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
import { CommonModule } from "@angular/common";
import {
AfterViewInit,
Component,
DestroyRef,
inject,
Inject,
OnInit,
ViewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AsyncActionsModule,
BitSubmitDirective,
ButtonComponent,
ButtonModule,
DialogModule,
DialogService,
FormFieldModule,
IconButtonModule,
ToastService,
} from "@bitwarden/components";
export type AddEditFolderDialogData = {
/** When provided, dialog will display edit folder variant */
editFolderConfig?: { folder: FolderView };
};
@Component({
standalone: true,
selector: "vault-add-edit-folder-dialog",
templateUrl: "./add-edit-folder-dialog.component.html",
imports: [
CommonModule,
JslibModule,
DialogModule,
ButtonModule,
FormFieldModule,
ReactiveFormsModule,
IconButtonModule,
AsyncActionsModule,
],
})
export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
@ViewChild(BitSubmitDirective) private bitSubmit: BitSubmitDirective;
@ViewChild("submitBtn") private submitBtn: ButtonComponent;
folder: FolderView;
variant: "add" | "edit";
folderForm = this.formBuilder.group({
name: ["", Validators.required],
});
private destroyRef = inject(DestroyRef);
constructor(
private formBuilder: FormBuilder,
private folderService: FolderService,
private folderApiService: FolderApiServiceAbstraction,
private toastService: ToastService,
private i18nService: I18nService,
private logService: LogService,
private dialogService: DialogService,
private dialogRef: DialogRef,
@Inject(DIALOG_DATA) private data?: AddEditFolderDialogData,
) {}
ngOnInit(): void {
this.variant = this.data?.editFolderConfig ? "edit" : "add";
if (this.variant === "edit") {
this.folderForm.controls.name.setValue(this.data.editFolderConfig.folder.name);
this.folder = this.data.editFolderConfig.folder;
} else {
// Create a new folder view
this.folder = new FolderView();
}
}
ngAfterViewInit(): void {
this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((loading) => {
if (!this.submitBtn) {
return;
}
this.submitBtn.loading = loading;
});
}
/** Submit the new folder */
submit = async () => {
if (this.folderForm.invalid) {
return;
}
this.folder.name = this.folderForm.controls.name.value;
try {
const folder = await this.folderService.encrypt(this.folder);
await this.folderApiService.save(folder);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("editedFolder"),
});
this.close();
} catch (e) {
this.logService.error(e);
}
};
/** Delete the folder with when the user provides a confirmation */
deleteFolder = async () => {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "deleteFolder" },
content: { key: "deleteFolderPermanently" },
type: "warning",
});
if (!confirmed) {
return;
}
try {
await this.folderApiService.delete(this.folder.id);
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("deletedFolder"),
});
} catch (e) {
this.logService.error(e);
}
this.close();
};
/** Close the dialog */
private close() {
this.dialogRef.close();
}
}

View File

@@ -3,20 +3,25 @@
{{ "new" | i18n }}
</button>
<bit-menu #itemOptions>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Login)">
<a bitMenuItem (click)="newItemNavigate(cipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Card)">
<a bitMenuItem (click)="newItemNavigate(cipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.Identity)">
<a bitMenuItem (click)="newItemNavigate(cipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</a>
<a type="button" bitMenuItem (click)="newItemNavigate(cipherType.SecureNote)">
<a bitMenuItem (click)="newItemNavigate(cipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</a>
<bit-menu-divider></bit-menu-divider>
<button type="button" bitMenuItem (click)="openFolderDialog()">
<i class="bwi bwi-folder" slot="start" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
</bit-menu>

View File

@@ -0,0 +1,108 @@
import { CommonModule } from "@angular/common";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, DialogService, MenuModule } from "@bitwarden/components";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
import { AddEditFolderDialogComponent } from "../add-edit-folder-dialog/add-edit-folder-dialog.component";
import { NewItemDropdownV2Component, NewItemInitialValues } from "./new-item-dropdown-v2.component";
describe("NewItemDropdownV2Component", () => {
let component: NewItemDropdownV2Component;
let fixture: ComponentFixture<NewItemDropdownV2Component>;
const open = jest.fn();
const navigate = jest.fn();
beforeEach(async () => {
open.mockClear();
navigate.mockClear();
await TestBed.configureTestingModule({
imports: [NewItemDropdownV2Component, MenuModule, ButtonModule, JslibModule, CommonModule],
providers: [
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: Router, useValue: { navigate } },
],
})
.overrideProvider(DialogService, { useValue: { open } })
.compileComponents();
fixture = TestBed.createComponent(NewItemDropdownV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("opens new folder dialog", () => {
component.openFolderDialog();
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent);
});
describe("new item", () => {
const emptyParams: AddEditQueryParams = {
collectionId: undefined,
organizationId: undefined,
folderId: undefined,
};
beforeEach(() => {
jest.spyOn(component, "newItemNavigate");
});
it("navigates to new login", () => {
component.newItemNavigate(CipherType.Login);
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
queryParams: { type: CipherType.Login.toString(), ...emptyParams },
});
});
it("navigates to new card", () => {
component.newItemNavigate(CipherType.Card);
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
queryParams: { type: CipherType.Card.toString(), ...emptyParams },
});
});
it("navigates to new identity", () => {
component.newItemNavigate(CipherType.Identity);
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
queryParams: { type: CipherType.Identity.toString(), ...emptyParams },
});
});
it("navigates to new note", () => {
component.newItemNavigate(CipherType.SecureNote);
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
queryParams: { type: CipherType.SecureNote.toString(), ...emptyParams },
});
});
it("includes initial values", () => {
component.initialValues = {
folderId: "222-333-444",
organizationId: "444-555-666",
collectionId: "777-888-999",
} as NewItemInitialValues;
component.newItemNavigate(CipherType.Login);
expect(navigate).toHaveBeenCalledWith(["/add-cipher"], {
queryParams: {
type: CipherType.Login.toString(),
folderId: "222-333-444",
organizationId: "444-555-666",
collectionId: "777-888-999",
},
});
});
});
});

View File

@@ -5,9 +5,10 @@ import { Router, RouterLink } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { ButtonModule, MenuModule, NoItemsModule } from "@bitwarden/components";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
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;
@@ -30,7 +31,10 @@ export class NewItemDropdownV2Component {
@Input()
initialValues: NewItemInitialValues;
constructor(private router: Router) {}
constructor(
private router: Router,
private dialogService: DialogService,
) {}
private buildQueryParams(type: CipherType): AddEditQueryParams {
return {
@@ -44,4 +48,8 @@ export class NewItemDropdownV2Component {
newItemNavigate(type: CipherType) {
void this.router.navigate(["/add-cipher"], { queryParams: this.buildQueryParams(type) });
}
openFolderDialog() {
this.dialogService.open(AddEditFolderDialogComponent);
}
}

View File

@@ -0,0 +1,47 @@
<popup-page>
<popup-header slot="header" [pageTitle]="'folders' | i18n" showBackButton>
<ng-container slot="end">
<app-new-item-dropdown></app-new-item-dropdown>
<app-pop-out></app-pop-out>
</ng-container>
</popup-header>
<ng-container *ngIf="folders$ | async as folders">
<ng-container *ngIf="folders.length; else noFolders">
<bit-item-group>
<bit-item *ngFor="let folder of folders">
<bit-item-content>
{{ folder.name }}
<button
slot="end"
type="button"
(click)="openAddEditFolderDialog(folder)"
[appA11yTitle]="'editFolder' | i18n"
bitIconButton="bwi-pencil-square"
class="tw-self-end"
data-testid="edit-folder-button"
></button>
</bit-item-content>
</bit-item>
</bit-item-group>
</ng-container>
<ng-template #noFolders>
<bit-no-items [icon]="NoFoldersIcon" class="tw-h-full tw-flex tw-items-center">
<ng-container slot="title">{{ "noFoldersAdded" | i18n }}</ng-container>
<ng-container slot="description">{{ "createFoldersToOrganize" | i18n }}</ng-container>
<button
bitButton
buttonType="primary"
type="button"
slot="button"
(click)="openAddEditFolderDialog()"
data-testid="empty-new-folder-button"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "newFolder" | i18n }}
</button>
</bit-no-items>
</ng-template>
</ng-container>
</popup-page>

View File

@@ -0,0 +1,115 @@
import { Component, Input } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
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 { 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";
@Component({
standalone: true,
selector: "popup-header",
template: `<ng-content></ng-content>`,
})
class MockPopupHeaderComponent {
@Input() pageTitle: string;
@Input() backAction: () => void;
}
@Component({
standalone: true,
selector: "popup-footer",
template: `<ng-content></ng-content>`,
})
class MockPopupFooterComponent {
@Input() pageTitle: string;
}
describe("FoldersV2Component", () => {
let component: FoldersV2Component;
let fixture: ComponentFixture<FoldersV2Component>;
const folderViews$ = new BehaviorSubject<FolderView[]>([]);
const open = jest.fn();
beforeEach(async () => {
open.mockClear();
await TestBed.configureTestingModule({
imports: [FoldersV2Component],
providers: [
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: FolderService, useValue: { folderViews$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
],
})
.overrideComponent(FoldersV2Component, {
remove: {
imports: [PopupHeaderComponent, PopupFooterComponent],
},
add: {
imports: [MockPopupHeaderComponent, MockPopupFooterComponent],
},
})
.overrideProvider(DialogService, { useValue: { open } })
.compileComponents();
fixture = TestBed.createComponent(FoldersV2Component);
component = fixture.componentInstance;
fixture.detectChanges();
});
beforeEach(() => {
folderViews$.next([
{ id: "1", name: "Folder 1" },
{ id: "2", name: "Folder 2" },
{ id: "0", name: "No Folder" },
] as FolderView[]);
fixture.detectChanges();
});
it("removes the last option in the folder array", (done) => {
component.folders$.subscribe((folders) => {
expect(folders).toEqual([
{ id: "1", name: "Folder 1" },
{ id: "2", name: "Folder 2" },
]);
done();
});
});
it("opens edit dialog for existing folder", () => {
const folder = { id: "1", name: "Folder 1" } as FolderView;
const editButton = fixture.debugElement.query(By.css('[data-testid="edit-folder-button"]'));
editButton.triggerEventHandler("click");
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, {
data: { editFolderConfig: { folder } },
});
});
it("opens add dialog for new folder when there are no folders", () => {
folderViews$.next([]);
fixture.detectChanges();
const addButton = fixture.debugElement.query(By.css('[data-testid="empty-new-folder-button"]'));
addButton.triggerEventHandler("click");
expect(open).toHaveBeenCalledWith(AddEditFolderDialogComponent, { data: {} });
});
});

View File

@@ -0,0 +1,76 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { map, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AsyncActionsModule,
ButtonModule,
DialogService,
IconButtonModule,
Icons,
} from "@bitwarden/components";
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";
import { NewItemDropdownV2Component } from "../components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
@Component({
standalone: true,
templateUrl: "./folders-v2.component.html",
imports: [
CommonModule,
JslibModule,
NewItemDropdownV2Component,
PopOutComponent,
PopupPageComponent,
PopupHeaderComponent,
ItemModule,
ItemGroupComponent,
NoItemsModule,
IconButtonModule,
ButtonModule,
AsyncActionsModule,
],
})
export class FoldersV2Component {
folders$: Observable<FolderView[]>;
NoFoldersIcon = Icons.NoFolders;
constructor(
private folderService: FolderService,
private dialogService: DialogService,
) {
this.folders$ = this.folderService.folderViews$.pipe(
map((folders) => {
// Remove the last folder, which is the "no folder" option folder
if (folders.length > 0) {
return folders.slice(0, folders.length - 1);
}
return folders;
}),
);
}
/** Open the Add/Edit folder dialog */
openAddEditFolderDialog(folder?: FolderView) {
// If a folder is provided, the edit variant should be shown
const editFolderConfig = folder ? { folder } : undefined;
this.dialogService.open<unknown, AddEditFolderDialogData>(AddEditFolderDialogComponent, {
data: { editFolderConfig },
});
}
}