diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index 3481ced35dc..8f30d00cc31 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -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) { + this.type = sendTypeValue; + } else { + throw new Error(`Invalid SendType: ${params.type}`); + } } /** diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 63ede7ba357..6d79f430a37 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -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 { SendState, SendV2Component } from "./send-v2.component"; describe("SendV2Component", () => { let component: SendV2Component; diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts index e3baba53c42..89769bdd1ce 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -38,12 +38,16 @@ import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-heade import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; import { VaultFadeInOutSkeletonComponent } from "../../../vault/popup/components/vault-fade-in-out-skeleton/vault-fade-in-out-skeleton.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]; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -114,6 +118,11 @@ export class SendV2Component implements OnDestroy { protected sendsDisabled = false; + private readonly sendTypeTitles: Record = { + [SendType.File]: "fileSends", + [SendType.Text]: "textSends", + }; + constructor( protected sendItemsService: SendItemsService, protected sendListFiltersService: SendListFiltersService, @@ -130,7 +139,7 @@ export class SendV2Component implements OnDestroy { .pipe(takeUntilDestroyed()) .subscribe(([emptyList, noFilteredResults, currentFilter]) => { if (currentFilter?.sendType !== null) { - this.title = `${this.sendType[currentFilter.sendType].toLowerCase()}Sends`; + this.title = this.sendTypeTitles[currentFilter.sendType] ?? "allSends"; } else { this.title = "allSends"; } diff --git a/apps/cli/src/tools/send/send.program.ts b/apps/cli/src/tools/send/send.program.ts index 6c643e04cd0..33bf4518ccd 100644 --- a/apps/cli/src/tools/send/send.program.ts +++ b/apps/cli/src/tools/send/send.program.ts @@ -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)) { diff --git a/apps/desktop/src/app/tools/send/send.component.ts b/apps/desktop/src/app/tools/send/send.component.ts index 3605ca3d2dc..b58c3961bde 100644 --- a/apps/desktop/src/app/tools/send/send.component.ts +++ b/apps/desktop/src/app/tools/send/send.component.ts @@ -25,13 +25,16 @@ 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", -} +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); + +type Action = (typeof Action)[keyof typeof Action]; const BroadcasterSubscriptionId = "SendComponent"; diff --git a/libs/common/src/tools/send/enums/send-type.ts b/libs/common/src/tools/send/enums/send-type.ts index 5b03c71d22a..c2c5bdc6f6e 100644 --- a/libs/common/src/tools/send/enums/send-type.ts +++ b/libs/common/src/tools/send/enums/send-type.ts @@ -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]; diff --git a/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts b/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts index 5b4e913f693..6f49c0ecce5 100644 --- a/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts +++ b/libs/tools/send/send-ui/src/add-edit/send-add-edit-dialog.component.ts @@ -35,19 +35,16 @@ export interface SendItemDialogParams { disableForm?: boolean; } -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum SendItemDialogResult { - /** - * A Send was saved (created or updated). - */ - Saved = "saved", +/** A result of the Send add/edit dialog. */ +export const SendItemDialogResult = Object.freeze({ + /** The send item was created or updated. */ + Saved: "saved", + /** The send item was deleted. */ + Deleted: "deleted", +} as const); - /** - * A Send was deleted. - */ - Deleted = "deleted", -} +/** A result of the Send add/edit dialog. */ +export type SendItemDialogResult = (typeof SendItemDialogResult)[keyof typeof SendItemDialogResult]; /** * Component for adding or editing a send item. diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts new file mode 100644 index 00000000000..576842cd877 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.spec.ts @@ -0,0 +1,27 @@ +import { DatePreset, isDatePreset, asDatePreset } from "./send-details.component"; + +describe("SendDetails DatePreset utilities", () => { + it("accepts all defined numeric presets", () => { + const presets: Array = [ + DatePreset.OneHour, + DatePreset.OneDay, + DatePreset.TwoDays, + DatePreset.ThreeDays, + DatePreset.SevenDays, + DatePreset.FourteenDays, + DatePreset.ThirtyDays, + ]; + presets.forEach((p) => { + expect(isDatePreset(p)).toBe(true); + expect(asDatePreset(p)).toBe(p); + }); + }); + + it("rejects invalid numbers and non-numeric values", () => { + const invalid: Array = [5, -1, 0.5, 0, 9999, "never", "foo", null, undefined, {}, []]; + invalid.forEach((v) => { + expect(isDatePreset(v)).toBe(false); + expect(asDatePreset(v)).toBeUndefined(); + }); + }); +}); diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts index 2996c18bf63..ec351bee923 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -29,24 +29,50 @@ import { SendOptionsComponent } from "../options/send-options.component"; import { SendFileDetailsComponent } from "./send-file-details.component"; import { SendTextDetailsComponent } from "./send-text-details.component"; -// Value = hours -// FIXME: update to use a const object instead of a typescript enum -// eslint-disable-next-line @bitwarden/platform/no-enums -export enum DatePreset { - OneHour = 1, - OneDay = 24, - TwoDays = 48, - ThreeDays = 72, - SevenDays = 168, - FourteenDays = 336, - ThirtyDays = 720, -} +/** A preset duration (in hours) for 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, + /** Fourteen-day duration (336 hours). */ + FourteenDays: 336, + /** Thirty-day duration (720 hours). */ + ThirtyDays: 720, +} as const); + +/** A preset duration (in hours) for deletion. */ +export type DatePreset = (typeof DatePreset)[keyof typeof DatePreset]; export interface DatePresetSelectOption { name: string; value: DatePreset | string; } +const namesByDatePreset = new Map( + Object.entries(DatePreset).map(([k, v]) => [v as DatePreset, k as keyof typeof DatePreset]), +); + +/** + * Runtime type guard to verify a value is a valid DatePreset. + */ +export function isDatePreset(value: unknown): value is DatePreset { + return namesByDatePreset.has(value as DatePreset); +} + +/** + * Safe converter to DatePreset (numeric preset), returns undefined for invalid inputs. + */ +export function asDatePreset(value: unknown): DatePreset | undefined { + return isDatePreset(value) ? (value as DatePreset) : undefined; +} + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -153,11 +179,18 @@ export class SendDetailsComponent implements OnInit { const now = new Date(); const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value; + // The form allows for custom date strings, if such is used, return it without worrying about DatePreset validation if (typeof selectedValue === "string") { return selectedValue; } - const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000); + // Otherwise, treat it as a preset and validate at runtime + const preset = asDatePreset(selectedValue); + if (!isDatePreset(preset)) { + return new Date(now).toString(); + } + + const milliseconds = now.setTime(now.getTime() + preset * 60 * 60 * 1000); return new Date(milliseconds).toString(); } }