diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 2a7f17e55e..a57b65f698 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2268,6 +2268,10 @@ "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "sendDetails": { + "message": "Send details", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "searchSends": { "message": "Search Sends", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2279,6 +2283,9 @@ "sendTypeText": { "message": "Text" }, + "sendTypeTextToShare": { + "message": "Text to share" + }, "sendTypeFile": { "message": "File" }, @@ -2286,6 +2293,9 @@ "message": "All Sends", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "hideTextByDefault": { + "message": "Hide text by default" + }, "maxAccessCountReached": { "message": "Max access count reached", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." @@ -2359,6 +2369,10 @@ "message": "The Send will be permanently deleted on the specified date and time.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "deletionDateDescV2": { + "message": "The Send will be permanently deleted on this date.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "expirationDate": { "message": "Expiration date" }, diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 63c07e862f..25937e7da1 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -57,6 +57,8 @@ export class SendService implements InternalSendServiceAbstraction { send.disabled = model.disabled; send.hideEmail = model.hideEmail; send.maxAccessCount = model.maxAccessCount; + send.deletionDate = model.deletionDate; + send.expirationDate = model.expirationDate; if (model.key == null) { const key = await this.keyGenerationService.createKeyWithPurpose( 128, diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts new file mode 100644 index 0000000000..282f6caffa --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/base-send-details.component.ts @@ -0,0 +1,111 @@ +import { DatePipe } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormBuilder, FormGroup, FormControl, Validators } from "@angular/forms"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; + +import { SendFormConfig } from "../../abstractions/send-form-config.service"; +import { SendFormContainer } from "../../send-form-container"; + +export type BaseSendDetailsForm = FormGroup<{ + name: FormControl; + selectedDeletionDatePreset: FormControl; +}>; + +// Value = hours +export enum DatePreset { + OneHour = 1, + OneDay = 24, + TwoDays = 48, + ThreeDays = 72, + SevenDays = 168, + FourteenDays = 336, + ThirtyDays = 720, +} + +export interface DatePresetSelectOption { + name: string; + value: DatePreset | string; +} + +@Component({ + selector: "base-send-details-behavior", + template: "", +}) +export class BaseSendDetailsComponent implements OnInit { + @Input() config: SendFormConfig; + @Input() originalSendView?: SendView; + + sendDetailsForm: BaseSendDetailsForm; + customDeletionDateOption: DatePresetSelectOption | null = null; + datePresetOptions: DatePresetSelectOption[] = []; + + constructor( + protected sendFormContainer: SendFormContainer, + protected formBuilder: FormBuilder, + protected i18nService: I18nService, + protected datePipe: DatePipe, + ) { + this.sendDetailsForm = this.formBuilder.group({ + name: new FormControl("", Validators.required), + selectedDeletionDatePreset: new FormControl(DatePreset.SevenDays || "", Validators.required), + }); + + this.sendDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + this.sendFormContainer.patchSend((send) => { + return Object.assign(send, { + name: value.name, + deletionDate: new Date(this.formattedDeletionDate), + expirationDate: new Date(this.formattedDeletionDate), + } as SendView); + }); + }); + } + + async ngOnInit() { + this.setupDeletionDatePresets(); + + if (this.originalSendView) { + this.sendDetailsForm.patchValue({ + name: this.originalSendView.name, + selectedDeletionDatePreset: this.originalSendView.deletionDate.toString(), + }); + + if (this.originalSendView.deletionDate) { + this.customDeletionDateOption = { + name: this.datePipe.transform(this.originalSendView.deletionDate, "MM/dd/yyyy, hh:mm a"), + value: this.originalSendView.deletionDate.toString(), + }; + this.datePresetOptions.unshift(this.customDeletionDateOption); + } + } + } + + setupDeletionDatePresets() { + const defaultSelections: DatePresetSelectOption[] = [ + { name: this.i18nService.t("oneHour"), value: DatePreset.OneHour }, + { name: this.i18nService.t("oneDay"), value: DatePreset.OneDay }, + { name: this.i18nService.t("days", "2"), value: DatePreset.TwoDays }, + { name: this.i18nService.t("days", "3"), value: DatePreset.ThreeDays }, + { name: this.i18nService.t("days", "7"), value: DatePreset.SevenDays }, + { name: this.i18nService.t("days", "14"), value: DatePreset.FourteenDays }, + { name: this.i18nService.t("days", "30"), value: DatePreset.ThirtyDays }, + ]; + + this.datePresetOptions = defaultSelections; + } + + get formattedDeletionDate(): string { + const now = new Date(); + const selectedValue = this.sendDetailsForm.controls.selectedDeletionDatePreset.value; + + if (typeof selectedValue === "string") { + return selectedValue; + } + + const milliseconds = now.setTime(now.getTime() + (selectedValue as number) * 60 * 60 * 1000); + return new Date(milliseconds).toString(); + } +} diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html new file mode 100644 index 0000000000..e1f3f4f0d9 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.html @@ -0,0 +1,35 @@ + + +

{{ "sendDetails" | i18n }}

+
+ + + + {{ "name" | i18n }} + + + + + + + {{ "deletionDate" | i18n }} + + + + {{ "deletionDateDescV2" | i18n }} + + +
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 new file mode 100644 index 0000000000..9fec6ff9b2 --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-details.component.ts @@ -0,0 +1,59 @@ +import { CommonModule, DatePipe } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { + SectionComponent, + SectionHeaderComponent, + TypographyModule, + CardComponent, + FormFieldModule, + IconButtonModule, + CheckboxModule, + SelectModule, +} from "@bitwarden/components"; + +import { SendFormContainer } from "../../send-form-container"; + +import { BaseSendDetailsComponent } from "./base-send-details.component"; +import { SendTextDetailsComponent } from "./send-text-details.component"; + +@Component({ + selector: "tools-send-details", + templateUrl: "./send-details.component.html", + standalone: true, + imports: [ + SectionComponent, + SectionHeaderComponent, + TypographyModule, + JslibModule, + CardComponent, + FormFieldModule, + ReactiveFormsModule, + SendTextDetailsComponent, + IconButtonModule, + CheckboxModule, + CommonModule, + SelectModule, + ], +}) +export class SendDetailsComponent extends BaseSendDetailsComponent implements OnInit { + FileSendType = SendType.File; + TextSendType = SendType.Text; + + constructor( + protected sendFormContainer: SendFormContainer, + protected formBuilder: FormBuilder, + protected i18nService: I18nService, + protected datePipe: DatePipe, + ) { + super(sendFormContainer, formBuilder, i18nService, datePipe); + } + + async ngOnInit() { + await super.ngOnInit(); + } +} diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html new file mode 100644 index 0000000000..bddd8f226d --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.html @@ -0,0 +1,10 @@ + + + {{ "sendTypeTextToShare" | i18n }} + + + + + {{ "hideTextByDefault" | i18n }} + + diff --git a/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts new file mode 100644 index 0000000000..970c74644d --- /dev/null +++ b/libs/tools/send/send-ui/src/send-form/components/send-details/send-text-details.component.ts @@ -0,0 +1,82 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + FormBuilder, + FormControl, + FormGroup, + Validators, + ReactiveFormsModule, +} from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { CheckboxModule, FormFieldModule, SectionComponent } from "@bitwarden/components"; + +import { SendFormConfig } from "../../abstractions/send-form-config.service"; +import { SendFormContainer } from "../../send-form-container"; + +import { BaseSendDetailsForm } from "./base-send-details.component"; + +type BaseSendTextDetailsForm = FormGroup<{ + text: FormControl; + hidden: FormControl; +}>; + +export type SendTextDetailsForm = BaseSendTextDetailsForm & BaseSendDetailsForm; + +@Component({ + selector: "tools-send-text-details", + templateUrl: "./send-text-details.component.html", + standalone: true, + imports: [ + CheckboxModule, + CommonModule, + JslibModule, + ReactiveFormsModule, + FormFieldModule, + SectionComponent, + ], +}) +export class SendTextDetailsComponent implements OnInit { + @Input() config: SendFormConfig; + @Input() originalSendView?: SendView; + @Input() sendDetailsForm: BaseSendDetailsForm; + + baseSendTextDetailsForm: BaseSendTextDetailsForm; + sendTextDetailsForm: SendTextDetailsForm; + + constructor( + private formBuilder: FormBuilder, + protected sendFormContainer: SendFormContainer, + ) { + this.baseSendTextDetailsForm = this.formBuilder.group({ + text: new FormControl("", Validators.required), + hidden: new FormControl(false), + }); + + this.sendTextDetailsForm = Object.assign(this.baseSendTextDetailsForm, this.sendDetailsForm); + + this.sendFormContainer.registerChildForm("sendTextDetailsForm", this.sendTextDetailsForm); + + this.sendTextDetailsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + this.sendFormContainer.patchSend((send) => { + return Object.assign(send, { + text: { + text: value.text, + hidden: value.hidden, + }, + }); + }); + }); + } + + ngOnInit() { + if (this.originalSendView) { + this.baseSendTextDetailsForm.patchValue({ + text: this.originalSendView.text?.text || "", + hidden: this.originalSendView.text?.hidden || false, + }); + } + } +} diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.html b/libs/tools/send/send-ui/src/send-form/components/send-form.component.html index 2ed7ef4d4f..ab10c53f97 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.html @@ -1,4 +1,8 @@
- - + + +
diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts index 270d610c8a..2cb1907d92 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts @@ -35,6 +35,8 @@ import { SendFormConfig } from "../abstractions/send-form-config.service"; import { SendFormService } from "../abstractions/send-form.service"; import { SendForm, SendFormContainer } from "../send-form-container"; +import { SendDetailsComponent } from "./send-details/send-details.component"; + @Component({ selector: "tools-send-form", templateUrl: "./send-form.component.html", @@ -55,6 +57,7 @@ import { SendForm, SendFormContainer } from "../send-form-container"; ReactiveFormsModule, SelectModule, NgIf, + SendDetailsComponent, ], }) export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, SendFormContainer { @@ -131,12 +134,11 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send } /** - * Patches the updated send with the provided partial senbd. Used by child components to update the send - * as their form values change. - * @param send + * Method to update the sendView with the new values. This method should be called by the child form components + * @param updateFn - A function that takes the current sendView and returns the updated sendView */ - patchSend(send: Partial): void { - this.updatedSendView = Object.assign(this.updatedSendView, send); + patchSend(updateFn: (current: SendView) => SendView): void { + this.updatedSendView = updateFn(this.updatedSendView); } /** diff --git a/libs/tools/send/send-ui/src/send-form/send-form-container.ts b/libs/tools/send/send-ui/src/send-form/send-form-container.ts index 01983360e3..f3af1ecd81 100644 --- a/libs/tools/send/send-ui/src/send-form/send-form-container.ts +++ b/libs/tools/send/send-ui/src/send-form/send-form-container.ts @@ -1,11 +1,16 @@ import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendFormConfig } from "./abstractions/send-form-config.service"; +import { SendDetailsComponent } from "./components/send-details/send-details.component"; +import { SendTextDetailsForm } from "./components/send-details/send-text-details.component"; /** * The complete form for a send. Includes all the sub-forms from their respective section components. * TODO: Add additional form sections as they are implemented. */ -export type SendForm = object; +export type SendForm = { + sendDetailsForm?: SendDetailsComponent["sendDetailsForm"]; + sendTextDetailsForm?: SendTextDetailsForm; +}; /** * A container for the {@link SendForm} that allows for registration of child form groups and patching of the send @@ -32,5 +37,5 @@ export abstract class SendFormContainer { group: Exclude, ): void; - abstract patchSend(send: Partial): void; + abstract patchSend(updateFn: (current: SendView) => SendView): void; } diff --git a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts index a94fadc708..9b6a6360ac 100644 --- a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts +++ b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts @@ -17,13 +17,8 @@ export class DefaultSendFormService implements SendFormService { return await send.decrypt(); } - async saveSend( - send: SendView, - file: File | ArrayBuffer, - config: SendFormConfig, - ): Promise { + async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) { const sendData = await this.sendService.encrypt(send, file, send.password, null); - const savedSend = await this.sendApiService.save(sendData); - return await savedSend.decrypt(); + return await this.sendApiService.save(sendData); } }