Edit And Review
Change a cell and watch the panel add a timestamped record with user and before/after value details.
AuditHistoryPlugin records accountable data-change transactions for spreadsheet-like business workflows. Use it when the grid edits important records and your application needs to answer who changed what, when it changed, and how to recover it.
import './audit-history.scss';
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import {
AuditHistoryPlugin,
CellFlashPlugin,
ColumnCollapsePlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { GridPresetPlugin } from '@revolist/revogrid-presets';
import { currentTheme } from '../composables/useRandomData';
import {
attachAuditHistoryIndicatorRefresh,
createAuditHistoryCellFlashConfig,
createAuditHistoryColumnTypes,
createAuditHistoryConfig,
createAuditHistoryColumns,
createAuditHistoryGridPreset,
createAuditHistoryRows,
type AuditHistoryRuntime,
} from './audit-history-shared';
const { isDark } = currentTheme();
const auditHistory = createAuditHistoryConfig();
export function load(parentSelector: string) {
const parent = document.querySelector(parentSelector);
if (!parent) return;
let auditPlugin: AuditHistoryRuntime | undefined;
const container = document.createElement('div');
container.className = 'audit-history-demo grow flex flex-row h-full gap-3 px-2 pt-2 box-border';
const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
grid.className = 'audit-history-grid cell-border';
grid.style.flexGrow = '1';
grid.range = true;
grid.hideAttribution = true;
grid.theme = isDark() ? 'darkCompact' : 'compact';
grid.stretch = 'last';
grid.gridPreset = createAuditHistoryGridPreset();
grid.multiRowHeader = true;
grid.columnTypes = createAuditHistoryColumnTypes();
grid.columns = createAuditHistoryColumns(() => auditPlugin);
grid.auditHistory = auditHistory;
grid.cellFlash = createAuditHistoryCellFlashConfig();
grid.plugins = [
GridPresetPlugin,
CellFlashPlugin,
AuditHistoryPlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
ColumnCollapsePlugin,
];
const panel = document.createElement('div');
panel.className = 'min-h-56';
container.appendChild(grid);
container.appendChild(panel);
parent.appendChild(container);
const panelHandle = defineAuditHistoryPanel(panel, grid, { placement: 'right' });
const detachIndicatorRefresh = attachAuditHistoryIndicatorRefresh(
grid,
(plugin) => {
auditPlugin = plugin;
},
);
grid.source = createAuditHistoryRows();
return () => {
detachIndicatorRefresh();
panelHandle.destroy();
grid.remove();
panel.remove();
container.remove();
};
}
import './audit-history.scss';
import { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid, type DataType } from '@revolist/react-datagrid';
import {
AuditHistoryPlugin,
CellFlashPlugin,
ColumnCollapsePlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { GridPresetPlugin } from '@revolist/revogrid-presets';
import { currentTheme } from '../composables/useRandomData';
import {
attachAuditHistoryIndicatorRefresh,
createAuditHistoryCellFlashConfig,
createAuditHistoryColumnTypes,
createAuditHistoryConfig,
createAuditHistoryColumns,
createAuditHistoryGridPreset,
createAuditHistoryRows,
type AuditHistoryRuntime,
} from './audit-history-shared';
function AuditHistory() {
const { isDark } = currentTheme();
const gridRef = useRef<HTMLRevoGridElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const auditPluginRef = useRef<AuditHistoryRuntime | undefined>(undefined);
const [source] = useState<DataType[]>(() => createAuditHistoryRows());
const columns = useMemo(
() => createAuditHistoryColumns(() => auditPluginRef.current),
[],
);
const columnTypes = useMemo(() => createAuditHistoryColumnTypes(), []);
const auditHistory = useMemo(() => createAuditHistoryConfig(), []);
const cellFlash = useMemo(() => createAuditHistoryCellFlashConfig(), []);
const gridPreset = useMemo(() => createAuditHistoryGridPreset(), []);
const plugins = useMemo(
() => [
GridPresetPlugin,
CellFlashPlugin,
AuditHistoryPlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
ColumnCollapsePlugin,
],
[],
);
useEffect(() => {
const grid = gridRef.current;
if (!grid || !panelRef.current) {
return;
}
const panelHandle = defineAuditHistoryPanel(panelRef.current, grid, { placement: 'right' });
const detachIndicatorRefresh = attachAuditHistoryIndicatorRefresh(
grid,
(plugin) => {
auditPluginRef.current = plugin;
},
);
return () => {
detachIndicatorRefresh();
panelHandle.destroy();
};
}, []);
return (
<div className="audit-history-demo grow flex h-full gap-3 px-2 pt-2 box-border">
<RevoGrid
ref={gridRef}
className="audit-history-grid cell-border"
style={{ flexGrow: 1 }}
theme={isDark() ? 'darkMaterial' : 'material'}
columns={columns}
columnTypes={columnTypes}
source={source}
plugins={plugins}
auditHistory={auditHistory}
cellFlash={cellFlash}
gridPreset={gridPreset}
multiRowHeader={true}
range
stretch="last"
hideAttribution
/>
<div ref={panelRef} className="min-h-56" />
</div>
);
}
export default AuditHistory;
<template>
<div class="audit-history-demo grow flex h-full gap-3 px-2 pt-2 box-border">
<VGrid
ref="gridRef"
class="audit-history-grid cell-border"
:style="{ flexGrow: 1 }"
:theme="isDark ? 'darkMaterial' : 'material'"
:columns="columns"
:column-types="columnTypes"
:source="rows"
:plugins="plugins"
:auditHistory.prop="auditHistory"
:cellFlash.prop="cellFlash"
:gridPreset.prop="gridPreset"
:multi-row-header.prop="true"
:range="true"
@aftergridinit="initializePanel"
stretch="last"
hide-attribution
/>
<div ref="panelRef" class="min-h-56"></div>
</div>
</template>
<script setup lang="ts">
import { nextTick, onUnmounted, ref } from 'vue';
import { VGrid } from '@revolist/vue3-datagrid';
import './audit-history.scss';
import {
AuditHistoryPlugin,
CellFlashPlugin,
ColumnCollapsePlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { GridPresetPlugin } from '@revolist/revogrid-presets';
import { currentThemeVue } from '../composables/useRandomData';
import {
attachAuditHistoryIndicatorRefresh,
createAuditHistoryCellFlashConfig,
createAuditHistoryColumnTypes,
createAuditHistoryConfig,
createAuditHistoryColumns,
createAuditHistoryGridPreset,
createAuditHistoryRows,
type AuditHistoryRuntime,
} from './audit-history-shared';
const { isDark } = currentThemeVue();
const plugins = [
GridPresetPlugin,
CellFlashPlugin,
AuditHistoryPlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
ColumnCollapsePlugin,
];
let auditPlugin: AuditHistoryRuntime | undefined;
const columns = createAuditHistoryColumns(() => auditPlugin);
const columnTypes = createAuditHistoryColumnTypes();
const rows = ref(createAuditHistoryRows());
const auditHistory = createAuditHistoryConfig();
const cellFlash = createAuditHistoryCellFlashConfig();
const gridPreset = createAuditHistoryGridPreset();
const gridRef = ref<{ $el?: HTMLRevoGridElement } | null>(null);
const panelRef = ref<HTMLElement | null>(null);
let panelInitialized = false;
let detachIndicatorRefresh: (() => void) | undefined;
let panelHandle: ReturnType<typeof defineAuditHistoryPanel> | undefined;
async function initializePanel() {
if (panelInitialized) return;
await nextTick();
const grid = gridRef.value?.$el as HTMLRevoGridElement | undefined;
if (grid && panelRef.value) {
panelHandle = defineAuditHistoryPanel(panelRef.value, grid, { placement: 'right' });
detachIndicatorRefresh = attachAuditHistoryIndicatorRefresh(
grid,
(plugin) => {
auditPlugin = plugin;
},
);
panelInitialized = true;
}
}
onUnmounted(() => {
detachIndicatorRefresh?.();
panelHandle?.destroy();
});
</script>
import {
AfterViewInit,
Component,
ElementRef,
NO_ERRORS_SCHEMA,
OnDestroy,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import {
AuditHistoryPlugin,
CellFlashPlugin,
ColumnCollapsePlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
defineAuditHistoryPanel,
} from '@revolist/revogrid-pro';
import { GridPresetPlugin } from '@revolist/revogrid-presets';
import { currentTheme } from '../composables/useRandomData';
import {
attachAuditHistoryIndicatorRefresh,
createAuditHistoryCellFlashConfig,
createAuditHistoryColumnTypes,
createAuditHistoryConfig,
createAuditHistoryColumns,
createAuditHistoryGridPreset,
createAuditHistoryRows,
type AuditHistoryRuntime,
} from './audit-history-shared';
@Component({
selector: 'audit-history-grid',
standalone: true,
imports: [RevoGrid],
template: `
<div class="audit-history-demo grow flex h-full gap-3 px-2 pt-2 box-border">
<revo-grid
#grid
class="audit-history-grid cell-border"
style="flex-grow: 1;"
[theme]="theme"
[columns]="columns"
[columnTypes]="columnTypes"
[source]="rows"
[plugins]="plugins"
[auditHistory]="auditHistory"
[cellFlash]="cellFlash"
[gridPreset]="gridPreset"
[multiRowHeader]="true"
[range]="true"
stretch="last"
[hideAttribution]="true"
></revo-grid>
<div #panel class="min-h-56"></div>
</div>
`,
styleUrls: ['./audit-history.scss'],
encapsulation: ViewEncapsulation.None,
schemas: [NO_ERRORS_SCHEMA],
})
export class AuditHistoryGridComponent implements AfterViewInit, OnDestroy {
@ViewChild('grid', { read: ElementRef })
gridElement!: ElementRef<HTMLRevoGridElement>;
@ViewChild('panel', { read: ElementRef })
panelElement!: ElementRef<HTMLElement>;
private auditPlugin: AuditHistoryRuntime | undefined;
private detachIndicatorRefresh?: () => void;
private panelHandle?: ReturnType<typeof defineAuditHistoryPanel>;
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
rows = createAuditHistoryRows();
columns = createAuditHistoryColumns(() => this.auditPlugin);
columnTypes = createAuditHistoryColumnTypes();
auditHistory = createAuditHistoryConfig();
cellFlash = createAuditHistoryCellFlashConfig();
gridPreset = createAuditHistoryGridPreset();
plugins = [
GridPresetPlugin,
CellFlashPlugin,
AuditHistoryPlugin,
FormulaPlugin,
MultiRowHeaderPlugin,
ColumnCollapsePlugin,
];
ngAfterViewInit() {
this.panelHandle = defineAuditHistoryPanel(
this.panelElement.nativeElement,
this.gridElement.nativeElement,
{ placement: 'right' },
);
this.detachIndicatorRefresh = attachAuditHistoryIndicatorRefresh(
this.gridElement.nativeElement,
(plugin) => {
this.auditPlugin = plugin;
},
);
}
ngOnDestroy() {
this.detachIndicatorRefresh?.();
this.panelHandle?.destroy();
}
}
The demo is wired like a production audit surface: edit cells, select a changed cell, filter the history, export records, and restore previous values from the panel.
Edit And Review
Change a cell and watch the panel add a timestamped record with user and before/after value details.
Inspect Markers
Hover a marked cell to see the latest change summary while keeping the full audit trail in the panel.
Scope The Trail
Click a cell or row to switch between focused cell history, row history, and full-grid history.
Recover Fast
Use restore actions to roll back a cell, row snapshot, or transaction while recording the restore itself.
When users edit operational data, the final cell value is not enough. Audit history keeps the context around each change so business users, admins, and support teams can review the full lifecycle of a value.
Accountability
Record the current user, timestamp, action type, row identity, changed column, previous value, and new value.
Bulk Awareness
Group paste and range-edit operations into one transaction instead of treating every changed cell as an unrelated record.
Recovery
Restore a single cell, a row snapshot, or a complete transaction after accidental edits.
Persistence
Keep records in memory, browser localStorage, or a custom backend adapter.
Audit history is useful when RevoGrid edits real business data, not temporary UI state.
ERP And Operations
Track changes to orders, invoices, stock levels, delivery statuses, prices, and approvals.
Financial Planning
Review manual adjustments to budgets, forecasts, risk values, and reporting tables.
Admin Panels
Log edits to permissions, limits, subscriptions, account status, and configuration records.
Support Workflows
Explain why a value changed without guessing from the current dataset.
The Audit History examples also mark edited cells directly in the grid. When a cell has recorded changes, the cell shows a small upper-right corner marker aligned to the cell edge. Hovering the cell uses TooltipPlugin to show the change count, latest before/after value, user, and timestamp.
This is built on existing plugin APIs: resolve the AuditHistoryPlugin instance, call getCellHistory(rowId, column) from cell rendering hooks, add tooltip attributes through cellProperties, and refresh visible rows when audit events fire.
const records = auditPlugin.getCellHistory(row.id, column.prop);The marker is only a visual affordance. The audit panel remains the complete review and restore surface.
Use AuditHistoryPlugin with EventManagerPlugin. The event manager normalizes cell edits, range edits, paste operations, undo, redo, and audit restore replays into edit events the audit plugin and other edit integrations can observe.
Audit History delegates range-shaped restore updates through the existing History replay path. That means cell restore, row replacement restore, and transaction restore emit beforehistedit; EventManagerPlugin turns that into the normal gridedit flow; and CellFlashPlugin can flash restored cells from the same edit pipeline. Row insertion and snapshot restore still use direct source/data updates because they are not pure range patches.
Import the plugins, panel helper, and config type.
import { AuditHistoryPlugin, CellFlashPlugin, EventManagerPlugin, createLocalStorageAuditHistoryAdapter, defineAuditHistoryPanel, type AuditHistoryConfig,} from '@revolist/revogrid-pro';Provide the audit config.
const auditHistory: AuditHistoryConfig = { getCurrentUser: () => ({ id: 'avery-stone', name: 'Avery Stone', }), rowIdProp: 'id', storage: 'memory', onAuditRecord(record) { // Persist to your API, Supabase, Firebase, PostgreSQL, etc. console.log(record); },};Register plugins and bind the config directly on the grid.
grid.plugins = [EventManagerPlugin, AuditHistoryPlugin, CellFlashPlugin];grid.auditHistory = auditHistory;HistoryPlugin does not need to be listed for normal Audit History restore behavior. Add it explicitly only when the page also has direct History UI controls or you want to control plugin order yourself.
Mount the optional review panel.
const auditPanel = defineAuditHistoryPanel(panelElement, grid, { pageSize: 25, allowExport: true, placement: 'right', restoreActions: ['cell', 'row'], formatDate: (value, { mode }) => new Intl.DateTimeFormat(undefined, mode === 'day' ? { weekday: 'short', month: 'short', day: 'numeric' } : { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(value)), events: { beforeExport: ({ format }) => format !== 'json', beforeRestore: ({ type }) => type !== 'transaction', },});
// Later:auditPanel.setPlacement('bottom');auditPanel.refresh();Audit records should point to business records, not visual row positions. Row indexes can change after sorting, filtering, grouping, or lazy loading, so every row should expose a stable identity.
const rows = [ { id: 'invoice-123', status: 'Draft', amount: 1200 }, { id: 'invoice-124', status: 'Paid', amount: 800 },];
const auditHistory: AuditHistoryConfig = { rowIdProp: 'id',};If your row identity is derived, use getRowId instead:
const auditHistory: AuditHistoryConfig = { getRowId: (row) => `${row.tenantId}:${row.invoiceNumber}`,};Each top-level audit item is an AuditRecord. A record contains one transaction ID and one or more AuditChange entries.
{ id: 'audit-001', transactionId: 'tx-001', type: 'cell-change', changedAt: '2026-05-08T12:00:00Z', changedBy: { id: 'avery-stone', }, changes: [ { id: 'change-001', rowId: 'invoice-123', rowType: 'rgRow', column: 'status', oldValue: 'Draft', newValue: 'Approved', }, ],}For a paste operation, the same record can contain many changes:
{ transactionId: 'tx-002', type: 'bulk-paste', changedAt: '2026-05-08T12:05:00Z', changedBy: { id: 'avery-stone' }, changes: [ { id: 'change-002', rowId: 'invoice-123', rowType: 'rgRow', column: 'status', oldValue: 'Draft', newValue: 'Approved', }, { id: 'change-003', rowId: 'invoice-124', rowType: 'rgRow', column: 'status', oldValue: 'Draft', newValue: 'Rejected', }, ],}Memory
Use storage: 'memory' for demos, temporary sessions, and test fixtures.
Local Storage
Use storage: 'localStorage' when browser persistence is enough for the workflow.
Custom Adapter
Use storage: 'custom' with storageAdapter when your backend should load, append, save, or clear records.
Provider Chain
Use storageProviders to list cache or server providers. Provider loads refresh the panel automatically.
Record Limit
Use maxRecords to bound retained records. The implementation defaults to 1000.
For production systems, prefer a trusted server provider. If you include several providers, the default is intentionally isolated: the first provider that returns records owns the loaded audit trail, and later providers with records are ignored with a console warning. Set mergeStorageProviders: true only when your app deliberately wants to merge records by id across providers.
const auditHistory: AuditHistoryConfig = { getCurrentUser: () => currentUser, rowIdProp: 'id', storageProviders: [ { async load() { const response = await fetch('/api/audit-history'); return response.json(); }, async append(record) { await fetch('/api/audit-history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(record), }); }, async save(records) { await fetch('/api/audit-history', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(records), }); }, async clear() { await fetch('/api/audit-history', { method: 'DELETE' }); }, }, ],};Provider write failures emit auditerror but do not block editing. Configuring storageProviders together with storage or storageAdapter shows a console warning because storageProviders takes precedence.
When you need a deliberate cache-plus-server merge, opt in explicitly:
const auditHistory: AuditHistoryConfig = { rowIdProp: 'id', mergeStorageProviders: true, storageProviders: [ createLocalStorageAuditHistoryAdapter('invoices:audit-history'), serverAuditHistoryAdapter, ],};Provider chains are useful when the user should see cached history quickly while a server-backed provider loads the durable records.
The panel mounts and reads the current in-memory records.
Storage providers load in order. By default, the first provider that returns records owns the loaded audit trail.
The plugin emits auditrecordsloaded whenever loaded records are applied.
The built-in panel refreshes on auditrecordsloaded, auditrecord, and auditrestore, so async loads and external restore controls stay visible without polling.
Production audit logs often need more than raw before/after values. Use ignoredColumns, sanitizeValue, getMetadata, canRestore, and immutable to align the client behavior with your business rules.
const auditHistory: AuditHistoryConfig = { getCurrentUser: () => currentUser, rowIdProp: 'id', ignoredColumns: ['internalNotes'], sanitizeValue(value, context) { if (context.column === 'amount') { return '[redacted]'; } return value; }, getMetadata() { return { workspaceId: currentWorkspace.id, requestId: crypto.randomUUID(), }; }, canRestore({ type }) { return currentUser.role === 'admin' && type !== 'transaction'; },};Use action-type policy when entire classes of audit records should be included or ignored:
const auditHistory: AuditHistoryConfig = { ignoredActionTypes: ['undo', 'redo'], trackedActionTypes: ['cell-change', 'bulk-paste', 'approval-submitted'], shouldTrackRecord({ type, changes, metadata }) { if (type === 'server-validation' && metadata?.severity === 'debug') { return false; } return changes.length > 0 || type === 'approval-submitted'; },};For compliance-style screens where the client must not mutate local audit records, set immutable: true. This disables clear, replaceRecords, and restore actions in the plugin. Server-side immutability should still be enforced by your backend.
const auditHistory: AuditHistoryConfig = { storage: 'custom', storageAdapter, immutable: true,};The audit panel lets users inspect grid changes without leaving the page. Common review flows include full-grid history, selected-row history, selected-cell history, user filtering, date filtering, action-type filtering, and old/new value comparison.
Scoped Views
Switch between full-grid, selected-row, and selected-cell history without changing the underlying audit records.
Live Sync
The panel refreshes after new audit records, provider-loaded batches, and restore events from other UI controls.
Policy-Aware Restore
canRestore, beforeauditrestore, and immutable let the app decide which restore actions users can run.
Portable Exports
Generate filtered CSV or JSON exports for reporting, support workflows, or backend handoff.
Use the plugin instance when you need audit data in custom UI:
const plugins = await grid.getPlugins();const auditHistoryPlugin = plugins.find( (plugin) => plugin instanceof AuditHistoryPlugin,);
const records = auditHistoryPlugin?.getRecords({ userId: 'avery-stone' });const cellHistory = auditHistoryPlugin?.getCellHistory('invoice-123', 'status');const rowHistory = auditHistoryPlugin?.getRowHistory('invoice-123');const lastStatusChange = auditHistoryPlugin?.getLastChange('invoice-123', 'status');const stats = auditHistoryPlugin?.getStats();Restore helpers return true when a restore was applied:
auditHistoryPlugin?.restoreCell(change);auditHistoryPlugin?.restoreRow('invoice-123');auditHistoryPlugin?.restoreTransaction(transactionId);Cell, row replacement, and transaction restores are replayed through HistoryPlugin.replayChange(). The restore itself is still recorded as restore-cell, restore-row, or restore-transaction, but the replay is guarded so it does not create a second normal edit record.
Use recordEvent for application-driven records that do not come from direct grid editing:
auditHistoryPlugin?.recordEvent({ type: 'api-sync', changedBy: { id: 'webhook', name: 'Webhook' }, presentation: { source: 'api', targetLabel: 'F1:F8', detailLabel: '8 cells recalculated', },});Custom action types are plain strings. When the record carries normal cell or row changes, existing restore helpers can use it for revert workflows:
auditHistoryPlugin?.recordEvent({ type: 'approval-submitted', transactionId: 'approval-42', changedBy: currentUser, changes: [{ rowId: 'invoice-123', rowIndex: 0, rowType: 'rgRow', column: 'status', oldValue: 'Draft', newValue: 'Approved', }], presentation: { source: 'api', verb: 'submitted approval', targetLabel: 'Invoice 123', },});
auditHistoryPlugin?.restoreTransaction('approval-42');Summary-only custom records are ignored unless you opt in with allowEmpty: true.
Use snapshots when users need a named restore point:
const snapshot = auditHistoryPlugin?.createSnapshot('Before Q4 close');if (snapshot) { auditHistoryPlugin?.restoreSnapshot(snapshot);}Query helpers return defensive copies, so application code cannot accidentally mutate the plugin’s internal audit log.
Use refreshRecords when your custom storage adapter can load existing records from a backend. A single storageAdapter still supports merge-by-id refreshes by default; pass { merge: false } to replace the local set. Provider chains follow the configured provider policy: isolated by default, merged only with mergeStorageProviders: true.
await auditHistoryPlugin?.refreshRecords();await auditHistoryPlugin?.replaceRecords(serverRecords, { merge: false });Use exportRecords for custom export buttons or reporting workflows:
const csv = auditHistoryPlugin?.exportRecords({ format: 'csv', filter: { actionType: 'bulk-paste' }, includeMetadata: true,});
const json = auditHistoryPlugin?.exportRecords({ format: 'json' });Export failures are reported through auditerror with phase export and return an empty string. That keeps export buttons from crashing the page while still giving your app a single error channel for observability.
The built-in panel docks to the right of the grid by default and can be pinned with placement: 'left' | 'right' | 'top' | 'bottom' | 'none'. It includes timeline day grouping, search, user/column/action/date filter chips, scoped cell and row views, pagination, CSV/JSON export controls, hover restore/jump actions, row-click compare mode, optional mini close state, and i18n-friendly labels. Use restoreActions, callback-capable allowCompare, formatDate, and events to control panel actions. Pass tooltips: false to hide all help icons, or override/hide individual entries with tooltips: { searchFilter: 'Find a record', clearFilters: false }. Pass labels to rename visible text, placeholders, aria labels, action names, source labels, and dynamic strings such as pagination or selected row/cell summaries.
User edits grid -> EventManagerPlugin normalizes the edit -> AuditHistoryPlugin creates an audit transaction -> the record is stored locally or sent to your backend -> users review changes in the audit panel -> admins restore cells, rows, or transactions when needed -> range-shaped restores replay through HistoryPlugin and EventManagerPluginAuditHistoryPlugin adds accountability to editable RevoGrid applications. Pair it with stable row IDs, a current-user provider, and a trusted persistence path when users edit important operational data.