10dcc8ae90
- Saved Filters: SavedFilterBar (dropdown, apply, delete), SaveFilterDialog (name + save) - Entity History: EntityHistoryPanel (timeline, restore, undo), HistoryDiff (field changes visual) - Activity Timeline: ActivityTimelinePage (grouped by day, pagination), ActivityFilter (user/entity/action/date) - API Docs Link: TopBar user menu entry, SettingsSystem Entwickler section (Swagger, ReDoc, OpenAPI) - Routes: /activity registered - Menu items: Aktivitäten added to automation plugin manifest
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
/**
|
|
* HistoryDiff — Visual diff table of field changes for an entity history entry.
|
|
* Shows old value (red strikethrough) → new value (green) per field.
|
|
*/
|
|
|
|
import React from 'react';
|
|
import clsx from 'clsx';
|
|
import { ArrowRight } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
export interface HistoryDiffProps {
|
|
changes: Record<string, { old: any; new: any }>;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Format a value for display, handling null/undefined gracefully.
|
|
*/
|
|
function formatValue(value: any): string {
|
|
if (value === null || value === undefined) {
|
|
return '—';
|
|
}
|
|
if (typeof value === 'boolean') {
|
|
return value ? 'true' : 'false';
|
|
}
|
|
if (typeof value === 'object') {
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
return String(value);
|
|
}
|
|
|
|
export function HistoryDiff({ changes, className }: HistoryDiffProps) {
|
|
const { t } = useTranslation();
|
|
const entries = Object.entries(changes);
|
|
|
|
if (entries.length === 0) {
|
|
return (
|
|
<p className="text-sm text-secondary-500 italic">
|
|
{t('entityHistory.noChanges', 'Keine Änderungen')}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={clsx('overflow-x-auto', className)}>
|
|
<table className="w-full text-sm border-collapse">
|
|
<thead>
|
|
<tr className="border-b border-secondary-200">
|
|
<th className="text-left font-medium text-secondary-600 py-2 pr-4">
|
|
{t('entityHistory.field', 'Feld')}
|
|
</th>
|
|
<th className="text-left font-medium text-secondary-600 py-2 pr-4">
|
|
{t('entityHistory.oldValue', 'Alter Wert')}
|
|
</th>
|
|
<th className="w-8 py-2" aria-hidden="true" />
|
|
<th className="text-left font-medium text-secondary-600 py-2">
|
|
{t('entityHistory.newValue', 'Neuer Wert')}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{entries.map(([field, change]) => {
|
|
const oldDisplay = formatValue(change.old);
|
|
const newDisplay = formatValue(change.new);
|
|
const isOldEmpty = change.old === null || change.old === undefined;
|
|
const isNewEmpty = change.new === null || change.new === undefined;
|
|
|
|
return (
|
|
<tr key={field} className="border-b border-secondary-100 last:border-0">
|
|
<td className="py-2 pr-4 font-medium text-secondary-700">
|
|
{field}
|
|
</td>
|
|
<td
|
|
className={clsx(
|
|
'py-2 pr-4',
|
|
isOldEmpty
|
|
? 'text-secondary-400 italic'
|
|
: 'text-danger-700 line-through'
|
|
)}
|
|
>
|
|
{oldDisplay}
|
|
</td>
|
|
<td className="py-2 text-center text-secondary-400">
|
|
<ArrowRight className="inline h-3.5 w-3.5" aria-hidden="true" />
|
|
</td>
|
|
<td
|
|
className={clsx(
|
|
'py-2',
|
|
isNewEmpty
|
|
? 'text-secondary-400 italic'
|
|
: 'text-success-700 font-medium'
|
|
)}
|
|
>
|
|
{newDisplay}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
);
|
|
}
|