Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
52f149e
chore: gitignore .planning workspace
ManukMinasyan Apr 26, 2026
6ca85c6
docs: scaffold new directory tree (placeholders)
ManukMinasyan Apr 26, 2026
cfc360b
docs: write concepts page (mental model + type taxonomy)
ManukMinasyan Apr 26, 2026
2ff7025
docs(concepts): fix renderer link and clarify source-origin filtering
ManukMinasyan Apr 26, 2026
546d55c
docs: write sources page (4 source types + perf notes + addSource)
ManukMinasyan Apr 26, 2026
c68ea23
docs: write filament UI page (defaults + URL filter UI surface)
ManukMinasyan Apr 26, 2026
338e73a
docs: write refining the timeline page
ManukMinasyan Apr 26, 2026
0bcce68
docs: rewrite customization (single-home renderer + B7 helpers)
ManukMinasyan Apr 26, 2026
170f672
docs: write caching page (with A3 invalidation limitation)
ManukMinasyan Apr 26, 2026
388b2e2
docs: write configuration page (full key reference + A1 callout)
ManukMinasyan Apr 26, 2026
91cd055
docs: write quick-start page (5-minute path)
ManukMinasyan Apr 26, 2026
3d3fb7e
docs: rewrite installation (folds db schema; tailwind single home)
ManukMinasyan Apr 26, 2026
5105f52
docs: add CRM person feed recipe
ManukMinasyan Apr 26, 2026
b494282
docs: add audit-log-for-admins recipe
ManukMinasyan Apr 26, 2026
c3aedca
docs: add testing page
ManukMinasyan Apr 26, 2026
a1717ee
docs: rewrite troubleshooting (real entries; A2/A3/B8 callouts)
ManukMinasyan Apr 26, 2026
13eb869
docs: polish intro (replace cursor-pagination jargon; add quickstart …
ManukMinasyan Apr 26, 2026
0ffa9ad
docs: delete obsolete 2.essentials/ folder (content fully migrated)
ManukMinasyan Apr 26, 2026
3820179
docs(community): add SEO frontmatter to contributing + license
ManukMinasyan Apr 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ composer.lock
.phpunit.cache
.phpunit.result.cache
.DS_Store
.planning/
73 changes: 60 additions & 13 deletions docs/content/1.getting-started/1.installation.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
title: Installation
description: Install and migrate the package.
description: Install relaticle/activity-log, index your tables, and wire the panel plugin.
navigation:
icon: i-lucide-download
seo:
description: Install relaticle/activity-log in a Laravel 12 + Filament 5 app.
description: Composer install, service-provider auto-discovery, database indexing, Tailwind integration, and panel plugin registration for relaticle/activity-log.
ogImage: /preview.png
---

Expand All @@ -21,33 +21,80 @@ seo:
composer require relaticle/activity-log
```

The service provider (`Relaticle\ActivityLog\ActivityLogServiceProvider`) is auto-discovered. It registers:
## What auto-discovery wires up

- Config file (`config/activity-log.php`)
- Blade views namespaced as `activity-log::*`
- Translations (under the `activity-log::messages.*` namespace)
The service provider (`Relaticle\ActivityLog\ActivityLogServiceProvider`) is auto-discovered and registers:

- Config file at `config/activity-log.php` (via `spatie/laravel-package-tools`)
- Views under the `activity-log::*` namespace
- Translations under the `activity-log::messages.*` namespace
- `RendererRegistry` and `TimelineCache` singletons
- A Livewire component registered as `activity-log`
- The built-in `activity_log` renderer
- The `activity-log` Livewire component (`Relaticle\ActivityLog\Filament\Livewire\ActivityLogLivewire`)
- The built-in `Relaticle\ActivityLog\Renderers\ActivityLogRenderer`, auto-registered for the `'activity_log'` type

## Publish the config (optional)

Only needed if you want to override defaults:

```bash [Terminal]
php artisan vendor:publish --tag=activity-log-config
```

## Index the `activity_log` table
See [Configuration](/essentials/configuration) for the full key reference.

## Database & indexing

The package owns **no migrations**; it reads from tables already present in your application.

| Table | Owner | Role |
| --- | --- | --- |
| `activity_log` | `spatie/laravel-activitylog` | Primary source of `activity_log` (own log) and related-log entries via `fromActivityLog()` and `fromActivityLogOf()`. |
| Your related tables | Your app | Sources registered via `fromRelation()` read their own timestamp columns. |

The plugin does not ship a migration (the table is owned by `spatie/laravel-activitylog`). For good performance on timeline queries, add this compound index:
### Required index

Add this compound index to the spatie `activity_log` table for responsive timeline queries:

```php
$table->index(['subject_type', 'subject_id', 'created_at']);
Schema::table('activity_log', function (Blueprint $table) {
$table->index(['subject_type', 'subject_id', 'created_at']);
});
```

## Tailwind source (custom panel themes)
Without it, paginating large logs scans significantly more rows than necessary.

### Related-model timestamp columns

When you register `fromRelation('tasks', fn ($s) => $s->event(column: 'completed_at', ...))`, the source filters the related table by the configured timestamp column. Index those columns when the related table is large.

If your panel uses a custom `theme.css`, include the plugin's views so Tailwind compiles the utilities used by the Blade templates:
## Tailwind theme integration

If your panel uses a custom `theme.css`, add the plugin's views to the Tailwind source list so the utilities used by the Blade templates are compiled:

```css [resources/css/filament/{panel}/theme.css]
@source '../../../../vendor/relaticle/activity-log/resources/views/**/*';
```

Without this line, you may see unstyled or partially-styled timeline entries in production builds. Skip this step if your panel uses the default Filament theme — Tailwind is already wired.

## Register the panel plugin (optional)

Only needed when you want to register custom renderers; auto-discovery covers everything else. Use the **Filament-namespaced plugin** (not the orphan root `Relaticle\ActivityLog\ActivityLogPlugin` — see [issue #13](https://github.com/relaticle/activity-log/issues/13)):

```php
use Relaticle\ActivityLog\Filament\ActivityLogPlugin;

public function panel(Panel $panel): Panel
{
return $panel
->plugin(ActivityLogPlugin::make())
// ... rest of panel config
;
}
```

See [Customization](/essentials/customization) for what to pass to `->renderers()`.

## Next

Head to [Quick start](/getting-started/quick-start) for the 5-minute working timeline.
88 changes: 88 additions & 0 deletions docs/content/1.getting-started/2.quick-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
title: Quick start
description: Working timeline in 5 minutes — Person model, spatie log, infolist render.
navigation:
icon: i-lucide-rocket
seo:
description: 5-minute quickstart for relaticle/activity-log — minimal model setup and first timeline render in a Filament resource.
ogImage: /preview.png
---

The smallest end-to-end path: log activity on one model with spatie, mount the timeline infolist on its Filament resource, watch entries appear. Once it works, see [/essentials/sources](/essentials/sources) to compose more sources.

## Prerequisites

- Package installed (`composer require relaticle/activity-log`) — see [/getting-started/installation](/getting-started/installation).
- `spatie/laravel-activitylog`'s `activity_log` table migrated.
- At least one Filament resource ready to host the timeline (this guide uses `PersonResource`).

## Step 1 — Make a model timeline-capable

```php [app/Models/Person.php]
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Relaticle\ActivityLog\Concerns\InteractsWithTimeline;
use Relaticle\ActivityLog\Contracts\HasTimeline;
use Relaticle\ActivityLog\Timeline\TimelineBuilder;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

final class Person extends Model implements HasTimeline
{
use InteractsWithTimeline;
use LogsActivity;

protected $fillable = ['name', 'email'];

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

public function timeline(): TimelineBuilder
{
return TimelineBuilder::make($this)->fromActivityLog();
}
}
```

`LogsActivity` (paired with `getActivitylogOptions`) makes spatie record create / update / delete events into the `activity_log` table. `HasTimeline` plus the `InteractsWithTimeline` trait give you `timeline()`, `paginateTimeline()`, and `forgetTimelineCache()` on the model. `fromActivityLog()` is the simplest source — the subject's own log.
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InteractsWithTimeline doesn’t provide timeline(); consumers must implement it to satisfy HasTimeline. This paragraph currently implies the trait “gives you timeline()” — suggest rewording to say it provides paginateTimeline() / forgetTimelineCache() helpers while you define timeline() yourself.

Suggested change
`LogsActivity` (paired with `getActivitylogOptions`) makes spatie record create / update / delete events into the `activity_log` table. `HasTimeline` plus the `InteractsWithTimeline` trait give you `timeline()`, `paginateTimeline()`, and `forgetTimelineCache()` on the model. `fromActivityLog()` is the simplest source — the subject's own log.
`LogsActivity` (paired with `getActivitylogOptions`) makes spatie record create / update / delete events into the `activity_log` table. `HasTimeline` requires your model to define `timeline()`, while the `InteractsWithTimeline` trait provides helpers like `paginateTimeline()` and `forgetTimelineCache()`. `fromActivityLog()` is the simplest source — the subject's own log.

Copilot uses AI. Check for mistakes.

## Step 2 — Render the timeline on the resource view page

```php [app/Filament/Resources/PersonResource.php]
namespace App\Filament\Resources;

use Filament\Schemas\Schema;
use Relaticle\ActivityLog\Filament\Infolists\Components\ActivityLog;

public function infolist(Schema $schema): Schema
{
return $schema->components([
// ... your existing entries
ActivityLog::make('activity')->columnSpanFull(),
]);
}
```

::callout{icon="i-lucide-info" color="info"}
`ActivityLog` must live on a page that renders the model record (typically `ViewRecord` or `EditRecord`). The component reads the active record from the page context.
::

## Step 3 — Trigger an activity

Edit a Person via the Filament UI, or via tinker:

```bash [Terminal]
php artisan tinker --execute 'App\Models\Person::find(1)->update(["name" => "New name"]);'
```

Refresh the view page — an entry titled "updated" appears, with the changed-fields diff inline (rendered by the built-in `ActivityLogRenderer`).

## Where to next?

- **Compose more sources** — pull in events from related models (`fromActivityLogOf`), timestamp columns (`fromRelation`), or external APIs (`fromCustom`). See [/essentials/sources](/essentials/sources).
- **Use the relation manager or header action** — alternative Filament surfaces for the timeline. See [/essentials/filament-ui](/essentials/filament-ui).
- **Customize the rendering** — replace the spatie diff renderer per event or wholesale. See [/essentials/customization](/essentials/customization).
- **See it in context** — full real-world wiring patterns at [/recipes/crm-person-feed](/recipes/crm-person-feed).
2 changes: 2 additions & 0 deletions docs/content/2.concepts/.navigation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
title: Concepts
icon: i-lucide-compass
122 changes: 122 additions & 0 deletions docs/content/2.concepts/1.how-it-works.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: How it works
description: The mental model and vocabulary behind the timeline pipeline.
navigation:
icon: i-lucide-compass
seo:
description: Mental model, vocabulary, and pipeline behind relaticle/activity-log timelines.
ogImage: /preview.png
---

This page is the canonical reference for the package's mental model: the pipeline, the building blocks, the type taxonomy, and the dedup/priority rules. Read it once before configuring sources, writing renderers, or debugging missing entries.

## The pipeline

```text
$record->timeline() [TimelineBuilder]
↓ resolves
TimelineSource(s) per source [resolve(subject, Window)]
↓ yields
TimelineEntry stream [filter → dedup → sort]
↓ paginated
LengthAwarePaginator → Renderer → Blade view
```

A call to `$record->timeline()` returns a fluent `TimelineBuilder`. Each registered source is asked to `resolve()` entries inside a shared `Window`. The combined stream is filtered, deduplicated, sorted, and either paginated or returned whole. Renderers turn each `TimelineEntry` into HTML at view time.

## Core building blocks

**Subject.** Any Eloquent model implementing `Relaticle\ActivityLog\Contracts\HasTimeline`. In practice you get this for free by using the `Relaticle\ActivityLog\Concerns\InteractsWithTimeline` trait, which provides `timeline()`, `paginateTimeline()`, and `forgetTimelineCache()`. The subject is passed into every source's `resolve()` call.
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InteractsWithTimeline does not implement timeline()—it only provides paginateTimeline() and forgetTimelineCache() (the HasTimeline contract still requires consumers to define timeline(): TimelineBuilder). Update this sentence to avoid implying timeline() comes “for free” from the trait.

Suggested change
**Subject.** Any Eloquent model implementing `Relaticle\ActivityLog\Contracts\HasTimeline`. In practice you get this for free by using the `Relaticle\ActivityLog\Concerns\InteractsWithTimeline` trait, which provides `timeline()`, `paginateTimeline()`, and `forgetTimelineCache()`. The subject is passed into every source's `resolve()` call.
**Subject.** Any Eloquent model implementing `Relaticle\ActivityLog\Contracts\HasTimeline`. The `Relaticle\ActivityLog\Concerns\InteractsWithTimeline` trait provides helper methods such as `paginateTimeline()` and `forgetTimelineCache()`, but models still need to define `timeline(): TimelineBuilder` to satisfy the contract. The subject is passed into every source's `resolve()` call.

Copilot uses AI. Check for mistakes.

**Source.** A class implementing `Relaticle\ActivityLog\Contracts\TimelineSource`. Sources own the data: they query the database (or any other store) and `yield` `TimelineEntry` instances. The package ships four built-ins, registered via the builder's helpers:

```php
$record->timeline()
->fromActivityLog() // ActivityLogSource
->fromActivityLogOf(['comments']) // RelatedActivityLogSource
->fromRelation('invoices', fn ($s) => $s->title(...)) // RelatedModelSource
->fromCustom(fn ($subject, $window) => yield ...); // CustomEventSource
```

See [/essentials/sources](/essentials/sources) for full source configuration.

**Entry.** `Relaticle\ActivityLog\Timeline\TimelineEntry` — an immutable `readonly` value object. It carries `id`, `type`, `event`, `occurredAt`, `dedupKey`, `sourcePriority`, optional `subject`/`causer`/`relatedModel`, plus presentation hints (`title`, `description`, `icon`, `color`, `renderer`, `properties`). Sources construct entries; the builder, dedup, sort, and renderer only consume them.

**Renderer.** A class implementing `Relaticle\ActivityLog\Contracts\TimelineRenderer`. Given a `TimelineEntry`, it returns a `View` or `HtmlString`. Renderers are looked up by the entry's `renderer` field (explicit), then its `event`, then its `type`, falling back to `DefaultRenderer`. See [/essentials/customization](/essentials/customization).

## The `Window` value object

`Relaticle\ActivityLog\Timeline\Window` is the read-only context passed to every source's `resolve()`. It carries:

- `from` / `to` — the date range set via `->between($from, $to)` (both nullable).
- `cap` — the per-source over-fetch limit. The builder computes `cap = perPage * (page + buffer)` for paginated reads, and uses a hard `10000` ceiling for `->get()`.
- `typeAllow` / `typeDeny` / `eventAllow` / `eventDeny` — the active filters, mirrored so sources can push them down into queries (otherwise the builder applies them post-yield).

Sources should respect `cap` to keep memory bounded — `RelatedActivityLogSource`, for example, calls `->limit($window->cap)` on its underlying query. The builder constructs the `Window` via `makeWindow($cap)`.

## Type taxonomy

::callout{icon="i-lucide-alert-triangle" color="warning"}
**Two distinct "type" axes exist. Do not confuse them.**

The **entry-type** axis (`$entry->type`) has **3 values today**:

- `activity_log`
- `related_model`
- `custom`

The **source-priority config** axis (`source_priorities` config keys) has **4 keys**:

- `activity_log`
- `related_activity_log`
- `related_model`
- `custom`

Critically: RelatedActivityLogSource emits entries with `type='activity_log'` (**not** `'related_activity_log'`). Filtering with `->ofType(['related_activity_log'])` will never match anything. To distinguish own-log entries from related-log entries today, inspect `$entry->relatedModel` (it's `null` for `ActivityLogSource` entries and an Eloquent model for `RelatedActivityLogSource` entries) after calling `->get()` — there is no builder-level filter for source-of-origin.

See [/troubleshooting](/troubleshooting) ("Type filter doesn't match anything") and [issue #11](https://github.com/relaticle/activity-log/issues/11).
::

## Source priorities

Defaults live in `config/activity-log.php` under `source_priorities`. Higher priority wins on dedup ties.

| Source | Default priority | Config key |
| -------------------------- | ---------------- | ---------------------------------------- |
| `ActivityLogSource` | 10 | `source_priorities.activity_log` |
| `RelatedActivityLogSource` | 10 | `source_priorities.related_activity_log` |
| `RelatedModelSource` | 20 | `source_priorities.related_model` |
| `CustomEventSource` | 30 | `source_priorities.custom` |

Override per-call by passing the second argument to any builder helper:

```php
$record->timeline()->fromCustom($resolver, priority: 100);
```

## Dedup behavior

Entries sharing a `dedupKey` collapse to a single entry: the one with the highest `sourcePriority` wins. On equal priority, **first-seen wins** (sources are resolved in the order they were registered).

The default `dedupKey` is generated by `AbstractTimelineSource::dedupKeyFor()`:

```text
{class}:{id}:{occurredAt-iso}
```

Override per builder:

```php
$record->timeline()
->dedupKeyUsing(fn (TimelineEntry $entry): string => "{$entry->type}:{$entry->event}:{$entry->occurredAt->toDateString()}");
```

Disable dedup entirely with `->deduplicate(false)`.

## Lifecycle

1. The builder calls each registered source's `resolve($subject, $window)`.
2. Yielded entries pass through `passesFilters()` — `typeAllow`/`typeDeny`/`eventAllow`/`eventDeny`.
3. Dedup is applied if enabled (default: `true`, overridable in config via `deduplicate_by_default`).
4. The collection is sorted — `sortByDateDesc()` is the default; `sortByDateAsc()` flips it.
5. Results are returned via `paginate(perPage, page)` (the standard Filament/Livewire path) or `get()` (capped at 10000 entries).
1 change: 0 additions & 1 deletion docs/content/2.essentials/.navigation.yml

This file was deleted.

Loading
Loading