Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
6937282
initial refactor to avoid deprecated getOrgKey() method
harr1424 Sep 8, 2025
a1b316f
added mocks necessary for testing updated KeyService methods
harr1424 Sep 8, 2025
4e68f24
added error handling to match file specific conventions
harr1424 Sep 9, 2025
6828f69
Merge branch 'PM-24099-Tools-Remove-getOrgKey-from-the-key-service' iโ€ฆ
harr1424 Sep 9, 2025
bb89cd9
initial refactor isolated to send.ts and platform services container
harr1424 Sep 9, 2025
8fff591
functional and unit tests pass after adding AccountService to global โ€ฆ
harr1424 Sep 9, 2025
0e94644
modified send.ts to use correct EncKey with test coverage
harr1424 Sep 9, 2025
30fd988
Refactor complete with passing unit tests
harr1424 Sep 9, 2025
dae7d41
PM-24105 refactoring complete with updated test coverage
harr1424 Sep 10, 2025
949f6a5
prefer using class members and testing with real GUIDs
harr1424 Sep 10, 2025
adbbf6a
prefer local userId when available & guard against null
harr1424 Sep 10, 2025
83c2b92
fix build errors
harr1424 Sep 10, 2025
a2e1081
Fix runtime errors and send regression
harr1424 Sep 10, 2025
948c6e3
PM-24099 refactor UserId collection from AccountService to wait for nโ€ฆ
harr1424 Sep 11, 2025
9ef9c32
PM-24106 fix breaking changes to send on desktop and CLI
harr1424 Sep 11, 2025
970bc9f
PM-24105 revert removal of Promise and .vscode setting
harr1424 Sep 11, 2025
38dbe74
PM-22143 initial commit to check lints
harr1424 Sep 11, 2025
196dcbe
PM-22143 WIP progress checkpoint
harr1424 Sep 11, 2025
19f5e10
PM-22143 replace all tools-owned enums with const objects
harr1424 Sep 12, 2025
0577917
PM-22143 fix build errors in browser and cli
harr1424 Sep 12, 2025
e1875a9
revert vs code settings to match origin
harr1424 Sep 12, 2025
0517e28
revert auto-generated desktop_native file
harr1424 Sep 15, 2025
19d4a02
resolve actions requested during review
harr1424 Sep 15, 2025
356ffb1
update types for enhanced runtime safety
harr1424 Sep 15, 2025
a911b08
address issues found during review
harr1424 Sep 16, 2025
30b430a
address remaining review items
harr1424 Sep 17, 2025
9b8b8aa
address review items
harr1424 Sep 17, 2025
c0255ba
Merge remote-tracking branch 'origin/main' into PM-22143-Tools-Refactโ€ฆ
harr1424 Sep 22, 2025
2314d44
Merge remote-tracking branch 'origin/main' into PM-24099-Tools-Removeโ€ฆ
harr1424 Sep 22, 2025
542e2c3
use UserId value made available in PM-24146
harr1424 Sep 22, 2025
b34f4ea
address review items
harr1424 Sep 25, 2025
4f2461d
remove org export key fallback to user key
harr1424 Sep 25, 2025
6e30700
Merge remote-tracking branch 'origin/PM-24099-Tools-Remove-getOrgKey-โ€ฆ
harr1424 Sep 28, 2025
8265b7c
use existing getUserId utility method
harr1424 Sep 29, 2025
8641595
prefer const vs type casting & remove unused helper method
harr1424 Sep 29, 2025
380fad1
add null check to fix strict type error
harr1424 Sep 29, 2025
b1b5f2f
partially address review items:
harr1424 Oct 2, 2025
8614dcf
lift UserId retrieval to occur once during import
harr1424 Oct 3, 2025
4da1fa1
Merge branch 'main' into PM-24105-Tools-Remove-getUserKey-on-the-key-โ€ฆ
harr1424 Oct 3, 2025
6d58a91
Merge branch 'main' into PM-24105-Tools-Remove-getUserKey-on-the-key-โ€ฆ
harr1424 Oct 3, 2025
a4e4a70
remove unused import not caught during commit workflow
harr1424 Oct 3, 2025
c97b17a
Merge branch 'PM-24105-Tools-Remove-getUserKey-on-the-key-service' ofโ€ฆ
harr1424 Oct 3, 2025
687ca14
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Oct 6, 2025
1d52c7a
isolate import related changes
harr1424 Oct 7, 2025
624109e
respond to review items
harr1424 Oct 8, 2025
e6c16b7
add export related code changes
harr1424 Oct 8, 2025
7fc47f2
remove redundnat userId retrieval
harr1424 Oct 8, 2025
a78c779
Merge branch 'PM-22143-import-only-enum-refactor' into PM-22143-Toolsโ€ฆ
harr1424 Oct 8, 2025
63c3049
Merge branch 'PM-24105-Tools-Remove-getUserKey-on-the-key-service' inโ€ฆ
harr1424 Oct 8, 2025
5884b00
Merge branch 'PM-22143-Tools-Refactor-TS-Enums-to-be-const-objects-EXโ€ฆ
harr1424 Oct 8, 2025
95f07c6
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Oct 15, 2025
af4da3e
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Nov 20, 2025
2993d93
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Nov 20, 2025
ee785f2
respond to review comments
harr1424 Nov 20, 2025
57a7ba6
respond to review comment (remove unused helpers)
harr1424 Nov 20, 2025
992f11f
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Nov 20, 2025
4345d72
Merge branch 'main' into PM-22143-Tools-Refactor-TS-Enums-to-be-constโ€ฆ
harr1424 Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ import { SendFilePopoutDialogContainerComponent } from "../send-file-popout-dial
class QueryParams {
constructor(params: Params) {
this.sendId = params.sendId;
this.type = parseInt(params.type, 10);
const sendTypeValue = parseInt(params.type, 10);
if (sendTypeValue === SendType.Text || sendTypeValue === SendType.File) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SendType.Text, SendType.File].indexOf(sendTypeValue) !== -1

could be

this.type = sendTypeValue;
} else {
throw new Error(`Invalid SendType: ${params.type}`);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service";

import { SendV2Component, SendState } from "./send-v2.component";
import { SendV2Component } from "./send-v2.component";

describe("SendV2Component", () => {
let component: SendV2Component;
Expand Down Expand Up @@ -142,12 +142,12 @@ describe("SendV2Component", () => {
it("should set listState to Empty when emptyList$ emits true", () => {
sendItemsServiceEmptyList$.next(true);
fixture.detectChanges();
expect(component["listState"]).toBe(SendState.Empty);
expect(component["listState"]).toBe(component["sendState"].Empty);
Copy link
Member

@Hinton Hinton Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why are we modifying this? The SendState type is public so we should be able to continue using it?

});

it("should set listState to NoResults when noFilteredResults$ emits true", () => {
sendItemsServiceNoFilteredResults$.next(true);
fixture.detectChanges();
expect(component["listState"]).toBe(SendState.NoResults);
expect(component["listState"]).toBe(component["sendState"].NoResults);
Copy link
Member

@Hinton Hinton Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why are we modifying this? The SendState type is public so we should be able to continue using it?

});
});
22 changes: 15 additions & 7 deletions apps/browser/src/tools/popup/send-v2/send-v2.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ import { PopOutComponent } from "../../../platform/popup/components/pop-out.comp
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";

// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SendState {
Empty,
NoResults,
}
/** A state of the Send list UI. */
export const SendState = Object.freeze({
/** No sends exist for the current filter (file or text). */
Empty: "Empty",
/** Sends exist, but none match the current filter/search. */
NoResults: "NoResults",
} as const);

/** A state of the Send list UI. */
export type SendState = (typeof SendState)[keyof typeof SendState];

@Component({
templateUrl: "send-v2.component.html",
Expand Down Expand Up @@ -91,7 +95,11 @@ export class SendV2Component implements OnDestroy {
.pipe(takeUntilDestroyed())
.subscribe(([emptyList, noFilteredResults, currentFilter]) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to add types for the args

if (currentFilter?.sendType !== null) {
this.title = `${this.sendType[currentFilter.sendType].toLowerCase()}Sends`;
if (currentFilter.sendType === SendType.File) {
this.title = "fileSends";
} else if (currentFilter.sendType === SendType.Text) {
this.title = "textSends";
}
} else {
this.title = "allSends";
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be this to avoid nested if conditions

if (currentFilter?.sendType != null) {
const titleMap: Record<SendType, string> = {
[SendType.File]: 'fileSends',
[SendType.Text]: 'textSends'
};

this.title = titleMap[currentFilter.sendType] ?? 'allSends';
} else {
this.title = 'allSends';
}

Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/tools/send/send.program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export class SendProgram extends BaseProgram {
let sendFile = null;
let sendText = null;
let name = Utils.newGuid();
let type = SendType.Text;
let type: SendType = SendType.Text;
if (options.file != null) {
data = path.resolve(data);
if (!fs.existsSync(data)) {
Expand Down
19 changes: 12 additions & 7 deletions apps/desktop/src/app/tools/send/send.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,18 @@ import { SearchBarService } from "../../layout/search/search-bar.service";

import { AddEditComponent } from "./add-edit.component";

// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum Action {
None = "",
Add = "add",
Edit = "edit",
}
/** An action that can be performed in the Send UI. */
export const Action = Object.freeze({
/** No action is currently active. */
None: "",
/** The user is adding a new Send. */
Add: "add",
/** The user is editing an existing Send. */
Edit: "edit",
} as const);

/** An action that can be performed in the Send UI. */
export type Action = (typeof Action)[keyof typeof Action];

const BroadcasterSubscriptionId = "SendComponent";

Expand Down
67 changes: 67 additions & 0 deletions libs/angular/src/tools/send/add-edit.component.spec.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { DatePreset, isDatePreset, asDatePreset, nameOfDatePreset } from "./add-edit.component";

// Organized by unit: each describe block focuses on a single utility's behavior.
describe("isDatePreset", () => {
it("returns true for all valid DatePreset values (numbers, 'never', and Custom)", () => {
const validPresets: Array<any> = [
DatePreset.OneHour,
DatePreset.OneDay,
DatePreset.TwoDays,
DatePreset.ThreeDays,
DatePreset.SevenDays,
DatePreset.ThirtyDays,
DatePreset.Custom,
"never",
];
validPresets.forEach((preset) => {
expect(isDatePreset(preset)).toBe(true);
});
});

it("returns false for invalid values", () => {
const invalidPresets: Array<any> = [5, -1, 999, null, undefined, "foo", {}, []];
invalidPresets.forEach((preset) => {
expect(isDatePreset(preset)).toBe(false);
});
});
});

describe("asDatePreset", () => {
it("returns the same value for valid DatePreset inputs", () => {
const validPresets: Array<any> = [
DatePreset.OneHour,
DatePreset.OneDay,
DatePreset.TwoDays,
DatePreset.ThreeDays,
DatePreset.SevenDays,
DatePreset.ThirtyDays,
DatePreset.Custom,
"never",
];
validPresets.forEach((preset) => {
expect(asDatePreset(preset)).toBe(preset);
});
});

it("returns undefined for invalid inputs", () => {
const invalidPresets: Array<any> = [5, -1, 999, null, undefined, "foo", {}, []];
invalidPresets.forEach((preset) => {
expect(asDatePreset(preset)).toBeUndefined();
});
});
});

describe("nameOfDatePreset", () => {
it("returns the correct key for valid DatePreset values", () => {
expect(nameOfDatePreset(DatePreset.OneHour)).toBe("OneHour");
expect(nameOfDatePreset(DatePreset.Custom)).toBe("Custom");
expect(nameOfDatePreset("never")).toBe("Never");
});

it("returns undefined for invalid inputs", () => {
const invalidPresets: Array<any> = [5, -1, 999, null, undefined, "foo", {}, []];
invalidPresets.forEach((preset) => {
expect(nameOfDatePreset(preset)).toBeUndefined();
});
});
});
131 changes: 88 additions & 43 deletions libs/angular/src/tools/send/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,65 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction";
import { DialogService, ToastService } from "@bitwarden/components";

// Value = hours
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
enum DatePreset {
OneHour = 1,
OneDay = 24,
TwoDays = 48,
ThreeDays = 72,
SevenDays = 168,
ThirtyDays = 720,
Custom = 0,
Never = null,
}
/** A preset duration (in hours) for expiration/deletion. */
export const DatePreset = Object.freeze({
/** One-hour duration. */
OneHour: 1,
/** One-day duration (24 hours). */
OneDay: 24,
/** Two-day duration (48 hours). */
TwoDays: 48,
/** Three-day duration (72 hours). */
ThreeDays: 72,
/** Seven-day duration (168 hours). */
SevenDays: 168,
/** Thirty-day duration (720 hours). */
ThirtyDays: 720,
/** Custom date/time, provided by the user. */
Custom: 0,
/** No expiration. */
Never: "never",
} as const);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: This is duplicated. Can we extract this to a single file and share it in both places. OR if we consider this file to be deprecated, not modify it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Hinton please let me know your preference here, if this file is deprecated my preference would be to not modify it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that's fine with me. The file is deprecated and will be replaced with the new component so the value of updating it probably only results in additional QA effort.


/** A preset duration (in hours) for expiration/deletion. */
export type DatePreset = (typeof DatePreset)[Exclude<keyof typeof DatePreset, "Never">] | "never";

interface DatePresetSelectOption {
name: string;
value: DatePreset;
}

const namesByDatePreset = new Map<DatePreset, keyof typeof DatePreset>(
Object.entries(DatePreset).map(([k, v]) => [v, k as keyof typeof DatePreset]),
);

/**
* Checks if a value is a valid DatePreset.
* @param value - The value to check.
* @returns True if the value is a valid DatePreset, false otherwise.
*/
export function isDatePreset(value: unknown): value is DatePreset {
return namesByDatePreset.has(value as DatePreset);
}

/**
* Converts a value to a DatePreset if it is valid.
* @param value - The value to convert.
* @returns The value as a DatePreset if valid, otherwise undefined.
*/
export function asDatePreset(value: unknown): DatePreset | undefined {
return isDatePreset(value) ? (value as DatePreset) : undefined;
}

/**
* Gets the name of a DatePreset value.
* @param value - The DatePreset value to get the name for.
* @returns The name of the DatePreset value, or undefined if not found.
*/
export function nameOfDatePreset(value: DatePreset): keyof typeof DatePreset | undefined {
return namesByDatePreset.get(value);
}

@Directive()
export class AddEditComponent implements OnInit, OnDestroy {
@Input() sendId: string;
Expand All @@ -74,7 +114,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
];

expirationDatePresets: DatePresetSelectOption[] = [
{ name: this.i18nService.t("never"), value: DatePreset.Never },
{ name: this.i18nService.t("never"), value: "never" },
...this.deletionDatePresets,
];

Expand Down Expand Up @@ -114,7 +154,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
type: [],
defaultExpirationDateTime: [],
defaultDeletionDateTime: ["", Validators.required],
selectedDeletionDatePreset: [DatePreset.SevenDays, Validators.required],
selectedDeletionDatePreset: this.formBuilder.nonNullable.control<DatePreset>(
DatePreset.SevenDays,
{ validators: [Validators.required] },
),
selectedExpirationDatePreset: [],
});

Expand Down Expand Up @@ -521,40 +564,42 @@ export class AddEditComponent implements OnInit, OnDestroy {
}

get formattedExpirationDate(): string {
switch (this.formGroup.controls.selectedExpirationDatePreset.value as DatePreset) {
case DatePreset.Never:
const rawPreset = this.formGroup.controls.selectedExpirationDatePreset.value;
const preset = asDatePreset(rawPreset);
if (!isDatePreset(preset)) {
return null;
}
Comment on lines +585 to +588
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โ›๏ธ You don't need both asDatePreset and isDatePreset. isDatePreset performs a type conversion, allowing preset to be used with the proper type.

if (preset === DatePreset.Custom) {
if (!this.formGroup.controls.defaultExpirationDateTime.value) {
return null;
case DatePreset.Custom:
if (!this.formGroup.controls.defaultExpirationDateTime.value) {
return null;
}
return this.formGroup.controls.defaultExpirationDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedExpirationDatePreset.value as number) * 60 * 60 * 1000,
);
return new Date(milliseconds).toString();
}
return this.formGroup.controls.defaultExpirationDateTime.value;
}
if (typeof preset === "number") {
const now = new Date();
const milliseconds = now.setTime(now.getTime() + preset * 60 * 60 * 1000);
return new Date(milliseconds).toString();
}
return null;
}

get formattedDeletionDate(): string {
switch (this.formGroup.controls.selectedDeletionDatePreset.value as DatePreset) {
case DatePreset.Never:
this.formGroup.controls.selectedDeletionDatePreset.patchValue(DatePreset.SevenDays);
return this.formattedDeletionDate;
case DatePreset.Custom:
return this.formGroup.controls.defaultDeletionDateTime.value;
default: {
const now = new Date();
const milliseconds = now.setTime(
now.getTime() +
(this.formGroup.controls.selectedDeletionDatePreset.value as number) * 60 * 60 * 1000,
);
return new Date(milliseconds).toString();
}
const rawPreset = this.formGroup.controls.selectedDeletionDatePreset.value;
const preset = asDatePreset(rawPreset); // what if this fails?
if (!isDatePreset(preset)) {
// is this reasonable as a safeguard? values come from a select control which should be trusted
return null;
}
if (preset === "never") {
//FIXME Getters should not have side effects
this.formGroup.controls.selectedDeletionDatePreset.patchValue(DatePreset.SevenDays);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Getters should not have side effects. Since this component likely relies upon it, please add a FIXME.

return this.formattedDeletionDate;
}
if (preset === DatePreset.Custom) {
return this.formGroup.controls.defaultDeletionDateTime.value;
}
const now = new Date();
const milliseconds = now.setTime(now.getTime() + preset * 60 * 60 * 1000);
return new Date(milliseconds).toString();
}
}
16 changes: 10 additions & 6 deletions libs/common/src/tools/send/enums/send-type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// FIXME: update to use a const object instead of a typescript enum
// eslint-disable-next-line @bitwarden/platform/no-enums
export enum SendType {
Text = 0,
File = 1,
}
/** A type of Send. */
export const SendType = Object.freeze({
/** Send contains plain text. */
Text: 0,
/** Send contains a file. */
File: 1,
} as const);

/** A type of Send. */
export type SendType = (typeof SendType)[keyof typeof SendType];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if these tests actually test anything valuable beyond that freeze and constant behave as you'd expect. But also no harm in keeping the tests.

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
SendItemDialogResult,
isSendItemDialogResult,
asSendItemDialogResult,
nameOfSendItemDialogResult,
} from "./send-add-edit-dialog.component";

describe("SendItemDialogResult utilities", () => {
it("accepts known results", () => {
const results: Array<any> = [SendItemDialogResult.Saved, SendItemDialogResult.Deleted];
results.forEach((r) => {
expect(isSendItemDialogResult(r)).toBe(true);
expect(asSendItemDialogResult(r)).toBe(r);
expect(nameOfSendItemDialogResult(r)).toBeDefined();
});
});

it("rejects invalid values", () => {
const invalid: Array<any> = ["save", "del", 0, 1, null, undefined, {}, []];
invalid.forEach((v) => {
expect(isSendItemDialogResult(v)).toBe(false);
expect(asSendItemDialogResult(v)).toBeUndefined();
expect(nameOfSendItemDialogResult(v as any)).toBeUndefined();
});
});

it("returns the correct key name", () => {
expect(nameOfSendItemDialogResult(SendItemDialogResult.Saved)).toBe("Saved");
expect(nameOfSendItemDialogResult(SendItemDialogResult.Deleted)).toBe("Deleted");
});
});
Loading
Loading