PHPackages                             hamoda-dev/filament-activity-history - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. hamoda-dev/filament-activity-history

ActiveLibrary

hamoda-dev/filament-activity-history
====================================

Beautiful, relation-aware activity history timelines for Filament panels.

v1.0.0(today)00MITPHPPHP ^8.3

Since Jul 27Pushed todayCompare

[ Source](https://github.com/hamoda-dev/filament-activity-history)[ Packagist](https://packagist.org/packages/hamoda-dev/filament-activity-history)[ Docs](https://github.com/hamoda-dev/filament-activity-history)[ RSS](/packages/hamoda-dev-filament-activity-history/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (4)Versions (3)Used By (0)

Filament Activity History
=========================

[](#filament-activity-history)

Relation-aware activity history timelines for Filament v5, on top of [spatie/laravel-activitylog](https://github.com/spatie/laravel-activitylog).

A record's history renders as a tree: the record's own changes, plus the changes of the relations it declares, with activities logged in the same batch nested under the change that caused them. Every line reads as a sentence — *"Sara updated **Price** to SAR 199.00 and **Stock** to 5 on variant Gray / S"* — in the reader's language.

The package ships no permissions, no tenancy rules and no domain formatting. Everything application-specific is supplied through closures, config, or your own translation files, so the same component drops into any project unchanged.

---

Contents
--------

[](#contents)

- [Requirements](#requirements)
- [Installation](#installation)
- [Logging a model](#logging-a-model)
    - [Relations](#relations)
    - [Naming a record](#naming-a-record)
- [Rendering a timeline](#rendering-a-timeline)
    - [Slide-over action](#slide-over-action)
    - [Full page](#full-page)
    - [Activity feed](#activity-feed)
    - [Inline in a schema](#inline-in-a-schema)
- [Localization](#localization)
    - [Scaffolding your translations](#scaffolding-your-translations)
    - [How a label is resolved](#how-a-label-is-resolved)
    - [Borrowing Filament's labels](#borrowing-filaments-labels)
    - [Values](#values)
- [Readability](#readability)
- [Row actions](#row-actions)
- [Authorization and scoping](#authorization-and-scoping)
- [Panel-wide defaults](#panel-wide-defaults)
- [Appearance](#appearance)
- [Configuration reference](#configuration-reference)
- [API reference](#api-reference)
- [Artisan commands](#artisan-commands)
- [Gotchas](#gotchas)

---

Requirements
------------

[](#requirements)

PHP8.3+Filamentv5spatie/laravel-activitylog^4.10 (installed with the package)---

Installation
------------

[](#installation)

```
composer require hamoda-dev/filament-activity-history
php artisan activity-history:install
```

The install command publishes the activity log migrations and this package's config file, then offers to run `migrate`.

Those migrations belong to `spatie/laravel-activitylog`, which owns the `activity_log` table's schema and config — the package publishes them rather than defining its own, so the table keeps evolving with spatie rather than forking away from it.

**No asset step.** The stylesheet is inlined into the panel head, so there is no `filament:assets` run, no `public/` artifact, and no stale cached file after an upgrade. It uses no Tailwind sources and needs no theme rebuild. Set `assets.inline` to `false` to serve it as a published file instead, in which case `php artisan filament:assets` applies as usual.

Optional publishes: `activity-history-translations`, `activity-history-views`.

Register the plugin on each panel that should show histories:

```
use HamodaDev\ActivityHistory\ActivityHistoryPlugin;

public function panel(Panel $panel): Panel
{
    return $panel->plugin(ActivityHistoryPlugin::make());
}
```

---

Logging a model
---------------

[](#logging-a-model)

Logging is spatie's job, unchanged:

```
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

class Order extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logFillable()
            ->logOnlyDirty()
            ->dontSubmitEmptyLogs();
    }
}
```

If your panels authenticate against their own guards, tell spatie which user to credit, or every change will be logged with no actor:

```
use Illuminate\Support\Facades\Auth;
use Spatie\Activitylog\Facades\CauserResolver;

CauserResolver::resolveUsing(function () {
    foreach (['store', 'admin', 'web'] as $guard) {
        if (Auth::guard($guard)->check()) {
            return Auth::guard($guard)->user();
        }
    }

    return null;
});
```

### Relations

[](#relations)

A record's history usually includes what happened to the things it owns. Declare those relations and they are merged into one timeline:

```
use HamodaDev\ActivityHistory\Contracts\HasActivityRelations;

class Order extends Model implements HasActivityRelations
{
    /** @return array */
    public function activityRelations(): array
    {
        return ['items', 'payments', 'refunds'];
    }
}
```

Any Eloquent relation works — `hasMany`, `hasOne`, `belongsTo`, `morphMany`. Soft-deleted related records are included: a child that was deleted still belongs to its parent's history.

Prefer a property? Use the trait:

```
use HamodaDev\ActivityHistory\Concerns\HasActivityHistory;

class Order extends Model implements HasActivityRelations
{
    use HasActivityHistory;

    protected array $activityRelations = ['items', 'payments'];
}
```

Relations can also be overridden per timeline with `->withRelations([...])`.

### Naming a record

[](#naming-a-record)

By default a record is named by the first of `subject_title_attributes` it has. When that would surface something technical — a slug, a SKU, a uuid — let the model answer for itself:

```
use HamodaDev\ActivityHistory\Contracts\HasActivityTitle;

class ProductVariant extends Model implements HasActivityTitle
{
    public function activityTitle(): ?string
    {
        return $this->optionDescriptor() ?: null;   // "Gray / S"
    }
}
```

Returning `null` leaves the record unnamed ("on the product variant") rather than naming it badly. Soft-deleted records are still asked — the timeline loads them explicitly, because a `morphTo` cannot see them.

---

Rendering a timeline
--------------------

[](#rendering-a-timeline)

The same `Timeline` component backs every surface below, so it is configured the same way wherever it appears.

### Slide-over action

[](#slide-over-action)

```
use HamodaDev\ActivityHistory\Actions\TimelineAction;
use HamodaDev\ActivityHistory\Timeline\Timeline;

TimelineAction::make()
    ->timeline(fn (Timeline $timeline) => $timeline->searchable())
```

Drop it in a page header, a table row, or an infolist section. It opens a slide-over by default; `action.slide_over => false` makes it a centred modal.

To send it to a full page instead of opening a modal:

```
TimelineAction::make()
    ->toPage(fn (Order $record): string => OrderResource::getUrl('history', ['record' => $record]))
```

### Full page

[](#full-page)

Generate it:

```
php artisan make:activity-history-page OrderHistory --resource=OrderResource
```

```
use HamodaDev\ActivityHistory\Pages\RecordActivityPage;

class OrderHistory extends RecordActivityPage
{
    protected static string $resource = OrderResource::class;

    protected function timeline(): Timeline
    {
        return parent::timeline()->attributeCast(Money::class, $formatter);
    }
}
```

```
// OrderResource::getPages()
'history' => OrderHistory::route('/{record}/history'),
```

The page sits the timeline in a card. Turn that off with `page.card => false`, or override `timelineContent()` for a different wrapper.

### Activity feed

[](#activity-feed)

A timeline with no record bound to it lists **every** activity, each row naming the record it happened on:

```
php artisan make:activity-history-page Activity --feed --panel=admin
```

```
use HamodaDev\ActivityHistory\Pages\ActivityFeedPage;

class Activity extends ActivityFeedPage
{
    protected function timeline(): Timeline
    {
        return parent::timeline()->modifyQueryUsing(
            fn (Builder $query) => $query->whereBelongsTo(Filament::getTenant()),
        );
    }
}
```

Scope it yourself — the package applies none.

### Inline in a schema

[](#inline-in-a-schema)

```
Timeline::make('activity_history')
    ->searchable()
    ->maxHeight('28rem')
```

Anywhere a schema component fits: an infolist section, a form, a custom page.

---

Localization
------------

[](#localization)

Every string the package itself produces ships in English and Arabic. Your tables and columns are yours, and resolve from your own language file — nothing about your domain ends up in a vendor directory.

### Scaffolding your translations

[](#scaffolding-your-translations)

```
php artisan activity-history:translations --locale=ar --locale=en
```

The command walks every model using `LogsActivity`, reads the attributes it actually logs (`LogOptions`, else `$fillable`), and writes `lang/{locale}/activity.php` with headline defaults. Re-run it after adding a model or a column: existing translations are never overwritten, only gaps filled.

```
// lang/ar/activity.php
return [
    'subjects' => [
        'product_variant' => 'خيار المنتج',
    ],
    'attributes' => [
        'product' => ['price' => 'السعر'],   // this model only
        'stock' => 'الكمية',                  // every model
    ],
    'values' => [
        'vat_class' => ['standard' => 'الأساسية'],
    ],
];
```

In CI, `--missing` lists untranslated keys and exits non-zero.

### How a label is resolved

[](#how-a-label-is-resolved)

First hit wins:

1. `->attributeLabel('price', 'Selling price')` on the timeline
2. `activity.attributes.{model}.{field}` — per model, for fields whose meaning differs between models
3. `activity.attributes.{field}` — shared across models
4. `validation.attributes.{field}` — most applications already fill this in for form errors, so those fields are localized for free
5. the label of the matching field in the model's Filament resource form (opt in, below)
6. a headline of the column name

Model names follow the same idea: `activity.subjects.{model}`, then the resource's `getModelLabel()`, then the class name.

### Borrowing Filament's labels

[](#borrowing-filaments-labels)

```
'translations' => ['use_filament_labels' => true],
```

The timeline then reads labels straight off the model's Filament resource form, so fields already labelled need no translation keys at all. It is **off by default on purpose**: renaming a form label would silently reword your history, and a resource that cannot be introspected is a silent miss rather than an error. Explicit language files are the predictable choice; this is the zero-config one.

### Values

[](#values)

Values are localized too, not just labels:

- enums through Filament's `HasLabel`, else their backing value
- dates through ISO format tokens, so month and day names follow the locale
- numbers through `Number::format()` in the active locale
- foreign keys as the title of the record they point at — a translatable related model resolves in the current locale
- plain string columns through `activity.values.{field}.{value}`
- booleans through the package's own `yes`/`no` strings

Sentence structure is translated, not concatenated: conjunctions and punctuation are placed by the translation file, so Arabic's *و* joins the word after it while English's *and* stands alone.

---

Readability
-----------

[](#readability)

A history is only useful if a person can read it, so the defaults lean towards plain language:

- **Technical columns are hidden.** `hidden_attributes` drops slugs, uuids and timestamps, and an update that touched nothing else is dropped with it rather than rendered as a bare "updated". `keep_empty_updates` brings them back.
- **Values are tidied.** Markup is stripped, whitespace collapsed, and long values trimmed to `max_value_length` with the full text in the row tooltip.
- **Sensitive values are masked.** `masked_attributes` lists the change but replaces both values with `mask`.
- **Milestones stand out.** `created`, `deleted` and `restored` get a circled, coloured icon; ordinary updates get a small node, so the eye follows the shape of the timeline. Configure with `milestone_events`, `icons` and `colors`.

---

Row actions
-----------

[](#row-actions)

Pass Filament actions to `itemActions()` and they render under each row, the way a table renders row actions:

```
use Filament\Actions\Action;

Timeline::make('activity_history')
    ->itemActions([
        Action::make('viewSubject')
            ->label(fn (array $arguments): string => $arguments['subjectType'] === Order::class
                ? 'View order'
                : 'View item')
            ->icon(Heroicon::OutlinedEye)
            ->visible(fn (array $arguments): bool => $arguments['subjectType'] === Order::class)
            ->url(fn (array $arguments): string => OrderResource::getUrl('view', ['record' => $arguments['subjectId']])),

        Action::make('revert')
            ->requiresConfirmation()
            ->action(fn (array $arguments) => revertActivity($arguments['activity'])),
    ])
```

Each action is mounted per row with these `$arguments`:

KeyValue`activity`the activity's primary key`subjectType`the logged morph type`subjectId`the logged subject key`event``created`, `updated`, `deleted`, `restored`, …So one definition serves the whole timeline and decides for itself where it applies. Actions render as links to suit the row density; call `->button()` on one to override. Modals, confirmations and URLs behave as they do anywhere else in Filament.

---

Authorization and scoping
-------------------------

[](#authorization-and-scoping)

The package defines no permissions and no policy. Two hooks cover both:

```
Timeline::make('activity_history')
    ->authorize(fn (): bool => auth()->user()->can('viewActivityHistory'))
    ->modifyQueryUsing(fn (Builder $query) => $query->where('team_id', auth()->user()->team_id))
```

`authorize()` gates the whole component; `modifyQueryUsing()` receives the activity query, which is where multi-tenancy, date windows and log-name filters belong.

A record timeline is already constrained to that record's own subject ids, so a user who cannot reach the record cannot reach its history. A **feed** page has no such constraint — scope it explicitly.

---

Panel-wide defaults
-------------------

[](#panel-wide-defaults)

Configure every timeline in a panel at once:

```
use HamodaDev\ActivityHistory\ActivityHistoryPlugin;
use HamodaDev\ActivityHistory\Timeline\Timeline;

$panel->plugin(
    ActivityHistoryPlugin::make()->timeline(function (Timeline $timeline): void {
        $timeline
            ->authorize(fn (): bool => Filament::auth()->check())
            ->causerUrl(fn (Model $causer): string => UserResource::getUrl('view', ['record' => $causer]))
            ->attributeCast(Money::class, fn (int $cents): string => Currency::format($cents));
    }),
);
```

Per-instance calls always win over panel defaults.

---

Appearance
----------

[](#appearance)

The stylesheet is plain CSS driven by custom properties, with logical properties throughout so RTL works without a separate build. Override the palette anywhere in your own CSS:

```
.fi-ah {
    --ah-accent: #2f7ab5;
    --ah-line: #e5e7eb;
    --ah-strong: #09090b;
}

.dark .fi-ah {
    --ah-accent: #60a5fa;
}
```

Structural classes: `.fi-ah` (root), `.fi-ah-timeline`, `.fi-ah-item`, `.fi-ah-marker`, `.fi-ah-node`, `.fi-ah-icon`, `.fi-ah-summary`, `.fi-ah-strong`, `.fi-ah-time`, `.fi-ah-actions`, `.fi-ah-children`, `.fi-ah-empty`.

For deeper changes, publish the views with `--tag=activity-history-views`.

---

Configuration reference
-----------------------

[](#configuration-reference)

KeyDefaultWhat it does`activity_model``null`Model activities are read from; falls back to spatie's`limit``50`Activities loaded per timeline`milestone_events``created, deleted, restored`Which events get a circled icon`icons` / `colors`heroicons / semanticPer event, with a `default` entry`translations.namespace``activity`Your language file for subjects and attributes`translations.use_validation_attributes``true`Fall back to `validation.attributes``translations.use_filament_labels``false`Read labels off Filament resource forms`values.resolve_relations``true`Render foreign keys as the related record's title`values.format_numbers``true`Locale-aware number formatting`causer_name_attributes``name, full_name, title, email`Tried in order when naming the actor`subject_title_attributes``name, title, label, reference, sku, code`Tried in order when naming a record`hidden_attributes``slug, uuid, timestamps, remember_token`Columns never shown`keep_empty_updates``false`Keep updates whose every change was hidden`max_value_length``80`Trim long values; markup is always stripped`masked_attributes` / `mask`passwords, tokens / `••••••`Values never rendered`null_placeholder``—`Stands in for an emptied value`date_format``LLL`ISO format for date attributes in a summary`relative_timestamps``true`"5m ago" versus an absolute time`short_relative_timestamps``true`Short units; turn off for languages that read badly abbreviated`timestamp_format` / `tooltip_format``LLL` / `LLLL`ISO formats for the row time and its tooltip`assets.inline``true`Inline the stylesheet instead of publishing it`empty_state_icon` / `search_icon`clock / magnifierIcons`page.card` / `page.compact_card``true` / `false`Whether page timelines sit in a card`action.icon/color/width/slide_over`clock, gray, large, `true``TimelineAction` defaults---

API reference
-------------

[](#api-reference)

### `Timeline`

[](#timeline)

**Data**

MethodPurpose`withRelations(array|Closure)`Override the relations declared on the model`modifyQueryUsing(Closure)`Mutate the activity query`limit(int|null)`Number of activities loaded`sortActivitiesDescending(bool)`Newest first (default) or oldest first`inlineBatches(bool)`Flatten batches instead of nesting them**Wording**

MethodPurpose`attributeLabel(string, string|Closure)`Label for one attribute`attributeValue(string, Closure)`Formatter for one attribute`attributeCast(string, Closure)`Formatter for every attribute using that cast`subjectLabel(Closure)`How a record is named`eventDescription(Closure)`Replace the whole summary line`causerName(Closure)` / `causerUrl(Closure)`How the actor is shown and linked`showCauser(bool)`Hide the actor's name`changesSummaryAttributeValues(bool)`List changed fields without their values`changesSummaryOldAttributeValues(bool)`Include the previous value ("from X to Y")**Appearance**

MethodPurpose`itemIcon(...)` / `itemIconColor(...)`Per-event icon and colour`compact(bool)`Tighter rows`searchable(bool)`Client-side filter box`maxHeight(string)`Scroll inside a fixed height`emptyStateHeading/Description/Icon(...)`Empty state**Behaviour**

MethodPurpose`authorize(bool|Closure)`Gate the whole component`itemActions(array)`Actions rendered under each row`Timeline::configureUsing(Closure)` applies defaults globally, as with any Filament component.

### Contracts

[](#contracts)

ContractMethodPurpose`HasActivityRelations``activityRelations(): array`Relations merged into the record's timeline`HasActivityTitle``activityTitle(): ?string`How the record names itself### Pages and actions

[](#pages-and-actions)

ClassPurpose`Actions\TimelineAction`Slide-over, modal, or link to a page`Pages\RecordActivityPage`Full page for one record's history`Pages\ActivityFeedPage`Panel-wide feed of every activity`ActivityHistoryPlugin`Panel registration and defaults---

Artisan commands
----------------

[](#artisan-commands)

CommandPurpose`activity-history:install`Publish migrations and config, then migrate`activity-history:translations`Scaffold `lang/{locale}/activity.php`; `--missing` for CI`make:activity-history-page`Generate a record page (`--resource=`) or feed page (`--feed`)---

Gotchas
-------

[](#gotchas)

- **A translatable attribute logs the locale that was active when it changed.**If `name` was updated while the app was in English, that entry holds the English string and cannot be shown in Arabic afterwards — the value was never recorded. Values logged as a full locale array *are* resolved per locale.
- **Scaffolding translations changes existing output.** Before you run `activity-history:translations`, labels may be coming from `validation.attributes`; afterwards the generated file wins. Review the generated file rather than committing it unread.
- **Feed pages are unscoped by design.** `modifyQueryUsing()` is not optional there if your application is multi-tenant.
- **`assets.inline => false`** re-introduces the publish step, and the asset is cache-busted by package version — while developing the package itself you will need a hard reload after editing the stylesheet.

---

License
-------

[](#license)

MIT.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity49

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

0d ago

Major Versions

v0.0.1 → v1.0.02026-07-27

### Community

Maintainers

![](https://www.gravatar.com/avatar/220b782cf42d7a19b879b90823af25b757fe7a1edcfb41e7295fb0bb75d36018?d=identicon)[hamoda-dev](/maintainers/hamoda-dev)

---

Top Contributors

[![hamoda-dev](https://avatars.githubusercontent.com/u/45687193?v=4)](https://github.com/hamoda-dev "hamoda-dev (1 commits)")

---

Tags

laravelactivityAudithistorytimelinefilamentactivitylog

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/hamoda-dev-filament-activity-history/health.svg)

```
[![Health](https://phpackages.com/badges/hamoda-dev-filament-activity-history/health.svg)](https://phpackages.com/packages/hamoda-dev-filament-activity-history)
```

###  Alternatives

[muhammadsadeeq/laravel-activitylog-ui

A beautiful, modern UI for Spatie's Activity Log with advanced filtering, analytics, and real-time features.

17717.0k](/packages/muhammadsadeeq-laravel-activitylog-ui)[alizharb/filament-activity-log

A powerful, feature-rich activity logging solution for FilamentPHP v4 &amp; v5 with timeline views, dashboard widgets, and revert actions.

2871.8k3](/packages/alizharb-filament-activity-log)[mradder/filament-logger

Audit logging, activity tracking, exports, alerts, and dashboards for Filament admin panels.

2318.8k](/packages/mradder-filament-logger)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
