diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 3fba511ded0d..ab600e5538d2 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -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." }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 1c6db0197033..3b6836b76fc1 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -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." diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 89697ecf228d..689548b0146c 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -6724,6 +6724,12 @@ "maxAccessCount": { "message": "Maximum access count" }, + "incrementMaxAccessCount": { + "message": "Increment maximum access count" + }, + "decrementMaxAccessCount": { + "message": "Decrement maximum access count" + }, "disabled": { "message": "Disabled" }, diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index f1974d4210ce..c9484f1af660 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -9,10 +9,32 @@

{{ "additionalOptions" | i18n }}

{{ "maxAccessCount" | i18n }} + @if (editing()) { +
+ + +
+ } @if (editing()) { {{ "limitSendViewsHint" | i18n }} } diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.spec.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.spec.ts index 9cc9ffbf4ad9..13f0063c7f13 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.spec.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.spec.ts @@ -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; @@ -24,7 +23,6 @@ describe("SendOptionsComponent", () => { fixture.componentRef.setInput("editing", !fixture.componentInstance.editing()); fixture.detectChanges(); }; - beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SendOptionsComponent], @@ -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, @@ -72,7 +65,6 @@ describe("SendOptionsComponent", () => { expect(cardEl).toBeTruthy(); }, ); - it("should display all subfields as readonly or disabled if they are defined", async () => { mockSendFormService.originalSendView.mockReturnValue({ maxAccessCount: 5, @@ -80,7 +72,9 @@ describe("SendOptionsComponent", () => { 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]")); @@ -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, @@ -106,7 +98,9 @@ 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(); @@ -114,4 +108,45 @@ describe("SendOptionsComponent", () => { 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"); + }); + }); }); diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts index d6d678cacbc4..d3ea75c427a2 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.ts @@ -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 == "") {