├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── postcss.config.cjs ├── resources ├── css │ └── index.css ├── dist │ └── .gitkeep ├── js │ └── index.js └── views │ ├── components │ ├── empty-state.blade.php │ └── section.blade.php │ ├── infolists │ └── components │ │ ├── activity-date.blade.php │ │ ├── activity-description.blade.php │ │ ├── activity-icon.blade.php │ │ ├── activity-section.blade.php │ │ └── activity-title.blade.php │ └── pages │ └── view-activities.blade.php └── src ├── ActivityTimelineServiceProvider.php ├── Components ├── ActivityDate.php ├── ActivityDescription.php ├── ActivityIcon.php ├── ActivitySection.php └── ActivityTitle.php ├── Concerns ├── CanModifyState.php ├── HasEmptyState.php └── HasSetting.php ├── Enums └── IconAnimation.php └── Pages └── ActivityTimelinePage.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `activity-timeline` will be documented in this file. 4 | 5 | ## v1.2.12 - 2025-05-15 6 | 7 | ### What's Changed 8 | 9 | * Added modify_state to activity_date by @albertobenavides in https://github.com/199ocero/activity-timeline/pull/38 10 | 11 | ### New Contributors 12 | 13 | * @albertobenavides made their first contribution in https://github.com/199ocero/activity-timeline/pull/38 14 | 15 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.11...v1.2.12 16 | 17 | ## v1.2.11 - 2025-02-27 18 | 19 | ### What's Changed 20 | 21 | * add support for laravel 12 by @atmonshi in https://github.com/199ocero/activity-timeline/pull/37 22 | * Added allow_html and modify_state handlers by @dotmot in https://github.com/199ocero/activity-timeline/pull/36 23 | 24 | ### New Contributors 25 | 26 | * @dotmot made their first contribution in https://github.com/199ocero/activity-timeline/pull/36 27 | 28 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.10...v1.2.11 29 | 30 | ## v1.2.10 - 2024-12-07 31 | 32 | ### What's Changed 33 | 34 | * Fix formatting of null values by @randomnetcat in https://github.com/199ocero/activity-timeline/pull/33 35 | * Add possibility to animate icon by @agencetwogether in https://github.com/199ocero/activity-timeline/pull/32 36 | 37 | ### New Contributors 38 | 39 | * @randomnetcat made their first contribution in https://github.com/199ocero/activity-timeline/pull/33 40 | * @agencetwogether made their first contribution in https://github.com/199ocero/activity-timeline/pull/32 41 | 42 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.9...v1.2.10 43 | 44 | ## v1.2.9 - 2024-09-04 45 | 46 | ### What's Changed 47 | 48 | * Display changes from null to a value by @pablo-gonzalez-helpwan in https://github.com/199ocero/activity-timeline/pull/30 49 | 50 | ### New Contributors 51 | 52 | * @pablo-gonzalez-helpwan made their first contribution in https://github.com/199ocero/activity-timeline/pull/30 53 | 54 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.8...v1.2.9 55 | 56 | ## v1.2.8 - 2024-05-23 57 | 58 | ### What's Changed 59 | 60 | * Fix error when model column is array for old value by @SujalRatnaTamrakar in https://github.com/199ocero/activity-timeline/pull/27 61 | 62 | ### New Contributors 63 | 64 | * @SujalRatnaTamrakar made their first contribution in https://github.com/199ocero/activity-timeline/pull/27 65 | 66 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.7...v1.2.8 67 | 68 | ## v1.2.7 - 2024-05-02 69 | 70 | ### What's Changed 71 | 72 | * Fix error when model column is array by @henryavila in https://github.com/199ocero/activity-timeline/pull/26 73 | 74 | ### New Contributors 75 | 76 | * @henryavila made their first contribution in https://github.com/199ocero/activity-timeline/pull/26 77 | 78 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.6...v1.2.7 79 | 80 | ## v1.2.6 - 2024-03-07 81 | 82 | ### What's Changed 83 | 84 | * Add placeholder for null values and display logs for null to not-null changes by @RixzZ in https://github.com/199ocero/activity-timeline/pull/22 85 | 86 | ### New Contributors 87 | 88 | * @RixzZ made their first contribution in https://github.com/199ocero/activity-timeline/pull/22 89 | 90 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.5...v1.2.6 91 | 92 | ## v1.2.5 - 2024-02-28 93 | 94 | ### What's Changed 95 | 96 | * Add CanAllowHtml and CanModifyState by @Puralogica in https://github.com/199ocero/activity-timeline/pull/20 97 | * add Laravel 11 support by @atmonshi in https://github.com/199ocero/activity-timeline/pull/21 98 | 99 | ### New Contributors 100 | 101 | * @Puralogica made their first contribution in https://github.com/199ocero/activity-timeline/pull/20 102 | 103 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.4...v1.2.5 104 | 105 | ## v1.2.4 - 2024-02-03 106 | 107 | ### What's Changed 108 | 109 | * Moved activity query out of private function by @bilaliqbalr in https://github.com/199ocero/activity-timeline/pull/19 110 | 111 | ### New Contributors 112 | 113 | * @bilaliqbalr made their first contribution in https://github.com/199ocero/activity-timeline/pull/19 114 | 115 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.3...v1.2.4 116 | 117 | ## v1.2.3 - 2024-01-30 118 | 119 | ### What's Changed 120 | 121 | * Eager load `subject` to prevent N+1 problem by @iRaziul in https://github.com/199ocero/activity-timeline/pull/18 122 | 123 | ### New Contributors 124 | 125 | * @iRaziul made their first contribution in https://github.com/199ocero/activity-timeline/pull/18 126 | 127 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.2...v1.2.3 128 | 129 | ## v1.2.2 - 2024-01-20 130 | 131 | ### What's Changed 132 | 133 | * improve section UI and add collapse effect by @atmonshi in https://github.com/199ocero/activity-timeline/pull/13 134 | * add support to pass any `extraAttributes` by @atmonshi in https://github.com/199ocero/activity-timeline/pull/14 135 | * added heading visible and docu by @199ocero in https://github.com/199ocero/activity-timeline/pull/15 136 | 137 | ### New Contributors 138 | 139 | * @atmonshi made their first contribution in https://github.com/199ocero/activity-timeline/pull/13 140 | 141 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.1...v1.2.2 142 | 143 | ## v1.2.1 - 2024-01-19 144 | 145 | ### What's Changed 146 | 147 | * added css hook classes and minor bug fixes by @199ocero in https://github.com/199ocero/activity-timeline/pull/12 148 | 149 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.2.0...v1.2.1 150 | 151 | ## v1.2.0 - 2024-01-17 152 | 153 | ### What's Changed 154 | 155 | * adding support for spatie activity log by @199ocero in https://github.com/199ocero/activity-timeline/pull/9 156 | 157 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.1.0...v1.2.0 158 | 159 | ## v1.1.0 - 2024-01-14 160 | 161 | ### What's Changed 162 | 163 | * Added empty state by @199ocero in https://github.com/199ocero/activity-timeline/pull/8 164 | 165 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.0.2...v1.1.0 166 | 167 | ## v1.0.2 - 2024-01-08 168 | 169 | ### What's Changed 170 | 171 | * fix allow html always true by @199ocero in https://github.com/199ocero/activity-timeline/pull/5 172 | * added docs for using record method by @199ocero in https://github.com/199ocero/activity-timeline/pull/6 173 | 174 | ### New Contributors 175 | 176 | * @199ocero made their first contribution in https://github.com/199ocero/activity-timeline/pull/5 177 | 178 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.0.1...v1.0.2 179 | 180 | ## v1.0.1 - 2024-01-07 181 | 182 | **Full Changelog**: https://github.com/199ocero/activity-timeline/compare/v1.0.0...v1.0.1 183 | 184 | ## v1.0.0 - 2024-01-06 185 | 186 | First release 187 | 188 | ## 1.0.0 - 202X-XX-XX 189 | 190 | - initial release 191 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) jaocero <199ocero@gmail.com> 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Activity Timeline 2 | 3 |
4 | 5 | ![Header](https://raw.githubusercontent.com/199ocero/activity-timeline/main/art/images/jaocero-activity-timeline.jpeg) 6 | 7 |
8 | 9 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/jaocero/activity-timeline.svg?style=flat-square)](https://packagist.org/packages/jaocero/activity-timeline) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/jaocero/activity-timeline.svg?style=flat-square)](https://packagist.org/packages/jaocero/activity-timeline) 11 | 12 | Add timelines to custom pages or infolist entries effortlessly. Plus, it teams up smoothly with Spatie Activitylog for easy tracking. 13 | 14 | ## Installation 15 | 16 | You can install the package via composer: 17 | 18 | ```bash 19 | composer require jaocero/activity-timeline 20 | ``` 21 | 22 | To adhere to Filament's theming approach, you'll be required to employ a personalized theme in order to utilize this plugin. 23 | 24 | > **Custom Theme Installation** > [Filament Docs](https://filamentphp.com/docs/3.x/panels/themes#creating-a-custom-theme) 25 | 26 | Add the plugin's views to your `tailwind.config.js` file. 27 | 28 | ```js 29 | content: [ 30 | ...'./vendor/jaocero/activity-timeline/resources/views/**/*.blade.php', 31 | ] 32 | ``` 33 | 34 | ## Usage 35 | 36 | This plugin is already accessible within the Infolists builder and now supports both the `->state([])` and `->record()` methods. 37 | 38 | ```php 39 | use JaOcero\ActivityTimeline\Enums\IconAnimation; 40 | 41 | public function activityTimelineInfolist(Infolist $infolist): Infolist 42 | { 43 | return $infolist 44 | ->state([ 45 | 'activities' => [ 46 | [ 47 | 'title' => "Published Article 🔥 - Published with Laravel Filament and Tailwind CSS", 48 | 'description' => "Approved and published. Here is the link.", 49 | 'status' => 'published', 50 | 'created_at' => now()->addDays(8), 51 | ], 52 | [ 53 | 'title' => 'Reviewing Article - Final Touches', 54 | 'description' => "Reviewing the article and making it ready for publication.", 55 | 'status' => '', 56 | 'created_at' => now()->addDays(5), 57 | ], 58 | [ 59 | 'title' => "Drafting Article - Make it ready for review", 60 | 'description' => 'Drafting the article and making it ready for review.', 61 | 'status' => 'drafting', 62 | 'created_at' => now()->addDays(2), 63 | ], 64 | [ 65 | 'title' => 'Ideation - Looking for Ideas 🤯', 66 | 'description' => 'Idea for my article.', 67 | 'status' => 'ideation', 68 | 'created_at' => now()->subDays(7), 69 | ] 70 | ] 71 | ]) 72 | ->schema([ 73 | 74 | /* 75 | You should enclose the entire components within a personalized "ActivitySection" entry. 76 | This section functions identically to the repeater entry; you simply have to provide the array state's key. 77 | */ 78 | 79 | ActivitySection::make('activities') 80 | ->label('My Activities') 81 | ->description('These are the activities that have been recorded.') 82 | ->schema([ 83 | ActivityTitle::make('title') 84 | ->placeholder('No title is set') 85 | ->allowHtml(), // Be aware that you will need to ensure that the HTML is safe to render, otherwise your application will be vulnerable to XSS attacks. 86 | ActivityDescription::make('description') 87 | ->placeholder('No description is set') 88 | ->allowHtml(), 89 | ActivityDate::make('created_at') 90 | ->date('F j, Y', 'Asia/Manila') 91 | ->placeholder('No date is set.'), 92 | ActivityIcon::make('status') 93 | ->icon(fn (string | null $state): string | null => match ($state) { 94 | 'ideation' => 'heroicon-m-light-bulb', 95 | 'drafting' => 'heroicon-m-bolt', 96 | 'reviewing' => 'heroicon-m-document-magnifying-glass', 97 | 'published' => 'heroicon-m-rocket-launch', 98 | default => null, 99 | }) 100 | /* 101 | You can animate icon with ->animation() method. 102 | Possible values : IconAnimation::Ping, IconAnimation::Pulse, IconAnimation::Bounce, IconAnimation::Spin or a Closure 103 | */ 104 | ->animation(IconAnimation::Ping) 105 | ->color(fn (string | null $state): string | null => match ($state) { 106 | 'ideation' => 'purple', 107 | 'drafting' => 'info', 108 | 'reviewing' => 'warning', 109 | 'published' => 'success', 110 | default => 'gray', 111 | }), 112 | ]) 113 | ->showItemsCount(2) // Show up to 2 items 114 | ->showItemsLabel('View Old') // Show "View Old" as link label 115 | ->showItemsIcon('heroicon-m-chevron-down') // Show button icon 116 | ->showItemsColor('gray') // Show button color and it supports all colors 117 | ->aside(true) 118 | ->headingVisible(true) // make heading visible or not 119 | ->extraAttributes(['class'=>'my-new-class']) // add extra class 120 | ]); 121 | } 122 | ``` 123 | 124 | When utilizing the `->record()` function, you provide your model in a manner similar to the code showcased below: 125 | 126 | ```php 127 | protected $activities; 128 | 129 | public function __construct() 130 | { 131 | $this->activities = User::query()->with('activities')->where('id', auth()->user()->id)->first(); 132 | } 133 | 134 | public function activityTimelineInfolist(Infolist $infolist): Infolist 135 | { 136 | return $infolist 137 | ->record($this->activities) 138 | // ... remaining code 139 | } 140 | ``` 141 | 142 | Sometimes, when we don't have any info to show to users, it's important to improve their experience by displaying something. So, I include an empty state, like the one in the [Filament Table Empty State](https://filamentphp.com/docs/3.x/tables/empty-state). 143 | 144 | ```php 145 | public function activityTimelineInfolist(Infolist $infolist): Infolist 146 | { 147 | return $infolist 148 | ->state([ 149 | 'activities' => [] 150 | )] 151 | ->schema([ 152 | ActivitySection::make('activities') 153 | // ... other code 154 | ->emptyStateHeading('No activities yet.') 155 | ->emptyStateDescription('Check back later for activities that have been recorded.') 156 | ->emptyStateIcon('heroicon-o-bolt-slash') 157 | ]) 158 | } 159 | ``` 160 | 161 | ## Usage with Spatie Activity Log Package 162 | 163 | This plugin works with [spatie/laravel-activitylog](https://github.com/spatie/laravel-activitylog), making it easy to log user actions in your app. It can also automatically log model events, storing everything in the `activity_log` table. To use the plugin, just install `spatie/laravel-activitylog`, set it up, and you're good to go. 164 | 165 | ### Creating a Custom Page 166 | 167 | You are required to create a custom page within your `resources` to display all activities based on the record passed to the route. 168 | 169 | ```php 170 | php artisan make:filament-page ViewOrderActivities --resource=OrderResource --type=custom 171 | ``` 172 | 173 | ### Including the Page in your Resource Class 174 | 175 | Simply include the custom page in the `getPages()` method so that we can access it. 176 | 177 | ```php 178 | public static function getPages(): array 179 | { 180 | return [ 181 | // ... other pages 182 | // The format of route doesn't matter, as long as it includes the route parameter {record}. 183 | 'activities' => Pages\ViewOrderActivities::route('/order/{record}/activities'), 184 | ]; 185 | } 186 | ``` 187 | 188 | In the `actions` method of your table, include an additional custom action. This action should redirect users to the custom page we've generated earlier. 189 | 190 | ```php 191 | public static function table(Table $table): Table 192 | { 193 | return $table 194 | ->columns([ 195 | // ... 196 | ]) 197 | ->filters([ 198 | // ... 199 | ]) 200 | ->actions([ 201 | Tables\Actions\Action::make('view_activities') 202 | ->label('Activities') 203 | ->icon('heroicon-m-bolt') 204 | ->color('purple') 205 | ->url(fn ($record) => OrderResource::getUrl('activities', ['record' => $record])), 206 | ]) 207 | ->bulkActions([ 208 | // ... 209 | ]); 210 | } 211 | ``` 212 | 213 | ### Setting up your Custom Page 214 | 215 | Changes are needed in your custom page. Instead of extending using the regular `Page` class will make use of a specific class called `ActivityTimelinePage` provided by the plugin. Additionally, you should include your resource class. 216 | 217 | ```php 218 | use App\Filament\Resources\OrderResource; 219 | use JaOcero\ActivityTimeline\Pages\ActivityTimelinePage; 220 | 221 | class ViewOrderActivities extends ActivityTimelinePage 222 | { 223 | protected static string $resource = OrderResource::class; 224 | } 225 | ``` 226 | 227 | ### Configuration 228 | 229 | Behind the scenes, the plugin utilizes the previously mentioned infolists entry. We only modify the properties/data, but the logic remains unchanged. 230 | 231 | ```php 232 | use App\Filament\Resources\OrderResource; 233 | use JaOcero\ActivityTimeline\Pages\ActivityTimelinePage; 234 | 235 | class ViewOrderActivities extends ActivityTimelinePage 236 | { 237 | protected static string $resource = OrderResource::class; 238 | 239 | protected function configuration(): array 240 | { 241 | return [ 242 | 'activity_section' => [ 243 | 'label' => 'Activities', // label for the section 244 | 'description' => 'These are the activities that have been recorded.', // description for the section 245 | 'show_items_count' => 0, // show the number of items to be shown 246 | 'show_items_label' => 'Show more', // show button label 247 | 'show_items_icon' => 'heroicon-o-chevron-down', // show button icon, 248 | 'show_items_color' => 'gray', // show button color, 249 | 'aside' => true, // show the section in the aside 250 | 'empty_state_heading' => 'No activities yet', // heading for the empty state 251 | 'empty_state_description' => 'Check back later for activities that have been recorded.', // description for the empty state 252 | 'empty_state_icon' => 'heroicon-o-bolt-slash', // icon for the empty state 253 | 'heading_visible' => true, // show the heading 254 | 'extra_attributes' => [], // extra attributes 255 | ], 256 | 'activity_title' => [ 257 | 'placeholder' => 'No title is set', // this will show when there is no title 258 | 'allow_html' => true, // set true to allow html in the title 259 | 260 | /** 261 | * You are free to adjust the state before displaying it on your page. 262 | * Take note that the state returns these data below: 263 | * [ 264 | * 'log_name' => $activity->log_name, 265 | * 'description' => $activity->description, 266 | * 'subject' => $activity->subject, 267 | * 'event' => $activity->event, 268 | * 'causer' => $activity->causer, 269 | * 'properties' => json_decode($activity->properties, true), 270 | * 'batch_uuid' => $activity->batch_uuid, 271 | * ] 272 | 273 | * If you wish to make modifications, please refer to the default code in the HasSetting trait. 274 | */ 275 | 276 | // 'modify_state' => function (array $state) { 277 | // 278 | // } 279 | 280 | ], 281 | 'activity_description' => [ 282 | 'placeholder' => 'No description is set', // this will show when there is no description 283 | 'allow_html' => true, // set true to allow html in the description 284 | 285 | /** 286 | * You are free to adjust the state before displaying it on your page. 287 | * Take note that the state returns these data below: 288 | * [ 289 | * 'log_name' => $activity->log_name, 290 | * 'description' => $activity->description, 291 | * 'subject' => $activity->subject, 292 | * 'event' => $activity->event, 293 | * 'causer' => $activity->causer, 294 | * 'properties' => json_decode($activity->properties, true), 295 | * 'batch_uuid' => $activity->batch_uuid, 296 | * ] 297 | 298 | * If you wish to make modifications, please refer to the default code in the HasSetting trait. 299 | */ 300 | 301 | // 'modify_state' => function (array $state) { 302 | // 303 | // } 304 | 305 | ], 306 | 'activity_date' => [ 307 | 'name' => 'created_at', // or updated_at 308 | 'date' => 'F j, Y g:i A', // date format 309 | 'placeholder' => 'No date is set', // this will show when there is no date 310 | 'modify_state' => function ($state) { 311 | return new HtmlString($state); 312 | } 313 | ], 314 | 'activity_icon' => [ 315 | 'icon' => fn (string | null $state): string | null => match ($state) { 316 | /** 317 | * 'event_name' => 'heroicon-o-calendar', 318 | * ... and more 319 | */ 320 | default => null 321 | }, 322 | 'color' => fn (string | null $state): string | null => match ($state) { 323 | /** 324 | * 'event_name' => 'primary', 325 | * ... and more 326 | */ 327 | default => null 328 | }, 329 | ] 330 | ]; 331 | } 332 | } 333 | ``` 334 | 335 | ## Style customization 336 | 337 | Similar to Filament, this plugin also includes CSS `hook` classes that enable the customization of different HTML elements through CSS. 338 | 339 | ```css 340 | .fi-timeline-section { 341 | @apply bg-transparent !important; 342 | } 343 | ``` 344 | 345 | This plugin comes with numerous CSS `hook` classes. For a straightforward approach, consider using your browser's developer tools to carefully examine the element and identify these classes. 346 | 347 | That's all! If you encounter any issues or have features you'd like to discuss, feel free to chat with me in our Discord channel. 348 | 349 | ## Changelog 350 | 351 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 352 | 353 | ## Contributing 354 | 355 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 356 | 357 | ## Security Vulnerabilities 358 | 359 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 360 | 361 | ## Credits 362 | 363 | - [Jay-Are Ocero](https://github.com/199ocero) 364 | - [All Contributors](../../contributors) 365 | 366 | ## License 367 | 368 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 369 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jaocero/activity-timeline", 3 | "description": "Add timelines to custom pages or infolist entries effortlessly. Plus, it teams up smoothly with Spatie Activitylog for easy tracking.", 4 | "keywords": [ 5 | "jaocero", 6 | "laravel", 7 | "activity-timeline", 8 | "timeline", 9 | "filament-infolists", 10 | "filamentphp" 11 | ], 12 | "homepage": "https://github.com/jaocero/activity-timeline", 13 | "support": { 14 | "issues": "https://github.com/jaocero/activity-timeline/issues", 15 | "source": "https://github.com/jaocero/activity-timeline" 16 | }, 17 | "license": "MIT", 18 | "authors": [ 19 | { 20 | "name": "Jay-Are Ocero", 21 | "email": "199ocero@gmail.com", 22 | "role": "Developer" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.1", 27 | "filament/filament": "^3.0", 28 | "spatie/laravel-package-tools": "^1.15.0", 29 | "illuminate/contracts": "^10.0|^11.0|^12.0" 30 | }, 31 | "require-dev": { 32 | "nunomaduro/collision": "^7.9", 33 | "orchestra/testbench": "^8.0|^9.0", 34 | "pestphp/pest": "^2.0", 35 | "pestphp/pest-plugin-arch": "^2.0", 36 | "pestphp/pest-plugin-laravel": "^2.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "JaOcero\\ActivityTimeline\\": "src/", 41 | "JaOcero\\ActivityTimeline\\Database\\Factories\\": "database/factories/" 42 | } 43 | }, 44 | "autoload-dev": { 45 | "psr-4": { 46 | "JaOcero\\ActivityTimeline\\Tests\\": "tests/" 47 | } 48 | }, 49 | "scripts": { 50 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 51 | "test": "vendor/bin/pest", 52 | "test-coverage": "vendor/bin/pest --coverage" 53 | }, 54 | "config": { 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true, 58 | "phpstan/extension-installer": true 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "JaOcero\\ActivityTimeline\\ActivityTimelineServiceProvider" 65 | ] 66 | } 67 | }, 68 | "minimum-stability": "dev", 69 | "prefer-stable": true 70 | } 71 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /resources/css/index.css: -------------------------------------------------------------------------------- 1 | @import '../../vendor/filament/filament/resources/css/theme.css'; 2 | -------------------------------------------------------------------------------- /resources/dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/199ocero/activity-timeline/c68f78fba3be717f4b6e3cb8be03dc13bb906b44/resources/dist/.gitkeep -------------------------------------------------------------------------------- /resources/js/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/199ocero/activity-timeline/c68f78fba3be717f4b6e3cb8be03dc13bb906b44/resources/js/index.js -------------------------------------------------------------------------------- /resources/views/components/empty-state.blade.php: -------------------------------------------------------------------------------- 1 | @props([ 2 | 'description' => null, 3 | 'heading', 4 | 'icon', 5 | ]) 6 | 7 |
class(['fi-timeline-empty-state px-6 py-12']) }}> 8 |
9 |
10 | 12 |
13 | 14 |

