Skip to content

Commit

Permalink
[PM-11899] - send text details component (#11002)
Browse files Browse the repository at this point in the history
* Temporary local changes not meant to be merged

* WIP - send text details

* send text details

* remove extraneous code

* create base send details component

* remove file components

* fix send text details form

* remove comments

* fix send text details component

* revert type changes

* send created redirect

* Revert "send created redirect"

This reverts commit 36711d5.

* Removed hint under textArea, as per design

* Removed unused message keys

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
  • Loading branch information
jaasen-livefront and djsmith85 authored Sep 18, 2024
1 parent 1b7bb01 commit f8fc626
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 16 deletions.
14 changes: 14 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand All @@ -2279,13 +2283,19 @@
"sendTypeText": {
"message": "Text"
},
"sendTypeTextToShare": {
"message": "Text to share"
},
"sendTypeFile": {
"message": "File"
},
"allSends": {
"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."
Expand Down Expand Up @@ -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"
},
Expand Down
2 changes: 2 additions & 0 deletions libs/common/src/tools/send/services/send.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>;
selectedDeletionDatePreset: FormControl<string | number>;
}>;

// 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<bit-section [formGroup]="sendDetailsForm">
<bit-section-header>
<h2 bitTypography="h5">{{ "sendDetails" | i18n }}</h2>
</bit-section-header>

<bit-card>
<bit-form-field>
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>

<tools-send-text-details
*ngIf="config.sendType === TextSendType"
[config]="config"
[originalSendView]="originalSendView"
[sendDetailsForm]="sendDetailsForm"
></tools-send-text-details>

<bit-form-field>
<bit-label>{{ "deletionDate" | i18n }}</bit-label>
<bit-select
id="deletionDate"
name="SelectedDeletionDatePreset"
formControlName="selectedDeletionDatePreset"
>
<bit-option
*ngFor="let o of datePresetOptions"
[value]="o.value"
[label]="o.name"
></bit-option>
</bit-select>
<bit-hint>{{ "deletionDateDescV2" | i18n }}</bit-hint>
</bit-form-field>
</bit-card>
</bit-section>
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<bit-section [formGroup]="sendTextDetailsForm">
<bit-form-field>
<bit-label>{{ "sendTypeTextToShare" | i18n }}</bit-label>
<textarea bitInput id="text" rows="6" formControlName="text"></textarea>
</bit-form-field>
<bit-form-control>
<input bitCheckbox type="checkbox" formControlName="hidden" />
<bit-label>{{ "hideTextByDefault" | i18n }}</bit-label>
</bit-form-control>
</bit-section>
Original file line number Diff line number Diff line change
@@ -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<string>;
hidden: FormControl<boolean>;
}>;

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,
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<form [id]="formId" [formGroup]="sendForm" [bitSubmit]="submit">
<!-- TODO: Should we show a loading spinner here? Or emit a ready event for the container to handle loading state -->
<ng-container *ngIf="!loading"> </ng-container>
<ng-container *ngIf="!loading">
<tools-send-details
[config]="config"
[originalSendView]="originalSendView"
></tools-send-details>
</ng-container>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -55,6 +57,7 @@ import { SendForm, SendFormContainer } from "../send-form-container";
ReactiveFormsModule,
SelectModule,
NgIf,
SendDetailsComponent,
],
})
export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, SendFormContainer {
Expand Down Expand Up @@ -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<SendView>): void {
this.updatedSendView = Object.assign(this.updatedSendView, send);
patchSend(updateFn: (current: SendView) => SendView): void {
this.updatedSendView = updateFn(this.updatedSendView);
}

/**
Expand Down
Loading

0 comments on commit f8fc626

Please sign in to comment.