Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/25875-audit-log-date-picker.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -134,6 +138,18 @@ function renderPage(): void {
render(<MemoryRouter><AuditLogPage /></MemoryRouter>);
}

/**
* 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<void> {
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 });
Expand All @@ -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');
Expand All @@ -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.
Expand All @@ -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(() =>
Expand Down
34 changes: 29 additions & 5 deletions ui/apps/console/src/pages/system/AuditLogPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,14 @@
* 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';
import {
Button,
Card,
CardContent,
Input,
Label,
Badge,
Table,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -562,11 +570,27 @@ export function AuditLogPage() {
</div>
<div className="space-y-1.5">
<Label htmlFor={fromDateId} className="text-xs">{t('console.system.auditLog.from', { defaultValue: 'From' })}</Label>
<Input id={fromDateId} type="date" value={fromDate} onChange={(e) => { 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. */}
<Suspense fallback={<Skeleton className="h-9 w-full" />}>
<DateFilterWidget
id={fromDateId}
value={fromDate}
onChange={(v: unknown) => { setFromDate(v == null ? '' : String(v)); setPage(0); }}
field={{ type: 'date', name: 'created_at_from', label: t('console.system.auditLog.from', { defaultValue: 'From' }) }}
/>
</Suspense>
</div>
<div className="space-y-1.5">
<Label htmlFor={toDateId} className="text-xs">{t('console.system.auditLog.to', { defaultValue: 'To' })}</Label>
<Input id={toDateId} type="date" value={toDate} onChange={(e) => { setToDate(e.target.value); setPage(0); }} className="h-9" />
<Suspense fallback={<Skeleton className="h-9 w-full" />}>
<DateFilterWidget
id={toDateId}
value={toDate}
onChange={(v: unknown) => { setToDate(v == null ? '' : String(v)); setPage(0); }}
field={{ type: 'date', name: 'created_at_to', label: t('console.system.auditLog.to', { defaultValue: 'To' }) }}
/>
</Suspense>
</div>
{activeFilterCount > 0 && (
<div className="md:col-span-5 flex justify-end">
Expand Down
Loading