class(['fi-timeline-empty-state-heading text-base font-semibold leading-6 text-gray-950 dark:text-white']) }}> 16 | {{ $heading }} 17 |

18 | 19 | @if ($description) 20 |

class(['fi-timeline-empty-state-description text-sm text-gray-500 dark:text-gray-400']) }}> 22 | {{ $description }} 23 |

24 | @endif 25 |
26 |
27 | -------------------------------------------------------------------------------- /resources/views/components/section.blade.php: -------------------------------------------------------------------------------- 1 | @props(['heading', 'headingVisible' => true, 'description' => null, 'aside' => true, 'extraAttributes' => []]) 2 | 3 |
merge($extraAttributes, escape: false)->class([ 5 | 'fi-timeline-section', 6 | 'grid items-start grid-cols-1 gap-x-6 gap-y-4 md:grid-cols-3' => $aside && $headingVisible, 7 | 'bg-white shadow-sm rounded-xl ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10' => !$aside, 8 | ]) }}> 9 | 10 | @if ($headingVisible) 11 |
!$aside, 14 | 'flex flex-col gap-3 overflow-hidden sm:flex-row sm:items-center' => $aside, 15 | ])> 16 |
17 |

19 | {{ $heading }} 20 |

21 |

22 | {{ $description }} 23 |

