diff --git a/.changeset/25875-audit-log-date-picker.md b/.changeset/25875-audit-log-date-picker.md new file mode 100644 index 00000000000..27ff2012703 --- /dev/null +++ b/.changeset/25875-audit-log-date-picker.md @@ -0,0 +1,10 @@ +--- +'@object-ui/console': patch +--- + +fix(console): the audit log's From and To filters use the platform date picker the record forms use + +Clause-②: no +Metadata-protocol: none + +Setup → Audit Log drew the browser's native date input for its From and To filters, with the operating system's `mm/dd/yyyy` face and picker whatever the console's language. Both filters now draw the same date picker as a record form's `date` field: a text box in the display locale's format, a calendar popover, a clear button and today / tomorrow shortcuts. A typed day commits on blur or Enter. The filters send the same `created_at` bounds as before (`YYYY-MM-DD`, the To day through 23:59:59), and each filter label still names its text box. diff --git a/ui/apps/console/src/pages/system/AuditLogPage.filterLabels-13224.test.tsx b/ui/apps/console/src/pages/system/AuditLogPage.filterLabels-13224.test.tsx index 1338b0d91c4..67d81198ae0 100644 --- a/ui/apps/console/src/pages/system/AuditLogPage.filterLabels-13224.test.tsx +++ b/ui/apps/console/src/pages/system/AuditLogPage.filterLabels-13224.test.tsx @@ -26,7 +26,11 @@ * it is that filter's own control: the Action select's trigger, the Object * combobox, the Actor lookup's trigger, and the two date fields; * - the controls found by their labels are the live filters: the action, the - * From date and the To date each still send the filter they always sent. + * From date and the To date each still send the filter they always sent; + * - From and To are the platform date picker the record forms draw + * (objectstack#25875), not the browser's native date input: the label names + * the picker's text box, never its calendar button, and a day typed in the + * display locale's format commits on blur or Enter as `YYYY-MM-DD`. */ import '@testing-library/jest-dom/vitest'; @@ -134,6 +138,18 @@ function renderPage(): void { render(); } +/** + * Waits for the From and To date pickers to mount. They are the lazily loaded + * `getLazyFieldWidget('date')` chunk behind a `Suspense` skeleton, and until + * the chunk resolves no text box carries the label's id; the zero-delay ticks + * of `settle()` do not cover a chunk load on a slow runner. + */ +async function datePickersMounted(): Promise { + for (const label of ['From', 'To']) { + await waitFor(() => expect(screen.queryAllByLabelText(label, { exact: true })).toHaveLength(1), { timeout: 5000 }); + } +} + /** The one control a visible filter label names; fails unless it names exactly one. */ function controlLabelled(label: string): HTMLElement { const controls = screen.queryAllByLabelText(label, { exact: true }); @@ -145,6 +161,7 @@ describe('AuditLogPage — each filter control is named by its visible label (ob it('Action, Object, Actor, From and To each name exactly one control, their own', async () => { renderPage(); await settle(); + await datePickersMounted(); const action = controlLabelled('Action'); const object = controlLabelled('Object'); @@ -160,9 +177,14 @@ describe('AuditLogPage — each filter control is named by its visible label (ob expect(object).toHaveTextContent('Any object'); // The Actor lookup's trigger button. expect(actor).toBe(screen.getByTestId('lookup-trigger-user_id')); - // The two date fields. - expect(from).toHaveAttribute('type', 'date'); - expect(to).toHaveAttribute('type', 'date'); + // The two date fields: the platform picker's text box, not the native + // date input and not the picker's calendar button (objectstack#25875). + for (const field of [from, to]) { + expect(field.tagName).toBe('INPUT'); + expect(field).toHaveAttribute('type', 'text'); + expect(field.closest('[data-slot="date-picker"]'), 'date picker not rendered').not.toBeNull(); + } + expect(document.querySelector('input[type="date"]')).toBeNull(); expect(new Set([action, object, actor, from, to]).size).toBe(5); // The accessible name assistive technology announces is the label's. @@ -187,15 +209,22 @@ describe('AuditLogPage — each filter control is named by its visible label (ob it('the date fields found by their labels are the live From and To filters', async () => { renderPage(); await settle(); + await datePickersMounted(); + // Typed in the display locale's format (en-US here), committed on blur. + const from = controlLabelled('From'); await act(async () => { - fireEvent.change(controlLabelled('From'), { target: { value: '2026-10-01' } }); + fireEvent.change(from, { target: { value: '10/01/2026' } }); + fireEvent.blur(from); }); await settle(); await waitFor(() => expect(lastFilter()).toEqual({ created_at: { $gte: '2026-10-01' } })); + // …and on Enter. + const to = controlLabelled('To'); await act(async () => { - fireEvent.change(controlLabelled('To'), { target: { value: '2026-10-20' } }); + fireEvent.change(to, { target: { value: '10/20/2026' } }); + fireEvent.keyDown(to, { key: 'Enter' }); }); await settle(); await waitFor(() => diff --git a/ui/apps/console/src/pages/system/AuditLogPage.tsx b/ui/apps/console/src/pages/system/AuditLogPage.tsx index e4e400972a5..3a0319e38b2 100644 --- a/ui/apps/console/src/pages/system/AuditLogPage.tsx +++ b/ui/apps/console/src/pages/system/AuditLogPage.tsx @@ -34,7 +34,7 @@ * once FLS lands) */ -import { useCallback, useEffect, useId, useMemo, useState } from 'react'; +import { Suspense, useCallback, useEffect, useId, useMemo, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { mergeFilterNodes, toFilterNodeSafely } from '@object-ui/core'; import { useDisplayLocale, useObjectTranslation } from '@object-ui/i18n'; @@ -42,7 +42,6 @@ import { Button, Card, CardContent, - Input, Label, Badge, Table, @@ -71,7 +70,7 @@ import { Combobox, type ComboboxOption, } from '@object-ui/components'; -import { LookupField, objectRefOptionLabel } from '@object-ui/fields'; +import { LookupField, getLazyFieldWidget, objectRefOptionLabel } from '@object-ui/fields'; import { ConsolePageHeader, useAdapter, useMetadata } from '@object-ui/app-shell'; import { Search, X, AlertCircle, ScrollText, ExternalLink } from 'lucide-react'; import { actionBadgeVariant, actionFilterOptionsOf } from './auditLogActions'; @@ -105,6 +104,15 @@ interface AuditRow { const PAGE_SIZE = 50; +/** + * The platform date picker a record form's `date` field draws (objectui#12642): + * a text box in the display locale's format with a calendar popover, through + * the shared lazy widget registry, so it stays in its own chunk. It writes + * `YYYY-MM-DD` and `''` on clear, the values the From / To filters send. + * Resolved once at module scope, not in render (`react-hooks/static-components`). + */ +const DateFilterWidget = getLazyFieldWidget('date'); + /** The object this page browses. */ const AUDIT_OBJECT = 'sys_audit_log'; @@ -562,11 +570,27 @@ export function AuditLogPage() {
- { setFromDate(e.target.value); setPage(0); }} className="h-9" /> + {/* The record form's date picker. Its `id` reaches the text box, the + control the label names; typed text commits on blur or Enter. */} + }> + { setFromDate(v == null ? '' : String(v)); setPage(0); }} + field={{ type: 'date', name: 'created_at_from', label: t('console.system.auditLog.from', { defaultValue: 'From' }) }} + /> +
- { setToDate(e.target.value); setPage(0); }} className="h-9" /> + }> + { setToDate(v == null ? '' : String(v)); setPage(0); }} + field={{ type: 'date', name: 'created_at_to', label: t('console.system.auditLog.to', { defaultValue: 'To' }) }} + /> +
{activeFilterCount > 0 && (