diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ade5220..93a86bd81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to ### Added - 🔧(backend) fine tune redis cache options +- ✨(frontend) make the full last-update date available #1215 ### Changed diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx new file mode 100644 index 000000000..5c367372d --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-header/__tests__/DocHeaderInfo.spec.tsx @@ -0,0 +1,89 @@ +import { act, fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import i18next from 'i18next'; +import { DateTime } from 'luxon'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { Doc, LinkReach, Role } from '@/docs/doc-management'; +import { AppWrapper } from '@/tests/utils'; + +import { DocHeaderInfo } from '../components/DocHeaderInfo'; + +vi.mock('@/core', async () => { + const actual = await vi.importActual('@/core'); + + return { + ...actual, + useConfig: () => ({ data: {} }), + }; +}); + +const updatedAt = '2026-08-14T14:23:00'; +const doc = { + id: 'doc-1', + abilities: { + partial_update: true, + }, + ancestors_link_reach: LinkReach.RESTRICTED, + computed_link_reach: LinkReach.RESTRICTED, + deleted_at: null, + link_reach: LinkReach.RESTRICTED, + nb_accesses_ancestors: 0, + nb_accesses_direct: 1, + updated_at: updatedAt, + user_role: Role.OWNER, +} as Doc; + +describe('', () => { + beforeEach(() => { + const now = DateTime.now().set({ + year: 2026, + month: 8, + day: 14, + hour: 14, + minute: 28, + second: 0, + millisecond: 0, + }); + vi.spyOn(DateTime, 'now').mockReturnValue(now); + }); + + afterEach(async () => { + await act(async () => { + await i18next.changeLanguage('en'); + }); + vi.restoreAllMocks(); + }); + + it('uses the current locale for the relative and full dates', async () => { + const user = userEvent.setup(); + + await act(async () => { + await i18next.changeLanguage('fr'); + }); + + render(, { wrapper: AppWrapper }); + + const relativeDate = screen.getByText('il y a 5 minutes'); + fireEvent.pointerMove(relativeDate, { pointerType: 'mouse' }); + await user.hover(relativeDate); + + expect(await screen.findByRole('tooltip')).toHaveTextContent( + '14/08/2026 14:23', + ); + }); + + it('exposes the full date on hover', async () => { + const user = userEvent.setup(); + + render(, { wrapper: AppWrapper }); + + const relativeDate = screen.getByText('5 minutes ago'); + fireEvent.pointerMove(relativeDate, { pointerType: 'mouse' }); + await user.hover(relativeDate); + + expect(await screen.findByRole('tooltip')).toHaveTextContent( + '08/14/2026, 02:23 PM', + ); + }); +}); diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx index 9f38b10a5..a88821ea9 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeaderInfo.tsx @@ -1,3 +1,4 @@ +import { Tooltip } from '@gouvfr-lasuite/ui-components'; import { t } from 'i18next'; import { Box, Icon, Text } from '@/components'; @@ -22,10 +23,11 @@ interface DocHeaderInfoProps { export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { const { transRole } = useTrans(); const { isEditable } = useIsCollaborativeEditable(doc); - const { relativeDate, calculateDaysLeft } = useDate(); + const { relativeDate, formatDate, calculateDaysLeft } = useDate(); const { data: config } = useConfig(); const relativeOnly = relativeDate(doc.updated_at); + const fullDate = formatDate(doc.updated_at); const trashbinCutoff = config?.TRASHBIN_CUTOFF_DAYS; @@ -87,8 +89,21 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => { {dateLabel}   - - {dateValue} + + {trashbinCutoff && doc.deleted_at ? ( + dateValue + ) : ( + + + + )} ); diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx index 37757c4d4..9fdb94b34 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocsGridItem.tsx @@ -37,7 +37,7 @@ export const DocsGridItem = ({ const { t } = useTranslation(); const { isSmallMobile, isLargeScreen } = useResponsiveStore(); - const dateToDisplay = useDateToDisplay(doc, isInTrashbin); + const { dateToDisplay } = useDateToDisplay(doc, isInTrashbin); const { openPanel } = useLeftPanelStore(); const handleKeyDown = (e: KeyboardEvent) => { @@ -247,6 +247,7 @@ const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => { const { relativeDate, calculateDaysLeft } = useDate(); let dateToDisplay = relativeDate(doc.updated_at); + let isRelativeDate = true; if (isInTrashbin && config?.TRASHBIN_CUTOFF_DAYS && doc.deleted_at) { const daysLeft = calculateDaysLeft( @@ -255,9 +256,10 @@ const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => { ); dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`; + isRelativeDate = false; } - return dateToDisplay; + return { dateToDisplay, isRelativeDate }; }; export const DocsGridItemDate = ({ @@ -267,7 +269,8 @@ export const DocsGridItemDate = ({ doc: Doc; isInTrashbin: boolean; }) => { - const dateToDisplay = useDateToDisplay(doc, isInTrashbin); + const { dateToDisplay, isRelativeDate } = useDateToDisplay(doc, isInTrashbin); + const { formatDate } = useDate(); return ( - {dateToDisplay} + {isRelativeDate ? ( + + + + ) : ( + dateToDisplay + )} ); }; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx index 2e78d0f58..89af851c4 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/__tests__/DocsGridItemDate.test.tsx @@ -1,4 +1,11 @@ -import { act, render, screen, waitFor } from '@testing-library/react'; +import { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; import i18next from 'i18next'; import { DateTime } from 'luxon'; @@ -79,6 +86,33 @@ describe('DocsGridItemDate', () => { }); }); + it('should expose the full updated_at date on hover', async () => { + const user = userEvent.setup(); + const updatedAt = DateTime.now().minus({ minutes: 1 }); + + render( + , + { wrapper: AppWrapper }, + ); + + const relativeDate = screen.getByText('1 minute ago'); + fireEvent.pointerMove(relativeDate, { pointerType: 'mouse' }); + await user.hover(relativeDate); + + expect(await screen.findByRole('tooltip')).toHaveTextContent( + updatedAt.setLocale('en').toLocaleString({ + month: '2-digit', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }), + ); + }); + [ { deleted_at: DateTime.now().toISO(),