24 |
25 |
26 | @endif 27 | 28 | @if ($aside) 29 |
31 | {{ $slot }} 32 |
33 | @else 34 |
$headingVisible, 37 | ])> 38 | {{ $slot }} 39 |
40 | @endif 41 |
42 | -------------------------------------------------------------------------------- /resources/views/infolists/components/activity-date.blade.php: -------------------------------------------------------------------------------- 1 | 2 | @if ($isHtmlAllowed()) 3 | {!! $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) !!} 4 | @else 5 | {{ $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) }} 6 | @endif 7 | 8 | -------------------------------------------------------------------------------- /resources/views/infolists/components/activity-description.blade.php: -------------------------------------------------------------------------------- 1 |

2 | @if ($isHtmlAllowed()) 3 | {!! $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) !!} 4 | @else 5 | {{ $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) }} 6 | @endif 7 |

8 | -------------------------------------------------------------------------------- /resources/views/infolists/components/activity-icon.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | use JaOcero\ActivityTimeline\Enums\IconAnimation; 3 | @endphp 4 | @if ($icon = $getIcon($getState())) 5 | @php 6 | $color = $getColor($getState()) ?? 'gray'; 7 | $animation = $getAnimation($getState()); 8 | @endphp 9 | 10 |
'animate-spin', 14 | IconAnimation::Ping, 'ping' => 'animate-ping', 15 | IconAnimation::Pulse, 'pulse' => 'animate-pulse', 16 | IconAnimation::Bounce, 'bounce' => 'animate-bounce', 17 | default => $animation, 18 | }, 19 | ])> 20 | 'bg-gray-100', 24 | default => 'fi-color-custom bg-custom-100', 25 | }, 26 | ]) @style([ 27 | \Filament\Support\get_color_css_variables($color, shades: [100, 800], alias: 'infolists::components.icon-entry.item') => $color !== 'gray', 28 | ])> 29 | 'fi-color-gray text-gray-400 dark:text-gray-500', 33 | default => 'fi-color-custom text-custom-500 dark:text-custom-400', 34 | }, 35 | ]) @style([ 36 | \Filament\Support\get_color_css_variables($color, shades: [400, 500], alias: 'infolists::components.icon-entry.item') => $color !== 'gray', 37 | ]) /> 38 | 39 |
40 | @else 41 |
42 |
43 |
44 | @endif 45 | -------------------------------------------------------------------------------- /resources/views/infolists/components/activity-section.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ $getLabel() ?? $getHeading() }} 4 | 5 | 6 | 7 | {{ $getDescription() }} 8 | 9 | 10 | @if (count($childComponentContainers = $getChildComponentContainers()) && 11 | count($childComponentContainers[0]->getComponents()) > 0) 12 | @php 13 | $childItemsCount = count($childComponentContainers); 14 | $showItemsCount = $getShowItemsCount() ?? $childItemsCount; 15 | @endphp 16 | 17 |
18 | @foreach ($childComponentContainers as $index => $container) 19 | @php 20 | $activityComponents = [ 21 | 'activityIcon' => null, 22 | 'activityTitle' => null, 23 | 'activityDate' => null, 24 | 'activityDescription' => null, 25 | ]; 26 | 27 | foreach ($container->getComponents() as $component) { 28 | $viewIdentifier = $component->getViewIdentifier(); 29 | if (array_key_exists($viewIdentifier, $activityComponents)) { 30 | $activityComponents[$viewIdentifier] = $component; 31 | } 32 | } 33 | 34 | extract($activityComponents); 35 | @endphp 36 | 37 | 38 |
40 | 41 |
42 | 43 |
!$loop->last, 46 | ])> 47 | {{ $activityIcon }} 48 |
49 | 50 |
!$loop->last, 54 | 'mb-0' => $loop->last, 55 | ])> 56 |
!$isAside(), 59 | 'flex-col items-start space-y-1 lg:space-y-0 lg:items-center lg:flex-row lg:justify-between lg:space-x-5' => $isAside(), 60 | ])> 61 | 62 | {{ $activityTitle }} 63 | 64 | {{ $activityDate }} 65 | 66 |
67 | 68 | {{ $activityDescription }} 69 | 70 |
71 | 72 |
73 |
74 | @endforeach 75 | 76 |
77 | @php 78 | $icon = $getShowItemsIcon(); 79 | $label = $getShowItemsLabel(); 80 | $color = $getShowItemsColor(); 81 | @endphp 82 | 84 | {{ $label }} 85 | 86 |
87 | 88 |
89 | @else 90 | 91 | @endif 92 |
93 | -------------------------------------------------------------------------------- /resources/views/infolists/components/activity-title.blade.php: -------------------------------------------------------------------------------- 1 |

