Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions apps/browser/src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6707,6 +6707,12 @@
"message": "Maximum access count",
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
},
"incrementMaxAccessCount": {
"message": "Increment maximum access count"
},
"decrementMaxAccessCount": {
"message": "Decrement maximum access count"
},
"hideMyEmail": {
"message": "Hide my email address from recipients."
},
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2725,6 +2725,12 @@
"message": "Maximum access count",
"description": "This text will be displayed after a Send has been accessed the maximum amount of times."
},
"incrementMaxAccessCount": {
"message": "Increment maximum access count"
},
"decrementMaxAccessCount": {
"message": "Decrement maximum access count"
},
"maxAccessCountDesc": {
"message": "If set, users will no longer be able to access this Send once the maximum access count is reached.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -6724,6 +6724,12 @@
"maxAccessCount": {
"message": "Maximum access count"
},
"incrementMaxAccessCount": {
"message": "Increment maximum access count"
},
"decrementMaxAccessCount": {
"message": "Decrement maximum access count"
},
"disabled": {
"message": "Disabled"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,32 @@ <h2 class="tw-mt-4" bitTypography="h6">{{ "additionalOptions" | i18n }}</h2>
<bit-label>{{ "maxAccessCount" | i18n }}</bit-label>
<input
bitInput
type="text"
inputmode="numeric"
formControlName="maxAccessCount"
[readonly]="!editing()"
id="maxAccessCountInput"
id="send-options_input_max-access-count"
/>
@if (editing()) {
<div bitSuffix ngProjectAs="[bitSuffix]" class="tw-flex tw-items-center">
<button
type="button"
bitIconButton="bwi-minus-circle"
size="small"
[label]="'decrementMaxAccessCount' | i18n"
(click)="decrementMaxAccessCount()"
data-testid="decrement-max-access-count"
></button>
<button
type="button"
bitIconButton="bwi-plus-circle"
size="small"
[label]="'incrementMaxAccessCount' | i18n"
(click)="incrementMaxAccessCount()"
data-testid="increment-max-access-count"
></button>
</div>
}
@if (editing()) {
<bit-hint>{{ "limitSendViewsHint" | i18n }}</bit-hint>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { SendPolicyService } from "../../..";
import { SendFormService } from "../../abstractions/send-form.service";

import { SendOptionsComponent } from "./send-options.component";

describe("SendOptionsComponent", () => {
let component: SendOptionsComponent;
let fixture: ComponentFixture<SendOptionsComponent>;
Expand All @@ -24,7 +23,6 @@ describe("SendOptionsComponent", () => {
fixture.componentRef.setInput("editing", !fixture.componentInstance.editing());
fixture.detectChanges();
};

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SendOptionsComponent],
Expand All @@ -39,26 +37,21 @@ describe("SendOptionsComponent", () => {
fixture = TestBed.createComponent(SendOptionsComponent);
component = fixture.componentInstance;
});

afterEach(() => {
jest.restoreAllMocks();
});

it("should create", () => {
expect(component).toBeTruthy();
});

describe("View mode", () => {
beforeEach(async () => {
fixture.componentRef.setInput("editing", false);
fixture.detectChanges();
});

it("should not display the section at all if none of its fields are visible", () => {
const cardEl = fixture.debugElement.query(By.css("bit-card"));
expect(cardEl).toBeNull();
});

it.each([
{ maxAccessCount: 5 } as SendView,
{ hideEmail: true } as SendView,
Expand All @@ -72,15 +65,16 @@ describe("SendOptionsComponent", () => {
expect(cardEl).toBeTruthy();
},
);

it("should display all subfields as readonly or disabled if they are defined", async () => {
mockSendFormService.originalSendView.mockReturnValue({
maxAccessCount: 5,
hideEmail: true,
notes: "My private note",
} as SendView);
cycleChangeDetection();
const maxAccessCountEl = fixture.debugElement.query(By.css("#maxAccessCountInput"));
const maxAccessCountEl = fixture.debugElement.query(
By.css("#send-options_input_max-access-count"),
);
expect(maxAccessCountEl).toBeTruthy();
expect(maxAccessCountEl.attributes.readonly).toEqual("");
const hideEmailEl = fixture.debugElement.query(By.css("input[type=checkbox]"));
Expand All @@ -91,13 +85,11 @@ describe("SendOptionsComponent", () => {
expect(privateNoteEl.attributes.readonly).toEqual("");
});
});

describe("Edit mode", () => {
beforeEach(async () => {
fixture.componentRef.setInput("editing", true);
await fixture.whenStable();
});

it("should display all fields whether or not they are defined", async () => {
await mockSendFormService.initializeSendForm({
areSendsAllowed: true,
Expand All @@ -106,12 +98,55 @@ describe("SendOptionsComponent", () => {
sendType: SendType.Text,
});
fixture.detectChanges();
const maxAccessCountEl = fixture.debugElement.query(By.css("#maxAccessCountInput"));
const maxAccessCountEl = fixture.debugElement.query(
By.css("#send-options_input_max-access-count"),
);
expect(maxAccessCountEl).toBeTruthy();
const hideEmailEl = fixture.debugElement.query(By.css("input[type=checkbox]"));
expect(hideEmailEl).toBeTruthy();
const privateNoteEl = fixture.debugElement.query(By.css("textarea"));
expect(privateNoteEl).toBeTruthy();
});
});
describe("Max access count increment/decrement", () => {
beforeEach(async () => {
mockSendFormService.originalSendView.mockReturnValue({ maxAccessCount: 3 } as SendView);
fixture.componentRef.setInput("editing", true);
fixture.detectChanges();
await fixture.whenStable();
});
it("should show +/- buttons in edit mode", () => {
const incrementBtn = fixture.debugElement.query(
By.css("[data-testid='increment-max-access-count']"),
);
const decrementBtn = fixture.debugElement.query(
By.css("[data-testid='decrement-max-access-count']"),
);
expect(incrementBtn).toBeTruthy();
expect(decrementBtn).toBeTruthy();
});
it("should not show +/- buttons in view mode", () => {
fixture.componentRef.setInput("editing", false);
fixture.detectChanges();
const incrementBtn = fixture.debugElement.query(
By.css("[data-testid='increment-max-access-count']"),
);
expect(incrementBtn).toBeNull();
});
it("should increment maxAccessCount when + button clicked", () => {
component.sendOptionsForm.patchValue({ maxAccessCount: "3" });
component.incrementMaxAccessCount();
expect(component.sendOptionsForm.get("maxAccessCount")?.value).toBe("4");
});
it("should decrement maxAccessCount when - button clicked", () => {
component.sendOptionsForm.patchValue({ maxAccessCount: "3" });
component.decrementMaxAccessCount();
expect(component.sendOptionsForm.get("maxAccessCount")?.value).toBe("2");
});
it("should not decrement below 1", () => {
component.sendOptionsForm.patchValue({ maxAccessCount: "1" });
component.decrementMaxAccessCount();
expect(component.sendOptionsForm.get("maxAccessCount")?.value).toBe("1");
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ export class SendOptionsComponent {
});
}

incrementMaxAccessCount(): void {
const current = Number(this.sendOptionsForm.get("maxAccessCount")?.value ?? 0);
this.sendOptionsForm.patchValue({ maxAccessCount: String(current + 1) });
}

decrementMaxAccessCount(): void {
const current = Number(this.sendOptionsForm.get("maxAccessCount")?.value ?? 1);
if (current > 1) {
this.sendOptionsForm.patchValue({ maxAccessCount: String(current - 1) });
}
}

isIntegerValidator(): ValidatorFn {
return (control: FormControl): ValidationErrors | null => {
if (control.value == null || control.value == "") {
Expand Down
Loading