diff --git a/e2e/shared-reports.spec.ts b/e2e/shared-reports.spec.ts index b6233442..dfffd1a1 100644 --- a/e2e/shared-reports.spec.ts +++ b/e2e/shared-reports.spec.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; import { PLAYWRIGHT_BASE_URL } from '../playwright/config'; import { test } from '../playwright/fixtures'; + +dayjs.extend(utc); import { createProjectViaApi, createClientViaApi, @@ -11,6 +15,7 @@ import { createBillableProjectViaApi, createTimeEntryWithBillableStatusViaApi, createTagViaApi, + createReportViaApi, } from './utils/api'; import { goToReporting, @@ -766,6 +771,97 @@ test('test that updating expiration date on already-public report works', async expect(returnedDate.getTime()).toBeGreaterThan(now.getTime()); }); +test('test that clearing the expiration date on a report works', async ({ page, ctx }) => { + const reportName = 'ClearExpReport ' + Math.floor(Math.random() * 10000); + + // Create a public report with an expiration date via API + await createReportViaApi(ctx, { + name: reportName, + is_public: true, + public_until: dayjs().add(1, 'month').utc().format('YYYY-MM-DDTHH:mm:ss[Z]'), + }); + + // Go to shared reports and edit the report + await goToReportingShared(page); + await expect(page.getByText(reportName)).toBeVisible(); + + await page + .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) }) + .click(); + await page.getByRole('menuitem', { name: /^Edit Report/ }).click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // The date picker should show a date (not "Pick a date") + await expect( + page.getByRole('dialog').getByRole('button', { name: 'Pick a date' }) + ).not.toBeVisible(); + + // Click the clear button (X icon) to remove the expiration date + const clearButton = page + .getByRole('dialog') + .locator('[role="button"]') + .filter({ has: page.locator('svg.lucide-x') }); + await expect(clearButton).toBeVisible(); + await clearButton.click(); + + // The date picker should now show "Pick a date" + await expect( + page.getByRole('dialog').getByRole('button', { name: 'Pick a date' }) + ).toBeVisible(); + + // The clear button should no longer be visible + await expect(clearButton).not.toBeVisible(); + + // Update the report and verify public_until is null + const [updateResponse] = 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 updateBody = await updateResponse.json(); + expect(updateBody.data.public_until).toBeNull(); +}); + +test('test that date picker clear button is not visible when no date is set', async ({ + page, + ctx, +}) => { + const reportName = 'NoClearReport ' + Math.floor(Math.random() * 10000); + + // Create a public report without an expiration date via API + await createReportViaApi(ctx, { + name: reportName, + is_public: true, + public_until: null, + }); + + // Go to shared reports and edit the report + await goToReportingShared(page); + await expect(page.getByText(reportName)).toBeVisible(); + + await page + .getByRole('button', { name: new RegExp('Actions for Project ' + reportName) }) + .click(); + await page.getByRole('menuitem', { name: /^Edit Report/ }).click(); + await expect(page.getByRole('dialog')).toBeVisible(); + + // The date picker should show "Pick a date" + await expect( + page.getByRole('dialog').getByRole('button', { name: 'Pick a date' }) + ).toBeVisible(); + + // The clear button should NOT be visible + const clearButton = page + .getByRole('dialog') + .locator('[role="button"]') + .filter({ has: page.locator('svg.lucide-x') }); + await expect(clearButton).not.toBeVisible(); +}); + // ────────────────────────────────────────────────── // Shared Report Cost Column Tests // ────────────────────────────────────────────────── diff --git a/e2e/utils/api.ts b/e2e/utils/api.ts index 4f21240c..35998a0a 100644 --- a/e2e/utils/api.ts +++ b/e2e/utils/api.ts @@ -724,3 +724,43 @@ export async function createRunningTimeEntryWithStartViaApi( const body = await response.json(); return body.data as { id: string; start: string; end: null; description: string }; } + +// ────────────────────────────────────────────────── +// Reports +// ────────────────────────────────────────────────── + +export async function createReportViaApi( + ctx: TestContext, + data: { + name: string; + is_public?: boolean; + public_until?: string | null; + } +) { + const response = await ctx.request.post( + `${PLAYWRIGHT_BASE_URL}/api/v1/organizations/${ctx.orgId}/reports`, + { + data: { + name: data.name, + description: '', + is_public: data.is_public ?? true, + public_until: data.public_until ?? null, + properties: { + start: '2024-01-01T00:00:00Z', + end: '2030-12-31T23:59:59Z', + group: 'project', + sub_group: 'project', + history_group: 'day', + }, + }, + } + ); + expect(response.status()).toBe(201); + const body = await response.json(); + return body.data as { + id: string; + name: string; + is_public: boolean; + public_until: string | null; + }; +} diff --git a/resources/js/Components/Common/Report/ReportCreateModal.vue b/resources/js/Components/Common/Report/ReportCreateModal.vue index 318554f9..5a3acdf0 100644 --- a/resources/js/Components/Common/Report/ReportCreateModal.vue +++ b/resources/js/Components/Common/Report/ReportCreateModal.vue @@ -111,7 +111,7 @@ async function submit() { Expires at
(optional)
- + diff --git a/resources/js/Components/Common/Report/ReportEditModal.vue b/resources/js/Components/Common/Report/ReportEditModal.vue index cffda577..55057f35 100644 --- a/resources/js/Components/Common/Report/ReportEditModal.vue +++ b/resources/js/Components/Common/Report/ReportEditModal.vue @@ -125,7 +125,7 @@ async function submit() { Expires at - + diff --git a/resources/js/packages/ui/src/Input/DatePicker.vue b/resources/js/packages/ui/src/Input/DatePicker.vue index d767aec1..921c3798 100644 --- a/resources/js/packages/ui/src/Input/DatePicker.vue +++ b/resources/js/packages/ui/src/Input/DatePicker.vue @@ -9,7 +9,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from '@/packages/ui/src/popover'; import { Calendar } from '..'; import { Button } from '@/packages/ui/src/Buttons'; -import { CalendarIcon } from 'lucide-vue-next'; +import { CalendarIcon, XIcon } from 'lucide-vue-next'; import { parseDate, type DateValue } from '@internationalized/date'; import type { Organization } from '@/packages/api/src'; @@ -17,6 +17,7 @@ const props = defineProps<{ tabindex?: string; class?: string; size?: 'sm' | 'default'; + clearable?: boolean; }>(); // This has to be a localized timestamp, not UTC @@ -60,6 +61,12 @@ function handleDateSelect(newDate: DateValue | undefined) { emit('changed', newValue); open.value = false; } + +function handleClear(event: Event) { + event.stopPropagation(); + model.value = null; + emit('changed', null); +}