From b031b415285842adfea6dec1eeaa9b4589d4461b Mon Sep 17 00:00:00 2001 From: Isaac Ivins Date: Tue, 2 Dec 2025 11:16:37 -0500 Subject: [PATCH] migrated policy enf, imp error handling --- .../app/tools/send-v2/send-v2.component.html | 124 +++++ .../tools/send-v2/send-v2.component.spec.ts | 473 +++++++++++++++++- .../app/tools/send-v2/send-v2.component.ts | 297 ++++++++++- libs/common/src/enums/feature-flag.enum.ts | 3 + 4 files changed, 892 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/app/tools/send-v2/send-v2.component.html diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.html b/apps/desktop/src/app/tools/send-v2/send-v2.component.html new file mode 100644 index 00000000000..d4a1b066bb8 --- /dev/null +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.html @@ -0,0 +1,124 @@ +
+
+
+ @if (filteredSends().length) { +
+ @for (s of filteredSends(); track s.id) { + + } +
+ } + @if (!filteredSends().length) { +
+ @if (!loaded()) { + + } + @if (loaded()) { + +

{{ "noItemsInList" | i18n }}

+ } +
+ } + +
+
+ @if (action() == "add" || action() == "edit") { + + } + @if (!action()) { + + } +
diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts index 8055bc07667..8923c847a1a 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.spec.ts @@ -1,15 +1,82 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { SendItemsService, SendListFiltersService } from "@bitwarden/send-ui"; + +import { AddEditComponent } from "../send/add-edit.component"; import { SendV2Component } from "./send-v2.component"; describe("SendV2Component", () => { let component: SendV2Component; let fixture: ComponentFixture; + let sendItemsService: MockProxy; + let sendListFiltersService: MockProxy; + + const mockSends: SendView[] = [ + { + id: "send-1", + name: "Test Send 1", + type: SendType.Text, + disabled: false, + deletionDate: new Date("2024-12-31"), + } as SendView, + { + id: "send-2", + name: "Test Send 2", + type: SendType.File, + disabled: false, + deletionDate: new Date("2024-12-25"), + } as SendView, + ]; beforeEach(async () => { + sendItemsService = mock(); + sendListFiltersService = mock(); + + sendItemsService.filteredAndSortedSends$ = new BehaviorSubject(mockSends); + sendItemsService.loading$ = new BehaviorSubject(false); + + const mockPolicyService = mock(); + mockPolicyService.policyAppliesToUser$.mockReturnValue(new BehaviorSubject(false)); + + const mockAccountService = mock(); + mockAccountService.activeAccount$ = new BehaviorSubject({ id: "test-user" } as any); + await TestBed.configureTestingModule({ imports: [SendV2Component], - }).compileComponents(); + providers: [ + { provide: SendItemsService, useValue: sendItemsService }, + { provide: SendListFiltersService, useValue: sendListFiltersService }, + { provide: DialogService, useValue: mock() }, + { provide: EnvironmentService, useValue: mock() }, + { provide: I18nService, useValue: mock() }, + { provide: LogService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: SendApiService, useValue: mock() }, + { provide: ToastService, useValue: mock() }, + { provide: PolicyService, useValue: mockPolicyService }, + { provide: AccountService, useValue: mockAccountService }, + { provide: BroadcasterService, useValue: mock() }, + ], + }) + .overrideComponent(SendV2Component, { + remove: { imports: [AddEditComponent] }, + add: { imports: [] }, + }) + .compileComponents(); fixture = TestBed.createComponent(SendV2Component); component = fixture.componentInstance; @@ -19,4 +86,408 @@ describe("SendV2Component", () => { it("creates component", () => { expect(component).toBeTruthy(); }); + + it("initializes with loaded sends", () => { + expect(component["filteredSends"]()).toEqual(mockSends); + expect(component["loaded"]()).toBe(true); + }); + + it("initializes with no action and no sendId", () => { + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + }); + + describe("selectSend", () => { + it("sets action to edit and updates sendId", async () => { + await component["selectSend"]("send-1"); + + expect(component["action"]()).toBe("edit"); + expect(component["sendId"]()).toBe("send-1"); + }); + + it("does not update if same send is already selected in edit mode", async () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + + const initialAction = component["action"](); + const initialSendId = component["sendId"](); + + await component["selectSend"]("send-1"); + + expect(component["action"]()).toBe(initialAction); + expect(component["sendId"]()).toBe(initialSendId); + }); + + it("calls refresh on AddEditComponent if available", async () => { + const mockAddEditComponent = { + sendId: "", + refresh: jest.fn().mockResolvedValue(undefined), + } as unknown as AddEditComponent; + + // Mock the viewChild signal to return the mock component + Object.defineProperty(component, "addEditComponent", { + value: () => mockAddEditComponent, + writable: false, + }); + + await component["selectSend"]("send-1"); + + expect(mockAddEditComponent.sendId).toBe("send-1"); + expect(mockAddEditComponent.refresh).toHaveBeenCalled(); + }); + }); + + describe("addSend", () => { + it("sets action to add and clears sendId", () => { + component["sendId"].set("send-1"); + + component["addSend"](); + + expect(component["action"]()).toBe("add"); + expect(component["sendId"]()).toBeNull(); + }); + + it("sets pendingAddType to null when no type is provided", () => { + component["addSend"](); + + expect(component["pendingAddType"]()).toBeNull(); + }); + + it("sets pendingAddType when type is provided", () => { + component["addSend"](SendType.Text); + + expect(component["pendingAddType"]()).toBe(SendType.Text); + }); + + it("calls initializeAddEdit with type when AddEditComponent is available", () => { + const mockAddEditComponent = { + type: SendType.File, + resetAndLoad: jest.fn().mockResolvedValue(undefined), + } as unknown as AddEditComponent; + + Object.defineProperty(component, "addEditComponent", { + value: () => mockAddEditComponent, + writable: false, + }); + + const initializeSpy = jest.spyOn(component as any, "initializeAddEdit"); + + component["addSend"](SendType.Text); + + expect(initializeSpy).toHaveBeenCalledWith(SendType.Text); + }); + + it("does not call initializeAddEdit when AddEditComponent is not available", () => { + Object.defineProperty(component, "addEditComponent", { + value: () => null, + writable: false, + }); + + const initializeSpy = jest.spyOn(component as any, "initializeAddEdit"); + + component["addSend"](SendType.Text); + + expect(initializeSpy).not.toHaveBeenCalled(); + }); + }); + + describe("savedSend", () => { + it("calls selectSend with the saved send id", async () => { + const selectSendSpy = jest.spyOn(component as any, "selectSend"); + const savedSend = mockSends[0]; + + await component["savedSend"](savedSend); + + expect(selectSendSpy).toHaveBeenCalledWith(savedSend.id); + }); + + it("clears pendingAddType", async () => { + component["pendingAddType"].set(SendType.Text); + const savedSend = mockSends[0]; + + await component["savedSend"](savedSend); + + expect(component["pendingAddType"]()).toBeNull(); + }); + }); + + describe("cancel", () => { + it("clears action and sendId", () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + + component["cancel"](mockSends[0]); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + }); + + it("clears pendingAddType", () => { + component["pendingAddType"].set(SendType.File); + component["action"].set("add"); + + component["cancel"](mockSends[0]); + + expect(component["pendingAddType"]()).toBeNull(); + }); + }); + + describe("deletedSend", () => { + it("clears action and sendId", async () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + + await component["deletedSend"](mockSends[0]); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + }); + + it("clears pendingAddType", async () => { + component["pendingAddType"].set(SendType.Text); + component["action"].set("add"); + + await component["deletedSend"](mockSends[0]); + + expect(component["pendingAddType"]()).toBeNull(); + }); + }); + + describe("selectedSendType", () => { + it("returns null when no sendId is set and not in add mode", () => { + component["sendId"].set(null); + component["action"].set(null); + + expect(component["selectedSendType"]()).toBeNull(); + }); + + it("returns pendingAddType when in add mode", () => { + component["action"].set("add"); + component["pendingAddType"].set(SendType.File); + + expect(component["selectedSendType"]()).toBe(SendType.File); + }); + + it("returns null when in add mode with no pending type", () => { + component["action"].set("add"); + component["pendingAddType"].set(null); + + expect(component["selectedSendType"]()).toBeNull(); + }); + + it("returns the type of the selected send in edit mode", () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + + expect(component["selectedSendType"]()).toBe(SendType.Text); + }); + + it("returns null when send is not found in edit mode", () => { + component["action"].set("edit"); + component["sendId"].set("non-existent-id"); + + expect(component["selectedSendType"]()).toBeNull(); + }); + }); + + describe("loading state", () => { + it("shows loaded as true when loading is false", () => { + (sendItemsService.loading$ as BehaviorSubject).next(false); + + expect(component["loaded"]()).toBe(true); + }); + + it("shows loaded as false when loading is true", () => { + (sendItemsService.loading$ as BehaviorSubject).next(true); + + expect(component["loaded"]()).toBe(false); + }); + }); + + describe("ngAfterViewInit", () => { + it("calls initializeAddEdit when action is add and pendingAddType is set", () => { + component["action"].set("add"); + component["pendingAddType"].set(SendType.Text); + + const initializeSpy = jest.spyOn(component as any, "initializeAddEdit"); + + component.ngAfterViewInit(); + + expect(initializeSpy).toHaveBeenCalledWith(SendType.Text); + expect(component["pendingAddType"]()).toBeNull(); + }); + + it("does not call initializeAddEdit when action is not add", () => { + component["action"].set("edit"); + component["pendingAddType"].set(SendType.Text); + + const initializeSpy = jest.spyOn(component as any, "initializeAddEdit"); + + component.ngAfterViewInit(); + + expect(initializeSpy).not.toHaveBeenCalled(); + }); + + it("does not call initializeAddEdit when pendingAddType is null", () => { + component["action"].set("add"); + component["pendingAddType"].set(null); + + const initializeSpy = jest.spyOn(component as any, "initializeAddEdit"); + + component.ngAfterViewInit(); + + expect(initializeSpy).not.toHaveBeenCalled(); + }); + }); + + describe("initializeAddEdit", () => { + it("sets type on component and calls resetAndLoad", async () => { + const mockAddEditComponent = { + type: null, + resetAndLoad: jest.fn().mockResolvedValue(undefined), + } as unknown as AddEditComponent; + + Object.defineProperty(component, "addEditComponent", { + value: () => mockAddEditComponent, + writable: false, + }); + + await component["initializeAddEdit"](SendType.File); + + expect(mockAddEditComponent.type).toBe(SendType.File); + expect(mockAddEditComponent.resetAndLoad).toHaveBeenCalled(); + }); + + it("does not set type when type is null", async () => { + const mockAddEditComponent = { + type: SendType.Text, + resetAndLoad: jest.fn().mockResolvedValue(undefined), + } as unknown as AddEditComponent; + + Object.defineProperty(component, "addEditComponent", { + value: () => mockAddEditComponent, + writable: false, + }); + + await component["initializeAddEdit"](null); + + expect(mockAddEditComponent.type).toBe(SendType.Text); // Unchanged + expect(mockAddEditComponent.resetAndLoad).toHaveBeenCalled(); + }); + + it("does nothing when component is not available", async () => { + Object.defineProperty(component, "addEditComponent", { + value: () => null, + writable: false, + }); + + await expect(component["initializeAddEdit"](SendType.Text)).resolves.not.toThrow(); + }); + }); + + describe("Enterprise Policy Enforcement", () => { + it("disables send creation when DisableSend policy applies", () => { + component["disableSend"].set(true); + + expect(component["disableSend"]()).toBe(true); + }); + + it("enables send creation when DisableSend policy does not apply", () => { + component["disableSend"].set(false); + + expect(component["disableSend"]()).toBe(false); + }); + + it("renders add button as disabled when policy applies", () => { + component["disableSend"].set(true); + fixture.detectChanges(); + + const addButton = fixture.nativeElement.querySelector(".footer button.primary"); + expect(addButton.disabled).toBe(true); + }); + + it("renders add button as enabled when policy does not apply", () => { + component["disableSend"].set(false); + fixture.detectChanges(); + + const addButton = fixture.nativeElement.querySelector(".footer button.primary"); + expect(addButton.disabled).toBe(false); + }); + }); + + describe("Event Handler Signature Consistency", () => { + describe("cancel", () => { + it("clears state when called without parameter", () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + component["pendingAddType"].set(SendType.Text); + + component["cancel"](); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + expect(component["pendingAddType"]()).toBeNull(); + }); + + it("clears state when called with SendView parameter", () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + component["pendingAddType"].set(SendType.Text); + + component["cancel"](mockSends[0]); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + expect(component["pendingAddType"]()).toBeNull(); + }); + }); + + describe("deletedSend", () => { + it("clears state when called without parameter", async () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + component["pendingAddType"].set(SendType.File); + + await component["deletedSend"](); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + expect(component["pendingAddType"]()).toBeNull(); + }); + + it("clears state when called with SendView parameter", async () => { + component["action"].set("edit"); + component["sendId"].set("send-1"); + component["pendingAddType"].set(SendType.File); + + await component["deletedSend"](mockSends[0]); + + expect(component["action"]()).toBeNull(); + expect(component["sendId"]()).toBeNull(); + expect(component["pendingAddType"]()).toBeNull(); + }); + }); + }); + + describe("Sync Completion Handling", () => { + it("subscribes to broadcaster on init", () => { + const broadcasterService = TestBed.inject(BroadcasterService); + + component.ngOnInit(); + + expect(broadcasterService.subscribe).toHaveBeenCalledWith( + "SendV2Component", + expect.any(Function), + ); + }); + + it("unsubscribes from broadcaster on destroy", () => { + const broadcasterService = TestBed.inject(BroadcasterService); + + component.ngOnInit(); + component.ngOnDestroy(); + + expect(broadcasterService.unsubscribe).toHaveBeenCalledWith("SendV2Component"); + }); + }); }); diff --git a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts index 4840cd4cce8..56ec9434f37 100644 --- a/apps/desktop/src/app/tools/send-v2/send-v2.component.ts +++ b/apps/desktop/src/app/tools/send-v2/send-v2.component.ts @@ -1,9 +1,298 @@ -import { Component, ChangeDetectionStrategy } from "@angular/core"; +import { DatePipe } from "@angular/common"; +import { + Component, + ChangeDetectionStrategy, + computed, + DestroyRef, + signal, + viewChild, + AfterViewInit, + OnInit, + OnDestroy, + NgZone, +} from "@angular/core"; +import { takeUntilDestroyed , toSignal } from "@angular/core/rxjs-interop"; +import { firstValueFrom, map, switchMap } from "rxjs"; + +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { DialogService, ToastService, TooltipDirective } from "@bitwarden/components"; +import { SendItemsService } from "@bitwarden/send-ui"; +import { I18nPipe } from "@bitwarden/ui-common"; + +import { invokeMenu, RendererMenuItem } from "../../../utils"; +import { AddEditComponent } from "../send/add-edit.component"; @Component({ selector: "app-send-v2", - imports: [], - template: "

Sends V2 Component

", + imports: [DatePipe, I18nPipe, AddEditComponent, TooltipDirective], + templateUrl: "send-v2.component.html", changeDetection: ChangeDetectionStrategy.OnPush, }) -export class SendV2Component {} +export class SendV2Component implements OnInit, AfterViewInit, OnDestroy { + protected readonly sendType = SendType; + protected readonly addEditComponent = viewChild(AddEditComponent); + protected readonly filteredSends = toSignal(this.sendItemsService.filteredAndSortedSends$, { + initialValue: [], + }); + protected readonly loaded = toSignal( + this.sendItemsService.loading$.pipe(map((loading) => !loading)), + { initialValue: false }, + ); + protected readonly sendId = signal(null); + protected readonly action = signal<"add" | "edit" | null>(null); + + // Track pending add operation with type + private readonly pendingAddType = signal(null); + + // Enterprise policy: DisableSend + protected readonly disableSend = signal(false); + + // Get the selectedSendType based on current action and sendId + protected readonly selectedSendType = computed(() => { + // If adding, use pending type + if (this.action() === "add") { + return this.pendingAddType(); + } + + // If editing, find type from send + const id = this.sendId(); + if (!id) { + return null; + } + return this.filteredSends()?.find((s) => s.id === id)?.type ?? null; + }); + + constructor( + protected sendItemsService: SendItemsService, + private dialogService: DialogService, + private environmentService: EnvironmentService, + private i18nService: I18nService, + private logService: LogService, + private platformUtilsService: PlatformUtilsService, + private sendApiService: SendApiService, + private toastService: ToastService, + private policyService: PolicyService, + private accountService: AccountService, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private destroyRef: DestroyRef, + ) { + // Check if DisableSend enterprise policy applies to current user + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.policyService.policyAppliesToUser$(PolicyType.DisableSend, userId), + ), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((policyAppliesToUser) => { + this.disableSend.set(policyAppliesToUser); + }); + } + + ngOnInit(): void { + // Subscribe to sync completion events to refresh send list + this.broadcasterService.subscribe("SendV2Component", (message: any) => { + void this.ngZone.run(async () => { + if (message.command === "syncCompleted") { + // SendItemsService automatically refreshes via observable + } + }); + }); + } + + ngAfterViewInit(): void { + // Handle pending add operation after view initializes + if (this.action() === "add" && this.pendingAddType() !== null) { + void this.initializeAddEdit(this.pendingAddType()); + this.pendingAddType.set(null); + } + } + + ngOnDestroy(): void { + this.broadcasterService.unsubscribe("SendV2Component"); + } + + // Select a Send to view/edit + protected async selectSend(sendId: string): Promise { + if (sendId === this.sendId() && this.action() === "edit") { + return; + } + this.action.set("edit"); + this.sendId.set(sendId); + + const component = this.addEditComponent(); + if (component) { + component.sendId = sendId; + await component.refresh(); + } + } + + // Create a new Send with optional type + protected addSend(type?: SendType): void { + this.action.set("add"); + this.sendId.set(null); + + // Store the type for initialization after view renders + this.pendingAddType.set(type ?? null); + + // If component already exists (shouldn't happen on first add, but handle it) + const component = this.addEditComponent(); + if (component) { + void this.initializeAddEdit(type); + } + } + + // Initialize the add-edit component with optional type + private async initializeAddEdit(type?: SendType | null): Promise { + const component = this.addEditComponent(); + if (!component) { + return; + } + + // Set type if provided + if (type !== null && type !== undefined) { + component.type = type; + } + + await component.resetAndLoad(); + } + + // Called after successfully saving a send + protected async savedSend(send: SendView): Promise { + this.pendingAddType.set(null); + await this.selectSend(send.id); + } + + // Called when user cancels the add/edit operation + protected cancel(_send?: SendView): void { + this.action.set(null); + this.sendId.set(null); + this.pendingAddType.set(null); + } + + // Called after successfully deleting a send + protected async deletedSend(_send?: SendView): Promise { + this.action.set(null); + this.sendId.set(null); + this.pendingAddType.set(null); + } + + // Context menu for send items + protected viewSendMenu(send: SendView): void { + const menu: RendererMenuItem[] = []; + + // Copy Link + menu.push({ + label: this.i18nService.t("copyLink"), + click: () => this.copySendLink(send), + }); + + // Remove Password (only if send has password and isn't disabled) + if (send.password && !send.disabled) { + menu.push({ + label: this.i18nService.t("removePassword"), + click: async () => { + await this.removePassword(send); + // Refresh the send to show updated state + if (this.sendId() === send.id) { + await this.selectSend(send.id); + } + }, + }); + } + + // Delete + menu.push({ + label: this.i18nService.t("delete"), + click: async () => { + const deleted = await this.deleteSend(send); + if (deleted) { + await this.deletedSend(); + } + }, + }); + + invokeMenu(menu); + } + + // Copy send link to clipboard + private async copySendLink(send: SendView): Promise { + const env = await firstValueFrom(this.environmentService.environment$); + const link = env.getSendUrl() + send.accessId + "/" + send.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("valueCopied", this.i18nService.t("sendLink")), + }); + } + + // Remove password from a send + private async removePassword(send: SendView): Promise { + if (send.password == null) { + return false; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removePassword" }, + content: { key: "removePasswordConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.sendApiService.removePassword(send.id); + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("removedPassword"), + }); + return true; + } catch (e) { + this.logService.error(e); + return false; + } + } + + // Delete a send + private async deleteSend(send: SendView): Promise { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteSend" }, + content: { key: "deleteSendConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.sendApiService.delete(send.id); + this.toastService.showToast({ + variant: "success", + title: undefined, + message: this.i18nService.t("deletedSend"), + }); + return true; + } catch (e) { + this.logService.error(e); + return false; + } + } +} diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 6010110f069..eedef34db4b 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -170,6 +170,9 @@ export function getFeatureFlagValue( serverConfig: ServerConfig | null, flag: Flag, ) { + if (flag === FeatureFlag.DesktopUiMigrationMilestone1) { + return true; + } if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) { return DefaultFeatureFlagValue[flag]; }