Record details and activity
Read typed details and connect an application-owned audit trail.
The current registry can open a record in a drawer, modal or inline consultation surface. The application supplies the audit data and implements undo.
With the default rowClickMode, a row click opens this record view in every display mode. You don't have to pass details to get it: without it, the record view derives its fields from the table columns. Pass details={productDetails} (:details="productDetails" in Vue) to control sections, labels and formats, or details={false} (:details="false" in Vue) to remove the built-in view entirely — a plain click then only calls onOpenDetails/onRowActivate when you supply one. enableRowClickEdit or rowClickMode: 'edit' open the edit form instead, and a row-link column still navigates.
Use the root TableConfig.presentation option for a shared view/create/edit/bulk surface, with an optional mobile choice. The default is a right drawer on every viewport. See shared record presentation for configuration and migration.
Configuration example
Start from the React quick start or Vue quick start. The following file extends their product catalog. Pass this configuration through getTableConfig in React or config in Vue.
import type {
DetailRevertHandler,
RecordDetailsConfig,
} from "@/components/ui/yayaw-table/utils/record-details";
import { productStore } from "../shared/store";
export const productDetails: RecordDetailsConfig = {
presentation: "drawer",
title: (row) => String(row.name),
updatedAt: (row) => String(row.updatedAt),
sections: [
{
id: "product",
title: "Product",
fields: [
{ id: "name", label: "Name", type: "text" },
{
id: "price",
label: "Price",
type: "number",
numberFormat: { style: "currency", currency: "EUR" },
},
{ id: "stock", label: "Stock", type: "number" },
{ id: "status", label: "Status", type: "text" },
],
},
],
activity: (row) => productStore.activity(String(row.id)),
};
export const revertActivity: DetailRevertHandler = (row, entry) =>
productStore.revert(String(row.id), entry.id);import type {
DetailRevertHandler,
RecordDetailsConfig,
} from "@/components/ui/yayaw-table-vue/record-details";
import { productStore } from "../shared/store";
export const productDetails: RecordDetailsConfig = {
presentation: "drawer",
title: (row) => String(row.name),
updatedAt: (row) => String(row.updatedAt),
sections: [
{
id: "product",
title: "Product",
fields: [
{ id: "name", label: "Name", type: "text" },
{
id: "price",
label: "Price",
type: "number",
numberFormat: { style: "currency", currency: "EUR" },
},
{ id: "stock", label: "Stock", type: "number" },
{ id: "status", label: "Status", type: "text" },
],
},
],
activity: (row) => productStore.activity(String(row.id)),
};
export const revertActivity: DetailRevertHandler = (row, entry) =>
productStore.revert(String(row.id), entry.id);Choose the visible fields
Pass details={productDetails} in React or :details="productDetails" in Vue. Without explicit sections, fields derive from table columns. Explicit sections let you choose readable labels, formats and which fields are omitted. The record recipe supplies the store imported above and wires onRevertActivity.
Append-only undo
Each activity item has an ID, actor, timestamp, action and optional before/after changes. Undo appends a new event with reverts pointing to the original event; the original is retained. The UI prevents invalid repeated or conflicting undo, but the backend must recheck current values and permissions atomically before writing.
Distribution requirement
Record details are in the current /r/ distribution and in the immutable v3.1.0 snapshot used by these examples; they are absent from v3.0.0. Do not combine a details example with that older snapshot. See compatibility.
Keyboard undo and deleted records
Ctrl/Cmd+Z calls the existing onRevertActivity(row, entry) handler for the newest eligible event. It uses the same reversible, reverts, conflict and details.canRevert checks as the Activity tab. The application must persist history, enforce authorization and recheck current values atomically; the library does not create a database audit log or make permanent deletion reversible.
Supply details.history: () => records where each record is { row, activity } to include events from deleted or off-page records. Without it, undo uses the loaded rows and details.activity(row). Supply only the intended undo scope, and mark events from other users or unsupported operations non-reversible. If activity is omitted, the record's Activity tab also reads this history.
Events from one bulk operation can share transactionId. One shortcut reverts its eligible members through the same handler. Successful members are refreshed and are not retried after partial failure; the remaining failed members stay available. This is sequential application-level compensation, not an atomic cross-record database transaction. Return { success: false, error } on failure and append a compensating event with reverts: originalEvent.id on success.
Inputs, text editors, open dialogs and menus keep native shortcuts. The active table owns shortcuts; a single visible table also handles them when browser focus is on the page body. Multiple tables require interaction to choose the active one. Redo and clipboard mutation shortcuts are not implied by an undo handler.
Video in a record
A detail field with type: "video" accepts a URL or { url, poster, tracks }. It renders native controls without autoplay; tracks can provide src, label, srcLang and kind: "captions" | "subtitles". Unsafe URLs are rejected in both frameworks.
Undo notifications
A successful undo displays one notification after the entire operation succeeds, including a grouped keyboard undo. Partial failures display their error and do not claim the whole action was undone. The Activity tab and Ctrl/Cmd+Z use the same localized sentence.
details.labels.undoSuccess accepts a complete sentence with an optional {action} placeholder. The defaults are Action “{action}” undone. in English and Action « {action} » annulée. in French. A media application can provide Deletion undone / Suppression annulée; translate the whole sentence instead of concatenating a suffix or inferring grammatical gender from the activity label.
Try it inline
Edit a record, open its information drawer and revert the activity entry.
Sample data. Changes stay in this preview.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { DataTable } from "@/components/ui/yayaw-table";
import { editableActions } from "./editable-actions";
import { getFormConfig } from "./get-form-config";
import type { ExamplePresentation } from "./presentation";
import { productDetails, revertActivity } from "./product-details";
import { recordConfig } from "./record-config";
import { viewActions } from "./view-actions";
const getConfig = () => recordConfig;
const recordActions = { ...editableActions, views: viewActions };
const getRecordActions = () => recordActions;
export default function RecordRecipe(presentation: ExamplePresentation = {}) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
details={productDetails}
getFormConfig={getFormConfig}
getRowId={(row) => String(row.id)}
getTableActions={getRecordActions}
getTableConfig={getConfig}
onRevertActivity={revertActivity}
tableId="recipe-record"
tableType="products"
title={presentation.title ?? "Record"}
/>
<Toaster />
</NuqsAdapter>
</QueryClientProvider>
);
}Sample data. Changes stay in this preview.
<script setup lang="ts">
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
import { YayawDataTable } from "@/components/ui/yayaw-table-vue";
import { editableActions } from "./editable-actions";
import { getFormConfig } from "./get-form-config";
import { productDetails, revertActivity } from "./product-details";
import { recordConfig } from "./record-config";
import { viewActions } from "./view-actions";
const recordActions = { ...editableActions, views: viewActions };
const getRecordActions = () => recordActions;
const getRowId = (row: Record<string, unknown>) => String(row.id);
</script>
<template>
<YayawDataTable v-bind="$props" table-type="products" table-id="recipe-record" :title="title ?? 'Record'"
:config="recordConfig" :get-table-actions="getRecordActions" :get-row-id="getRowId"
:get-form-config="getFormConfig" :details="productDetails" :on-revert-activity="revertActivity" />
</template>