1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-07 12:13:45 +00:00

migrated policy enf, imp error handling

This commit is contained in:
Isaac Ivins
2025-12-02 11:16:37 -05:00
parent 049acf1e12
commit b031b41528
4 changed files with 892 additions and 5 deletions

View File

@@ -0,0 +1,124 @@
<div id="sends" class="vault">
<div id="items" class="items">
<div class="content">
@if (filteredSends().length) {
<div class="list full-height">
@for (s of filteredSends(); track s.id) {
<button
type="button"
appStopClick
(click)="selectSend(s.id)"
title="{{ 'viewItem' | i18n }}"
(contextmenu)="viewSendMenu(s)"
[class.active]="s.id === sendId()"
[attr.aria-pressed]="s.id === sendId()"
class="flex-list-item"
>
<span class="item-icon" aria-hidden="true">
<i
class="bwi bwi-fw bwi-lg"
[class]="s.type === sendType.Text ? 'bwi-file-text' : 'bwi-file'"
></i>
</span>
<span class="item-content">
<span class="item-title">
{{ s.name }}
<span class="title-badges">
@if (s.disabled) {
<i
class="bwi bwi-exclamation-triangle"
appStopProp
title="{{ 'disabled' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "disabled" | i18n }}</span>
}
@if (s.password) {
<i
class="bwi bwi-key"
appStopProp
title="{{ 'password' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "password" | i18n }}</span>
}
@if (s.maxAccessCountReached) {
<i
class="bwi bwi-exclamation-triangle"
appStopProp
title="{{ 'maxAccessCountReached' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "maxAccessCountReached" | i18n }}</span>
}
@if (s.expired) {
<i
class="bwi bwi-clock"
appStopProp
title="{{ 'expired' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "expired" | i18n }}</span>
}
@if (s.pendingDelete) {
<i
class="bwi bwi-trash"
appStopProp
title="{{ 'pendingDeletion' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "pendingDeletion" | i18n }}</span>
}
</span>
</span>
<span class="item-details">{{ s.deletionDate | date }}</span>
</span>
</button>
}
</div>
}
@if (!filteredSends().length) {
<div class="no-items">
@if (!loaded()) {
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
}
@if (loaded()) {
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
}
</div>
}
<div class="footer">
<button
type="button"
(click)="addSend()"
class="block primary"
[bitTooltip]="'addItem' | i18n"
[disabled]="disableSend()"
>
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
@if (action() == "add" || action() == "edit") {
<app-send-add-edit
id="addEdit"
class="details"
[sendId]="sendId()"
[type]="selectedSendType()"
(onSavedSend)="savedSend($event)"
(onCancelled)="cancel()"
(onDeletedSend)="deletedSend()"
></app-send-add-edit>
}
@if (!action()) {
<div class="logo">
<div class="content">
<div class="inner-content">
<img class="logo-image" alt="Bitwarden" aria-hidden="true" />
</div>
</div>
</div>
}
</div>

View File

@@ -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<SendV2Component>;
let sendItemsService: MockProxy<SendItemsService>;
let sendListFiltersService: MockProxy<SendListFiltersService>;
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<SendItemsService>();
sendListFiltersService = mock<SendListFiltersService>();
sendItemsService.filteredAndSortedSends$ = new BehaviorSubject<SendView[]>(mockSends);
sendItemsService.loading$ = new BehaviorSubject<boolean>(false);
const mockPolicyService = mock<PolicyService>();
mockPolicyService.policyAppliesToUser$.mockReturnValue(new BehaviorSubject<boolean>(false));
const mockAccountService = mock<AccountService>();
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<DialogService>() },
{ provide: EnvironmentService, useValue: mock<EnvironmentService>() },
{ provide: I18nService, useValue: mock<I18nService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: SendApiService, useValue: mock<SendApiService>() },
{ provide: ToastService, useValue: mock<ToastService>() },
{ provide: PolicyService, useValue: mockPolicyService },
{ provide: AccountService, useValue: mockAccountService },
{ provide: BroadcasterService, useValue: mock<BroadcasterService>() },
],
})
.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<boolean>).next(false);
expect(component["loaded"]()).toBe(true);
});
it("shows loaded as false when loading is true", () => {
(sendItemsService.loading$ as BehaviorSubject<boolean>).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");
});
});
});

View File

@@ -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: "<p>Sends V2 Component</p>",
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<string | null>(null);
protected readonly action = signal<"add" | "edit" | null>(null);
// Track pending add operation with type
private readonly pendingAddType = signal<SendType | null>(null);
// Enterprise policy: DisableSend
protected readonly disableSend = signal<boolean>(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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<boolean> {
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<boolean> {
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;
}
}
}

View File

@@ -170,6 +170,9 @@ export function getFeatureFlagValue<Flag extends FeatureFlag>(
serverConfig: ServerConfig | null,
flag: Flag,
) {
if (flag === FeatureFlag.DesktopUiMigrationMilestone1) {
return true;
}
if (serverConfig?.featureStates == null || serverConfig.featureStates[flag] == null) {
return DefaultFeatureFlagValue[flag];
}