2 | @if ($isHtmlAllowed()) 3 | {!! $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) !!} 4 | @else 5 | {{ $getModifiedState() ?? (!is_array($getState()) ? $getState() ?? $getPlaceholder() : null) }} 6 | @endif 7 |

8 | -------------------------------------------------------------------------------- /resources/views/pages/view-activities.blade.php: -------------------------------------------------------------------------------- 1 | 2 | {{ $this->activityInfolist }} 3 | 4 | -------------------------------------------------------------------------------- /src/ActivityTimelineServiceProvider.php: -------------------------------------------------------------------------------- 1 | name(static::$name) 29 | ->hasCommands($this->getCommands()) 30 | ->hasInstallCommand(function (InstallCommand $command) { 31 | $command->askToStarRepoOnGitHub('jaocero/activity-timeline'); 32 | }); 33 | 34 | if (file_exists($package->basePath('/../resources/views'))) { 35 | $package->hasViews(static::$viewNamespace); 36 | } 37 | } 38 | 39 | public function packageRegistered(): void {} 40 | 41 | public function packageBooted(): void 42 | { 43 | // Asset Registration 44 | FilamentAsset::register( 45 | $this->getAssets(), 46 | $this->getAssetPackageName() 47 | ); 48 | 49 | FilamentAsset::registerScriptData( 50 | $this->getScriptData(), 51 | $this->getAssetPackageName() 52 | ); 53 | 54 | // Icon Registration 55 | FilamentIcon::register($this->getIcons()); 56 | } 57 | 58 | protected function getAssetPackageName(): ?string 59 | { 60 | return 'jaocero/activity-timeline'; 61 | } 62 | 63 | /** 64 | * @return array 65 | */ 66 | protected function getAssets(): array 67 | { 68 | return [ 69 | // AlpineComponent::make('activity-timeline', __DIR__ . '/../resources/dist/components/activity-timeline.js'), 70 | // Css::make('activity-timeline-styles', __DIR__.'/../resources/dist/activity-timeline.css'), 71 | // Js::make('activity-timeline-scripts', __DIR__.'/../resources/dist/activity-timeline.js'), 72 | ]; 73 | } 74 | 75 | /** 76 | * @return array 77 | */ 78 | protected function getCommands(): array 79 | { 80 | return []; 81 | } 82 | 83 | /** 84 | * @return array 85 | */ 86 | protected function getIcons(): array 87 | { 88 | return []; 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | protected function getRoutes(): array 95 | { 96 | return []; 97 | } 98 | 99 | /** 100 | * @return array 101 | */ 102 | protected function getScriptData(): array 103 | { 104 | return []; 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | protected function getMigrations(): array 111 | { 112 | return []; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Components/ActivityDate.php: -------------------------------------------------------------------------------- 1 | dateFormat = $format; 28 | $this->dateTimezone = $timezone; 29 | 30 | return $this; 31 | } 32 | 33 | public function getDate($value): ?string 34 | { 35 | $date = Carbon::parse($value) 36 | ->setTimezone($this->getTimezone()); 37 | 38 | if ($this->getFormat() != null) { 39 | $this->date = $date->translatedFormat($this->getFormat()); 40 | } else { 41 | $this->date = $date; 42 | } 43 | 44 | return $this->evaluate($this->date); 45 | } 46 | 47 | public function getFormat(): ?string 48 | { 49 | return $this->evaluate($this->dateFormat); 50 | } 51 | 52 | public function getTimezone(): ?string 53 | { 54 | return $this->evaluate($this->dateTimezone) ?? config('app.timezone'); 55 | } 56 | 57 | public function getViewIdentifier(): string 58 | { 59 | return $this->viewIdentifier; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Components/ActivityDescription.php: -------------------------------------------------------------------------------- 1 | viewIdentifier; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Components/ActivityIcon.php: -------------------------------------------------------------------------------- 1 | animation = $animation; 20 | 21 | return $this; 22 | } 23 | 24 | public function getAnimation(mixed $state): IconAnimation|string|null 25 | { 26 | return $this->evaluate($this->animation, [ 27 | 'state' => $state, 28 | ]); 29 | } 30 | 31 | public function getViewIdentifier(): string 32 | { 33 | return $this->viewIdentifier; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Components/ActivitySection.php: -------------------------------------------------------------------------------- 1 | description = $description; 34 | 35 | return $this; 36 | } 37 | 38 | public function aside(bool|Closure|null $condition = true): static 39 | { 40 | $this->isAside = $condition; 41 | 42 | return $this; 43 | } 44 | 45 | public function showItemsCount(int|Closure $items): static 46 | { 47 | $this->showItemsCount = $items; 48 | 49 | return $this; 50 | } 51 | 52 | public function showItemsLabel(string|Closure $label): static 53 | { 54 | $this->showItemsLabel = $label; 55 | 56 | return $this; 57 | } 58 | 59 | public function showItemsIcon(string|Closure|null $icon = null): static 60 | { 61 | $this->showItemsIcon = $icon; 62 | 63 | return $this; 64 | } 65 | 66 | public function showItemsColor(string|Closure $color): static 67 | { 68 | $this->showItemsColor = $color; 69 | 70 | return $this; 71 | } 72 | 73 | public function headingVisible(bool|Closure|null $condition = true): static 74 | { 75 | $this->isHeadingVisible = $condition; 76 | 77 | return $this; 78 | } 79 | 80 | public function isAside(): bool 81 | { 82 | return (bool) ($this->evaluate($this->isAside) ?? false); 83 | } 84 | 85 | public function getDescription(): ?string 86 | { 87 | return $this->evaluate($this->description); 88 | } 89 | 90 | public function getShowItemsCount(): ?int 91 | { 92 | $showItemsCount = $this->evaluate($this->showItemsCount); 93 | 94 | if ($showItemsCount == 0) { 95 | return null; 96 | } 97 | 98 | return $showItemsCount; 99 | } 100 | 101 | public function getShowItemsLabel(): string 102 | { 103 | return $this->evaluate($this->showItemsLabel) ?? 'Show More'; 104 | } 105 | 106 | public function getShowItemsIcon(): ?string 107 | { 108 | return $this->evaluate($this->showItemsIcon); 109 | } 110 | 111 | public function getShowItemsColor(): string 112 | { 113 | return $this->evaluate($this->showItemsColor) ?? 'gray'; 114 | } 115 | 116 | public function isHeadingVisible(): bool 117 | { 118 | return (bool) ($this->evaluate($this->isHeadingVisible) ?? true); 119 | } 120 | 121 | /** 122 | * @return array 123 | */ 124 | public function getChildComponentContainers(bool $withHidden = false): array 125 | { 126 | if ((! $withHidden) && $this->isHidden()) { 127 | return []; 128 | } 129 | 130 | $containers = []; 131 | 132 | foreach ($this->getState() ?? [] as $itemKey => $itemData) { 133 | $container = $this 134 | ->getChildComponentContainer() 135 | ->getClone() 136 | ->statePath($itemKey) 137 | ->inlineLabel(false); 138 | 139 | if ($itemData instanceof Model) { 140 | $container->record($itemData); 141 | } 142 | 143 | $containers[$itemKey] = $container; 144 | } 145 | 146 | return $containers; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Components/ActivityTitle.php: -------------------------------------------------------------------------------- 1 | viewIdentifier; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Concerns/CanModifyState.php: -------------------------------------------------------------------------------- 1 | state = $callback; 15 | 16 | return $this; 17 | } 18 | 19 | public function getModifiedState(): ?HtmlString 20 | { 21 | return $this->evaluate($this->state); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Concerns/HasEmptyState.php: -------------------------------------------------------------------------------- 1 | emptyStateDescription = $description; 22 | 23 | return $this; 24 | } 25 | 26 | public function emptyStateHeading(string|Htmlable|Closure|null $heading): static 27 | { 28 | $this->emptyStateHeading = $heading; 29 | 30 | return $this; 31 | } 32 | 33 | public function emptyStateIcon(string|Closure|null $icon): static 34 | { 35 | $this->emptyStateIcon = $icon; 36 | 37 | return $this; 38 | } 39 | 40 | public function getEmptyStateDescription(): string|Htmlable|null 41 | { 42 | return $this->evaluate($this->emptyStateDescription); 43 | } 44 | 45 | public function getEmptyStateHeading(): string|Htmlable 46 | { 47 | return $this->evaluate($this->emptyStateHeading) ?? 'No '.str($this->getName())->title(); 48 | } 49 | 50 | public function getEmptyStateIcon(): string 51 | { 52 | return $this->evaluate($this->emptyStateIcon) ?? 'heroicon-o-x-mark'; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Concerns/HasSetting.php: -------------------------------------------------------------------------------- 1 | [ 21 | 'label' => 'Activities', 22 | 'description' => 'These are the activities that have been recorded.', 23 | 'show_items_count' => 0, 24 | 'show_items_label' => 'Show more', 25 | 'show_items_icon' => 'heroicon-o-chevron-right', 26 | 'show_items_color' => 'gray', 27 | 'aside' => true, 28 | 'empty_state_heading' => 'No activities yet', 29 | 'empty_state_description' => 'Check back later for activities that have been recorded.', 30 | 'empty_state_icon' => 'heroicon-o-bolt-slash', 31 | 'heading_visible' => true, 32 | 'extra_attributes' => [], 33 | ], 34 | 'activity_title' => [ 35 | 'placeholder' => 'No title is set', 36 | 'allow_html' => true, 37 | ], 38 | 'activity_description' => [ 39 | 'placeholder' => 'No description is set', 40 | 'allow_html' => true, 41 | ], 42 | 'activity_date' => [ 43 | 'name' => 'created_at', 44 | 'date' => 'F j, Y g:i A', 45 | 'placeholder' => 'No date is set', 46 | ], 47 | 'activity_icon' => [ 48 | 'icon' => fn (?string $state): ?string => match ($state) { 49 | default => null 50 | }, 51 | 'color' => fn (?string $state): ?string => match ($state) { 52 | default => null 53 | }, 54 | ], 55 | ]; 56 | } 57 | 58 | public function activityInfolist(Infolist $infolist): Infolist 59 | { 60 | $activityTitle = $this->modifiedState()['activity_title']['modify_state']; 61 | $activityDescription = $this->modifiedState()['activity_description']['modify_state']; 62 | $activityDate = $this->modifiedState()['activity_date']['modify_state']; 63 | 64 | if (isset($this->configuration()['activity_title']['modify_state'])) { 65 | $activityTitle = $this->configuration()['activity_title']['modify_state']; 66 | } 67 | 68 | if (isset($this->configuration()['activity_description']['modify_state'])) { 69 | $activityDescription = $this->configuration()['activity_description']['modify_state']; 70 | } 71 | if (isset($this->configuration()['activity_date']['modify_state'])) { 72 | $activityDate = $this->configuration()['activity_date']['modify_state']; 73 | } 74 | 75 | return $infolist 76 | ->state([ 77 | 'activities' => $this->getActivityLogRecord(), 78 | ]) 79 | ->schema([ 80 | ActivitySection::make('activities') 81 | ->label($this->configuration()['activity_section']['label']) 82 | ->description($this->configuration()['activity_section']['description']) 83 | ->schema([ 84 | ActivityTitle::make('activityData') 85 | ->placeholder($this->configuration()['activity_title']['placeholder']) 86 | ->allowHtml($this->configuration()['activity_title']['allow_html']) 87 | ->modifyState($activityTitle), 88 | ActivityDescription::make('activityData') 89 | ->placeholder($this->configuration()['activity_description']['placeholder']) 90 | ->allowHtml($this->configuration()['activity_description']['allow_html']) 91 | ->modifyState($activityDescription), 92 | ActivityDate::make($this->configuration()['activity_date']['name']) 93 | ->date($this->configuration()['activity_date']['date']) 94 | ->placeholder($this->configuration()['activity_date']['placeholder']) 95 | ->allowHtml($this->configuration()['activity_description']['allow_html']) 96 | ->modifyState($activityDate), 97 | ActivityIcon::make('event') 98 | ->icon($this->configuration()['activity_icon']['icon']) 99 | ->color($this->configuration()['activity_icon']['color']), 100 | ]) 101 | ->showItemsCount($this->configuration()['activity_section']['show_items_count']) 102 | ->showItemsLabel($this->configuration()['activity_section']['show_items_label']) 103 | ->showItemsIcon($this->configuration()['activity_section']['show_items_icon']) 104 | ->showItemsColor($this->configuration()['activity_section']['show_items_color']) 105 | ->aside($this->configuration()['activity_section']['aside']) 106 | ->emptyStateHeading($this->configuration()['activity_section']['empty_state_heading']) 107 | ->emptyStateDescription($this->configuration()['activity_section']['empty_state_description']) 108 | ->emptyStateIcon($this->configuration()['activity_section']['empty_state_icon']) 109 | ->headingVisible($this->configuration()['activity_section']['heading_visible']) 110 | ->extraAttributes($this->configuration()['activity_section']['extra_attributes']), 111 | ]) 112 | ->columns(1); 113 | } 114 | 115 | protected function getActivites(): \Illuminate\Database\Eloquent\Collection 116 | { 117 | $activityModelClass = config('activitylog.activity_model'); 118 | $activityModel = new $activityModelClass; 119 | 120 | return $activityModel::query() 121 | ->with(['causer', 'subject']) 122 | ->where('subject_id', $this->record->id) 123 | ->where('subject_type', get_class($this->record)) 124 | ->orderBy('created_at', 'desc') 125 | ->get(); 126 | } 127 | 128 | private function getActivityLogRecord(): Collection 129 | { 130 | $activities = $this->getActivites(); 131 | 132 | $activities->transform(function ($activity) { 133 | 134 | $activity->activityData = [ 135 | 'log_name' => $activity->log_name, 136 | 'description' => $activity->description, 137 | 'subject' => $activity->subject, 138 | 'event' => $activity->event, 139 | 'causer' => $activity->causer, 140 | 'properties' => json_decode($activity->properties, true), 141 | 'batch_uuid' => $activity->batch_uuid, 142 | ]; 143 | 144 | return $activity; 145 | }); 146 | 147 | return $activities; 148 | } 149 | 150 | private static function formatValue($value) 151 | { 152 | if ($value === null) { 153 | return '—'; 154 | } 155 | if (is_array($value)) { 156 | return json_encode($value); 157 | } 158 | 159 | return $value; 160 | } 161 | 162 | private function modifiedState(): array 163 | { 164 | return [ 165 | 'activity_title' => [ 166 | 'modify_state' => function (array $state) { 167 | if ($state['description'] == $state['event']) { 168 | $className = Str::lower(Str::snake(class_basename($state['subject']), ' ')); 169 | $causerName = $state['causer']->name ?? $state['causer']->first_name ?? $state['causer']->last_name ?? $state['causer']->username ?? 'Unknown'; 170 | 171 | return new HtmlString(sprintf('The %s was %s by %s.', $className, $state['event'], $causerName)); 172 | } 173 | 174 | return new HtmlString($state['description']); 175 | }, 176 | ], 177 | 'activity_description' => [ 178 | 'modify_state' => function (array $state) { 179 | 180 | $properties = $state['properties']; 181 | 182 | if (! empty($properties) && isset($properties['old']) && isset($properties['attributes'])) { 183 | 184 | $oldValues = $properties['old']; 185 | $newValues = $properties['attributes']; 186 | 187 | $changes = []; 188 | 189 | foreach ($newValues as $key => $newValue) { 190 | $oldValue = self::formatValue($oldValues[$key] ?? null); 191 | $newValue = self::formatValue($newValue); 192 | 193 | if ($oldValue != $newValue) { 194 | $changes[] = "- {$key} from ".htmlspecialchars($oldValue).' to '.htmlspecialchars($newValue).''; 195 | } 196 | } 197 | 198 | $causerName = $state['causer']->name ?? $state['causer']->first_name ?? $state['causer']->last_name ?? $state['causer']->username ?? 'Unknown'; 199 | 200 | return new HtmlString(sprintf('%s %s the following:
%s', $causerName, $state['event'], implode('
', $changes))); 201 | } 202 | 203 | return null; 204 | }, 205 | ], 206 | 'activity_date' => [ 207 | 'modify_state' => function ($state) { 208 | return new HtmlString($state); 209 | }, 210 | ], 211 | ]; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Enums/IconAnimation.php: -------------------------------------------------------------------------------- 1 | record = $this->resolveRecord($record); 20 | } 21 | } 22 | --------------------------------------------------------------------------------