Phase 3: Saved Filters UI, Entity History UI, Activity Timeline, API Docs Link
- 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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user