From c17d87b710def35fc331ee5c8227a154ef307471 Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 5 Feb 2026 15:48:06 +0100 Subject: [PATCH] Allow updating public_until on already-public reports --- .../Controllers/Api/V1/ReportController.php | 3 + e2e/shared-reports.spec.ts | 61 +++++++++++++++++++ .../Components/Common/Report/ReportTable.vue | 4 +- .../Common/Report/ReportTableHeading.vue | 2 + .../Common/Report/ReportTableRow.vue | 22 ++++++- .../Endpoint/Api/V1/ReportEndpointTest.php | 49 +++++++++++++++ 6 files changed, 136 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/Api/V1/ReportController.php b/app/Http/Controllers/Api/V1/ReportController.php index b8f2fd21..5a8ca312 100644 --- a/app/Http/Controllers/Api/V1/ReportController.php +++ b/app/Http/Controllers/Api/V1/ReportController.php @@ -150,6 +150,9 @@ class ReportController extends Controller $report->share_secret = null; $report->public_until = null; } + } elseif ($report->is_public && $request->has('public_until')) { + // Allow updating expiration date on already-public reports + $report->public_until = $request->getPublicUntil(); } $report->save(); diff --git a/e2e/shared-reports.spec.ts b/e2e/shared-reports.spec.ts index 27605619..46f1d0e5 100644 --- a/e2e/shared-reports.spec.ts +++ b/e2e/shared-reports.spec.ts @@ -382,3 +382,64 @@ test('test that shared report with No Tag filter shows entries without tags', as await expect(page.getByText('Reporting')).toBeVisible(); await expect(page.getByText('Total')).toBeVisible(); }); + +test('test that updating expiration date on already-public report works', async ({ page }) => { + const projectName = 'UpdateExpDateProj ' + Math.floor(Math.random() * 10000); + const reportName = 'UpdateExpDateReport ' + Math.floor(Math.random() * 10000); + + await createProject(page, projectName); + await createTimeEntryWithProject(page, projectName, '1h'); + + await goToReporting(page); + await expect(page.getByTestId('reporting_view').getByText(projectName)).toBeVisible(); + + // Create a public report (already public by default) + await saveAsSharedReport(page, reportName); + + // Go to shared reports and edit + await goToReportingShared(page); + await expect(page.getByText(reportName)).toBeVisible(); + + // Click more options and edit + await page + .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) }) + .click(); + await page.getByRole('menuitem', { name: /^Edit Report/ }).click(); + + // The date picker should be visible (report is already public) + const datePicker = page + .getByRole('dialog') + .getByRole('button', { name: DATE_PICKER_BUTTON_PATTERN }); + await expect(datePicker).toBeVisible(); + await datePicker.click(); + + // Select the 25th of next month + const calendarGrid = page.getByRole('grid'); + await expect(calendarGrid).toBeVisible({ timeout: 5000 }); + await page.getByRole('button', { name: /Next/i }).click(); + await page.getByRole('gridcell').filter({ hasText: /^25$/ }).first().click(); + + // Wait for the calendar to close + await expect(calendarGrid).not.toBeVisible(); + + // Update the report and verify it includes the correct public_until date + const [response] = await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/reports/') && + response.request().method() === 'PUT' && + response.status() === 200 + ), + page.getByRole('button', { name: 'Update Report' }).click(), + ]); + const responseBody = await response.json(); + expect(responseBody.data.public_until).toBeTruthy(); + + // Verify the date is the 25th of a future month + const returnedDate = new Date(responseBody.data.public_until); + expect(returnedDate.getUTCDate()).toBe(25); + + // The returned date should be in the future + const now = new Date(); + expect(returnedDate.getTime()).toBeGreaterThan(now.getTime()); +}); diff --git a/resources/js/Components/Common/Report/ReportTable.vue b/resources/js/Components/Common/Report/ReportTable.vue index f8077471..b688fd40 100644 --- a/resources/js/Components/Common/Report/ReportTable.vue +++ b/resources/js/Components/Common/Report/ReportTable.vue @@ -14,7 +14,7 @@ defineProps<{ }>(); const gridTemplate = computed(() => { - return `grid-template-columns: minmax(150px, auto) minmax(250px, 1fr) minmax(140px, auto) minmax(130px, auto) 80px;`; + return `grid-template-columns: minmax(150px, auto) minmax(200px, 1fr) minmax(100px, 120px) minmax(80px, 100px) minmax(100px, 120px) minmax(130px, auto) 80px;`; }); @@ -23,7 +23,7 @@ const gridTemplate = computed(() => {
-
+

No shared reports found

diff --git a/resources/js/Components/Common/Report/ReportTableHeading.vue b/resources/js/Components/Common/Report/ReportTableHeading.vue index 2d5f63b6..958b84a8 100644 --- a/resources/js/Components/Common/Report/ReportTableHeading.vue +++ b/resources/js/Components/Common/Report/ReportTableHeading.vue @@ -8,7 +8,9 @@ import TableHeading from '@/Components/Common/TableHeading.vue'; Name

Description
+
Created At
Visibility
+
Expires At
Public URL
Edit diff --git a/resources/js/Components/Common/Report/ReportTableRow.vue b/resources/js/Components/Common/Report/ReportTableRow.vue index cb167ca6..1b2238eb 100644 --- a/resources/js/Components/Common/Report/ReportTableRow.vue +++ b/resources/js/Components/Common/Report/ReportTableRow.vue @@ -1,15 +1,17 @@