# @e-llm-studio/audit-log

A flexible, composable **React** library for rendering audit / change-log surfaces with search, sorting, advanced filtering, infinite scroll, expandable row detail, and version restore.

It ships one drop-in component — [`<AuditLog />`](src/AuditLog.tsx) — that wires everything together from a few API endpoints, **plus** every sub-component is exported individually so you can compose your own UI.

> **Package:** `@e-llm-studio/audit-log`
> **Part of:** the e-LLM Studio component library

---

## Table of contents

1. [What you get](#1-what-you-get)
2. [Installation](#2-installation)
3. [Setup (styles + router)](#3-setup-styles--router)
4. [Quick start](#4-quick-start)
5. [`<AuditLog />` — full prop reference](#5-auditlog--full-prop-reference)
6. [The `LogRow` shape](#6-the-logrow-shape)
7. [Behavior details](#7-behavior-details)
8. [`<AuditLog />` examples](#8-auditlog-examples)
9. [API contracts](#9-api-contracts)
10. [Using the individual components](#10-using-the-individual-components)
11. [Exports reference](#11-exports-reference)
12. [Gotchas](#12-gotchas)

---

## 1. What you get

`<AuditLog />` renders a complete audit-log surface:

- A **toolbar** — keyword search (debounced), a date-range picker, a sort menu (Latest / Oldest / Impact % High↔Low) and a **Filter** button.
- A **right-side sidebar** — multi-select "Modified By", "Change Title", a date-range filter, and an "Impact percentage" range slider. Uses a draft model (changes apply only on **Apply Filter**).
- A **virtualized data table** — infinite scroll, per-row **Restore this version** action, optional **expandable row detail**, and an inline restore toast.
- An **internal data layer** — calls your cursor-paginated audit API, auto-populates sidebar dropdowns, and handles restore. Or you fully control data externally.

URL state for search, date range, sort, analyst filter, change-title filter, and impact range is handled automatically via `react-router-dom`'s `useSearchParams`, so links are shareable and the back button works.

Every sub-piece — table, toolbar, sidebar, date picker, multi-select, impact slider — is also exported on its own for fully custom layouts (see [§10](#10-using-the-individual-components)).

---

## 2. Installation

```bash
npm install @e-llm-studio/audit-log
```

### Peer dependencies

Your host app must provide these (they are **not** bundled):

```jsonc
{
  "react":            "^18.3.1",
  "react-dom":        "^18.3.1",
  "react-router-dom": "^7.6.2",
  "lucide-react":     "^0.476.0",
  "primeicons":       "^7.0.0",
  "primereact":       "^10.9.7"
}
```

```bash
npm install react react-dom react-router-dom lucide-react primeicons primereact
```

---

## 3. Setup (styles + router)

**1. Import the stylesheet once** (e.g. in your app entry point):

```ts
import "@e-llm-studio/audit-log/dist/styles.css";
```

**2. Render inside a router.** Filter state lives in the URL, so the component must be mounted inside a `<BrowserRouter>` (or any router exposing `useSearchParams`):

```tsx
import { BrowserRouter } from "react-router-dom";

<BrowserRouter>
  {/* ...your routes, including the page that renders <AuditLog /> */}
</BrowserRouter>
```

---

## 4. Quick start

```tsx
import { AuditLog } from "@e-llm-studio/audit-log";
import { useParams } from "react-router-dom";

export function AuditLogPage() {
  const { id: externalResourceId } = useParams();

  const API = import.meta.env.VITE_BASE_API_URL;

  return (
    <AuditLog
      configId="6a0ab685346f096a6934b358"
      getData={{ baseUrl: API, endpoint: "/claims-management-mode/audits/" }}
      distinctValuesApi={{ baseUrl: API, endpoint: "/audit-log/distinct-values" }}
      restoreApi={{ baseUrl: API, endpoint: "/audit-log/restore", method: "POST" }}
      query={{
        externalResourceId: [externalResourceId!],
        paginationLimit: 20,
      }}
      table={{ tableProps: { scrollHeight: "60vh" } }}
    />
  );
}
```

That's a fully functional audit-log page: data fetching, filtering, sorting, infinite scroll, sidebar dropdowns, and restore.

---

## 5. `<AuditLog />` — full prop reference

| Prop | Type | Required | Description |
|---|---|:---:|---|
| `configId` | `string` | ✅ | Tenant / configuration identifier sent to every audit API call. |
| `getData` | `{ baseUrl: string; endpoint: string }` | optional¹ | Audit-logs API for internal fetching (`POST`). Required unless you use external `data`. |
| `distinctValuesApi` | `{ baseUrl: string; endpoint?: string }` | optional | If provided, the sidebar's "Modified By" and "Change Title" dropdowns are auto-populated. `endpoint` defaults to `/audit-log/distinct-values`. |
| `restoreApi` | `{ baseUrl: string; endpoint: string; method?: "POST" \| "PUT" }` | optional | Enables the **Restore this version** button. Without it, restore only logs a warning. |
| `query` | `{ externalResourceId?: string[]; paginationLimit?: number }` | optional | The resource(s) to scope the log to, and the page size (`pageSize`) for pagination. |
| `filters` | `Partial<LogFiltersProps>` | optional | Pass-through overrides for the toolbar. See [§10.2](#102-logfilters--the-toolbar). |
| `sidebar` | `Partial<LogFiltersSidebarProps>` | optional | Pass-through overrides for the filter sidebar. See [§10.3](#103-logfilterssidebar--the-filter-panel). |
| `table` | `Partial<LogTableProps>` | optional | Pass-through overrides for the data table. See [§10.1](#101-logtable--the-data-table). |
| `config` | `{ showFilters?: boolean; showSidebar?: boolean }` | optional | Toggle whole sub-sections off. Both default to `true`. |
| `data` | `LogRow[]` | optional | **External data mode.** Skips the internal fetcher and renders these rows. |
| `loading` | `boolean` | optional | Initial-load flag (external mode). |
| `loadingMore` | `boolean` | optional | "Loading more" flag (external mode). |
| `hasMore` | `boolean` | optional | Whether more rows exist (external mode). |
| `onLoadMore` | `() => void` | optional | Infinite-scroll callback (external mode). |
| `refetchKey` | `string \| number` | optional | Change this value to force an internal refetch (e.g. after a mutation elsewhere). |

> ¹ If both `data` and `getData` are passed, **external data wins** — internal fetching is disabled.

---

## 6. The `LogRow` shape

```ts
type LogRow = {
  id?: string;            // table key; auto-derived from change_log_id
  historyId?: string;     // used as the key for restore
  modifiedBy?: string;
  actionType?: string;    // shown as the chip label in "Change Summary"
  actionText?: string;    // long text (truncated at 95 chars w/ "See More")
  actionDate?: string;    // ISO date string
  updatedByICE?: boolean; // when true, "Modified By" shows "ICE"
  isLive?: boolean;       // when true, shows a red "Live" pill instead of Restore
  [key: string]: any;
};
```

When the internal fetcher is used, the raw API response is normalized into this shape automatically (see [§9.1](#91-getdata--audit-log-fetch)). In external mode you must provide rows matching this shape.

---

## 7. Behavior details

### URL state

The component stores filter state in the URL via `react-router-dom`:

| URL param | Meaning |
|---|---|
| `keyword` | Search input (debounced 350 ms) |
| `createdAtStart` / `createdAtEnd` | Date range (ISO strings) |
| `orderBy` | `latest` (default), `oldest`, `impactHighToLow`, or `impactLowToHigh` |
| `userEmails` | Comma-separated list (Modified By) |
| `changeTitles` | Comma-separated list (Change Title) |
| `impactMin` / `impactMax` | Impact-percentage range (set on Apply) |
| `cursor` | Cleared on every filter change |

### Sidebar draft state

The sidebar uses a **draft** model — selections are local until the user clicks **Apply Filter**, which writes them to the URL. **Reset Filter** clears both the draft and the applied URL state.

### Infinite scroll

The internal fetcher is cursor-based. When the user scrolls within 100 px of the bottom (or when content isn't tall enough to scroll), `fetchMore()` fires. Skeletons render during the initial load and at the bottom during "load more".

### Restore flow

1. User clicks **Restore this version** on a row → button shows "Restoring…".
2. `POST`/`PUT` `{baseUrl}{endpoint}` is called with `{ change_log_id }`.
3. On success, the internal data `refetch()`s and a success toast shows (customizable via `table.restoreToast`).
4. Rows with `isLive === true` display a red **Live** pill instead of the restore button.

### Expandable row detail

Pass `table.rowDetail.render` to render your own content inline beneath a clicked row (AI summaries, diffs, request/response payloads, etc.). Accordion by default; pass `multiple: true` to allow several open at once. See [§10.1](#101-logtable--the-data-table).

---

## 8. `<AuditLog />` examples

### 8.1 Minimal — API-driven, all defaults

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
/>
```

### 8.2 With sidebar dropdowns + restore

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  distinctValuesApi={{ baseUrl: API }}                  // endpoint defaults
  restoreApi={{ baseUrl: API, endpoint: "/restore" }}
  query={{ externalResourceId: [resourceId], paginationLimit: 25 }}
/>
```

### 8.3 External data mode (you control fetching)

```tsx
const { rows, isLoading, hasMore, loadMore } = useMyAuditQuery(resourceId);

<AuditLog
  configId={CONFIG_ID}
  data={rows}
  loading={isLoading}
  hasMore={hasMore}
  onLoadMore={loadMore}
  restoreApi={{ baseUrl: API, endpoint: "/restore" }}
/>
```

### 8.4 Hide the sidebar, keep just toolbar + table

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  config={{ showSidebar: false }}
  filters={{ config: { showFilterButton: false } }}
/>
```

### 8.5 Limit the sort options + impact range

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  filters={{
    config: {
      sortOptions: { latest: true, oldest: true, impactHighToLow: false, impactLowToHigh: false },
    },
  }}
  sidebar={{
    impactRangeLimits: { min: 0, max: 50 },         // clamp the slider
    config: { showImpactPercentage: true },
  }}
/>
```

### 8.6 Expandable row detail

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  table={{
    rowDetail: {
      expandOnRowClick: true,   // default
      multiple: false,          // accordion (default)
      render: (row, { close }) => (
        <div style={{ padding: 16 }}>
          <h4>Change detail — {row.actionType}</h4>
          <pre>{row.actionText}</pre>
          <button onClick={close}>Close</button>
        </div>
      ),
    },
  }}
/>
```

### 8.7 Add a custom column + tweak chip colors

```tsx
import { ShieldCheck } from "lucide-react";

<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  table={{
    hiddenColumns: ["logId"],
    extraColumns: [
      {
        key: "severity",
        header: "Severity",
        body: (row) => <span>{row.severity ?? "—"}</span>,
        style: { width: "120px" },
      },
    ],
    chipConfig: {
      getTone: (row) => (row.modifiedBy === "ICE" ? "orange" : "purple"),
      getIcon: () => <ShieldCheck size={12} />,
    },
    tableProps: { scrollHeight: "70vh" },
    emptyState: {
      title: "No changes yet",
      description: "Edit the record to see audit entries here.",
    },
  }}
/>
```

### 8.8 Customize the restore toast

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  restoreApi={{ baseUrl: API, endpoint: "/restore" }}
  query={{ externalResourceId: [resourceId] }}
  table={{
    restoreToast: {
      enabled: true,            // set false to suppress and handle toasts yourself
      position: "top-right",
      life: 4000,
      success: { summary: "Restored", detail: (id) => `Reverted to ${id}` },
      error:   { summary: "Oops", detail: () => "Restore failed, try again." },
    },
  }}
/>
```

### 8.9 Localized labels

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  sidebar={{
    labels: {
      title: "Filtrer par",
      multiSelectLabel: "Modifié par",
      dateLabel: "Sélectionner une date",
      changeTitleLabel: "Titre du changement",
      impactPercentageTitle: "Pourcentage d'impact",
    },
  }}
/>
```

### 8.10 Analytics — tap into every interaction

```tsx
<AuditLog
  configId={CONFIG_ID}
  getData={{ baseUrl: API, endpoint: "/audits" }}
  query={{ externalResourceId: [resourceId] }}
  filters={{
    onChange: {
      onSearch: (v) => analytics.track("audit_search", { v }),
      onSort:   (v) => analytics.track("audit_sort",   { v }),
    },
  }}
  sidebar={{
    onApply: () => analytics.track("audit_filter_apply"),
    onReset: () => analytics.track("audit_filter_reset"),
  }}
/>
```

---

## 9. API contracts

### 9.1 `getData` — audit-log fetch

**Request** — `POST {baseUrl}{endpoint}`:

```jsonc
{
  "configId": "string",
  "filters": {
    "externalResourceId":    ["string"],
    "userEmails":            ["string?"],
    "changeTitle":           ["string?"],
    "content":               "string?",
    "modifiedDateStart":     "ISO date?",
    "modifiedDateEnd":       "ISO date?",
    "orderBy":               "latestFirst",  // | oldestFirst | impactPercentageHighToLow | impactPercentageLowToHigh
    "impactPercentageStart": 0,              // only sent once the impact filter is applied
    "impactPercentageEnd":   100
  },
  "pageSize": 20,
  "cursor": "opaque-cursor-from-previous-page?"
}
```

**Response** — the library normalizes `items` into `LogRow`s:

```jsonc
{
  "data": {
    "items": [
      {
        "change_log_id":        "string",  // → id + historyId
        "external_resource_id": "string",
        "modifiedbyname":       "string",  // → modifiedBy
        "modifiedbyemail":      "string",
        "changeTitle":          "string",  // → actionType
        "change":               "string",  // → actionText
        "created_at":           "ISO date" // → actionDate
      }
    ],
    "nextCursor": "string?",
    "hasNext":    true
  }
}
```

### 9.2 `distinctValuesApi`

**Request** — `GET {baseUrl}{endpoint}/{configId}?external_resource_id=...` (one query param per id). `endpoint` defaults to `/audit-log/distinct-values`.

**Response**:

```jsonc
{
  "success": true,
  "data": {
    "change_made_title":   ["string"],                  // → Change Title options
    "change_made_by_user": { "User Name": "user@x" },    // → Modified By options (label: name, value: email)
    "truncated": false
  }
}
```

### 9.3 `restoreApi`

**Request** — `POST` (or `PUT`) `{baseUrl}{endpoint}`:

```json
{ "change_log_id": "string" }
```

A non-2xx response throws `Failed to restore audit version`.

---

## 10. Using the individual components

Every building block is exported so you can compose a bespoke layout instead of using `<AuditLog />`. Import the stylesheet once ([§3](#3-setup-styles--router)) regardless of which components you use.

```ts
import {
  LogTable,
  LogFilters,
  LogFiltersSidebar,
  DateRangeSelector,
  ImpactRangeSelector,
  CustomMultiSelect,
  ActionTakenBody,
} from "@e-llm-studio/audit-log";
```

### 10.1 `LogTable` — the data table

Renders rows, infinite-scroll skeletons, the empty state, the chip + restore cells, and optional expandable detail. **You** own the data and pagination.

| Prop | Type | Description |
|---|---|---|
| `rows` | `LogRow[]` | Rows to render. |
| `loading` / `loadingMore` | `boolean` | Initial-load / load-more flags. |
| `hasMore` | `boolean` | Whether more rows exist. |
| `onLoadMore` | `() => void` | Called on scroll-to-bottom. |
| `emptyState` | `{ title?; description? }` | Empty-state copy. |
| `hiddenColumns` | `string[]` | Hide built-ins by key: `logId`, `modifiedBy`, `changeSummary`, `changeDateTime`, `actions`. |
| `extraColumns` | `{ key; header; body; style? }[]` | Append columns, or **replace** a built-in by reusing its key. |
| `chipConfig` | `{ getTone?; getIcon?; getStyle? }` | Per-row chip customization. `getTone` returns a suffix used as `audit-chip--{tone}`. |
| `tableProps` | `Record<string, any>` | Forwarded verbatim to the PrimeReact `<DataTable>` (`scrollHeight`, `className`, …). |
| `onRowClick` | `(row, helpers) => void` | Fires on row click. `helpers`: `{ isExpanded, expand, collapse, toggle }`. Ignores clicks on buttons/links/inputs. |
| `rowDetail` | `{ render; expandOnRowClick?; multiple?; className?; style? }` | Consumer-rendered inline detail under a clicked row. `render(row, { close })`. |
| `restoringId` | `string \| null` | Marks the row currently restoring. |
| `onRestore` | `(historyId) => void \| Promise` | Called when the restore button is clicked. |
| `restoreToast` | `{ enabled?; position?; life?; success?; error? }` | Inline PrimeReact toast config. |

```tsx
import { LogTable } from "@e-llm-studio/audit-log";

function MyTable({ rows, hasMore, loadMore, loading }) {
  return (
    <LogTable
      rows={rows}
      loading={loading}
      hasMore={hasMore}
      onLoadMore={loadMore}
      chipConfig={{ getTone: (r) => (r.updatedByICE ? "orange" : "purple") }}
      onRestore={async (id) => myRestoreApi(id)}
      rowDetail={{
        render: (row, { close }) => (
          <div style={{ padding: 12 }}>
            {row.actionText}
            <button onClick={close}>Close</button>
          </div>
        ),
      }}
      tableProps={{ scrollHeight: "65vh" }}
    />
  );
}
```

### 10.2 `LogFilters` — the toolbar

Controlled search / date / sort toolbar. `value` and `onChange` are required.

| Prop | Type | Description |
|---|---|---|
| `value` | `{ search?; dateRange?; sort? }` | Current toolbar state. |
| `onChange` | `{ onSearch?; onDate?; onSort? }` | Change handlers. |
| `config` | `{ showSearch?; showDate?; showSort?; showFilterButton?; sortOptions? }` | Toggle pieces. `sortOptions` toggles each of `latest`/`oldest`/`impactHighToLow`/`impactLowToHigh`. |
| `onOpenFilter` | `() => void` | Fired when the Filter button is clicked. |

```tsx
import { LogFilters, SortOrder, DateRange } from "@e-llm-studio/audit-log";
import { useState } from "react";

function Toolbar() {
  const [search, setSearch] = useState("");
  const [dateRange, setDateRange] = useState<DateRange>({ start: null, end: null });
  const [sort, setSort] = useState<SortOrder>("latest");

  return (
    <LogFilters
      value={{ search, dateRange, sort }}
      onChange={{ onSearch: setSearch, onDate: setDateRange, onSort: setSort }}
      config={{ showFilterButton: true }}
      onOpenFilter={() => console.log("open sidebar")}
    />
  );
}
```

### 10.3 `LogFiltersSidebar` — the filter panel

A right-side PrimeReact `Sidebar` with multi-selects, a date range, and an impact slider. Fully controlled.

| Prop | Type | Description |
|---|---|---|
| `visible` / `onHide` | `boolean` / `() => void` | Open state. |
| `options` / `selectedValues` / `onChange` | — | "Modified By" multi-select. |
| `changeTitleOptions` / `selectedChangeTitles` / `onChangeTitleChange` | — | "Change Title" multi-select. |
| `dateRange` / `onDateRangeChange` | — | Date-range filter. |
| `impactRange` / `impactRangeLimits` / `onImpactRangeChange` | `ImpactRange` | Impact-percentage slider (`{ min, max }`). |
| `onApply` / `onReset` | `() => void` | Footer actions. |
| `config` | `{ showAnalystFilter?; showDateFilter?; showChangeTitleFilter?; showImpactPercentage? }` | Toggle blocks. |
| `labels` | object | Localize every label/hint/title. |

```tsx
import { LogFiltersSidebar } from "@e-llm-studio/audit-log";

<LogFiltersSidebar
  visible={open}
  onHide={() => setOpen(false)}
  options={[{ label: "Renee Ramirez", value: "renee@x.com" }]}
  selectedValues={analysts}
  onChange={setAnalysts}
  dateRange={dateRange}
  onDateRangeChange={setDateRange}
  impactRange={impact}
  impactRangeLimits={{ min: 0, max: 100 }}
  onImpactRangeChange={setImpact}
  onApply={applyFilters}
  onReset={resetFilters}
/>
```

### 10.4 `DateRangeSelector` — range date picker

Self-contained day/month/year range picker with draft + Apply/Reset. Note: the committed `end` is stored as **exclusive** (one day after the visually selected end).

```tsx
import { DateRangeSelector, DateRange } from "@e-llm-studio/audit-log";
import { useState } from "react";

const [range, setRange] = useState<DateRange>({ start: null, end: null });

<DateRangeSelector
  value={range}
  onChange={setRange}
  maxDate={new Date()}
  endIcon            // render the calendar icon on the right (sidebar style)
/>
```

### 10.5 `ImpactRangeSelector` — min/max slider

A PrimeReact range slider with numeric Min/Max inputs.

```tsx
import { ImpactRangeSelector, ImpactRange } from "@e-llm-studio/audit-log";
import { useState } from "react";

const [impact, setImpact] = useState<ImpactRange>({ min: 0, max: 100 });

<ImpactRangeSelector value={impact} onChange={setImpact} min={0} max={100} />
```

### 10.6 `CustomMultiSelect` — searchable multi-select

A lightweight checkbox dropdown with search and "All".

```tsx
import { CustomMultiSelect, SelectOption } from "@e-llm-studio/audit-log";
import { useState } from "react";

const options: SelectOption[] = [
  { label: "Renee Ramirez", value: "renee@x.com" },
  { label: "ICE", value: "ice@x.com" },
];
const [selected, setSelected] = useState<string[]>([]);

<CustomMultiSelect
  options={options}
  value={selected}
  onChange={setSelected}
  placeholder="Select analysts"
/>
```

### 10.7 `ActionTakenBody` — chip + truncated text cell

The chip-plus-"See More" cell renderer used inside the table. Useful as a custom column `body`.

```tsx
import { ActionTakenBody } from "@e-llm-studio/audit-log";
import { FileText } from "lucide-react";

<ActionTakenBody
  title="Field updated"
  desciption={row.actionText}      // note: prop spelling is `desciption`
  tone="purple"
  icon={<FileText size={12} />}
/>
```

---

## 11. Exports reference

```ts
// Components
import {
  AuditLog,            // the all-in-one component
  LogTable,
  LogFilters,
  LogFiltersSidebar,
  ActionTakenBody,
  DateRangeSelector,
  ImpactRangeSelector,
  CustomMultiSelect,
} from "@e-llm-studio/audit-log";

// Types
import type {
  LogRow,
  RowClickHelpers,
  RowDetailHelpers,
  LogTableProps,
  LogFiltersProps,
  SortOrder,
  LogFiltersSidebarProps,
  LogFiltersSidebarOption,
  DateRange,
  ImpactRange,
  SelectOption,
} from "@e-llm-studio/audit-log";

// Styles (import once)
import "@e-llm-studio/audit-log/dist/styles.css";
```

---

## 12. Gotchas

- `<AuditLog />` **must** be rendered inside a router — filter state lives in the URL.
- Don't forget to **import the stylesheet** (`dist/styles.css`) or the UI will be unstyled.
- If `query.externalResourceId` is empty/undefined, the internal fetcher will not fire.
- `distinctValuesApi` is fetched lazily — only when the sidebar is visible (`config.showSidebar !== false`).
- `restoreApi` is opt-in; without it, the restore button only `console.warn`s.
- In external data mode, you are responsible for refetching after a restore — the internal `refetch()` is skipped. Use `refetchKey` (internal mode) to force a refresh from a parent.
- `paginationLimit` is sent as `pageSize`; the backend decides the page size if omitted.
- The impact filter is only sent to the API once **applied** (committed to `impactMin`/`impactMax` in the URL).
- `ActionTakenBody`'s description prop is spelled `desciption` (kept for backward compatibility).
