From 3be46efd764e2eb08f6ab9b3abbf9b9fd1a8ac8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Bascop=C3=A9?= Date: Thu, 14 Aug 2025 18:07:21 -0400 Subject: [PATCH 01/14] created a new web route to see files by media id --- .../Http/Controllers/Api/FileController.php | 14 +++++++++++++- routes/web.php | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ProcessMaker/Http/Controllers/Api/FileController.php b/ProcessMaker/Http/Controllers/Api/FileController.php index 8cd1de0171..9fbdb07bf5 100644 --- a/ProcessMaker/Http/Controllers/Api/FileController.php +++ b/ProcessMaker/Http/Controllers/Api/FileController.php @@ -284,7 +284,19 @@ public function download(Media $file) { $path = Storage::disk('public')->path($file->id . '/' . $file->file_name); - // Register the Event + // Inline preview when requested + if (request()->boolean('inline', false)) { + if (!empty($file->file_name)) { + FilesDownloaded::dispatch($file); + } + + return response()->file($path, [ + 'Content-Type' => $file->mime_type, + 'Content-Disposition' => 'inline; filename="' . addslashes($file->file_name) . '"', + ]); + } + + // Default: force download if (!empty($file->file_name)) { FilesDownloaded::dispatch($file); } diff --git a/routes/web.php b/routes/web.php index 6173442946..02222e4ae8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ name('storage.serve'); Route::middleware('auth', 'session_kill', 'sanitize', 'force_change_password', '2fa')->group(function () { + Route::get('files/{file}/contents', [ApiFileController::class, 'download']) + ->name('web.files.download') + ->middleware('can:view,file'); + // Routes related to Authentication (password reset, etc) // Auth::routes(); Route::prefix('admin')->group(function () { From 0dd55fb9f2cf0b42f64f2d25251d57fdfb65d72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:34:15 -0300 Subject: [PATCH 02/14] Php cs fixer fixes --- ...eld_to_the_process_request_token_table.php | 3 +- tests/Feature/Api/TaskControllerTest.php | 4 +- .../unit/ProcessMaker/Models/ProcessTest.php | 94 +++++++++---------- 3 files changed, 50 insertions(+), 51 deletions(-) diff --git a/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php index 69811599b1..4877eec6d9 100644 --- a/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php +++ b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/tests/Feature/Api/TaskControllerTest.php b/tests/Feature/Api/TaskControllerTest.php index a545789a58..b0b832c32c 100644 --- a/tests/Feature/Api/TaskControllerTest.php +++ b/tests/Feature/Api/TaskControllerTest.php @@ -254,10 +254,10 @@ public function testShowTaskIncludesNewProperty() $user = User::factory()->create(); $this->actingAs($user); - $comments = "This is a comment"; + $comments = 'This is a comment'; $task = ProcessRequestToken::factory()->create([ - "comments" => $comments + 'comments' => $comments, ]); $this->assertEquals($comments, $task->comments); diff --git a/tests/unit/ProcessMaker/Models/ProcessTest.php b/tests/unit/ProcessMaker/Models/ProcessTest.php index 337d42bfec..f9a5a77bb3 100644 --- a/tests/unit/ProcessMaker/Models/ProcessTest.php +++ b/tests/unit/ProcessMaker/Models/ProcessTest.php @@ -42,12 +42,12 @@ public function testGetAssignableUsersByAssignmentTypeWithManagerOnlyRules() { $manager1 = User::factory()->create(['status' => 'ACTIVE']); $manager2 = User::factory()->create(['status' => 'ACTIVE']); - + // Create process with multiple managers (array) $process = Process::factory()->create([ - 'properties' => ['manager_id' => [$manager1->id, $manager2->id]] + 'properties' => ['manager_id' => [$manager1->id, $manager2->id]], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -57,13 +57,13 @@ public function testGetAssignableUsersByAssignmentTypeWithManagerOnlyRules() // Mock getAssignmentRule to return different manager-only rules $managerOnlyRules = ['previous_task_assignee', 'requester', 'process_manager']; - + foreach ($managerOnlyRules as $rule) { $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn($rule); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should return both managers $this->assertIsArray($result); $this->assertCount(2, $result); @@ -78,12 +78,12 @@ public function testGetAssignableUsersByAssignmentTypeWithManagerOnlyRules() public function testGetAssignableUsersByAssignmentTypeWithSingleManager() { $manager = User::factory()->create(['status' => 'ACTIVE']); - + // Create process with single manager (not array) $process = Process::factory()->create([ - 'properties' => ['manager_id' => $manager->id] + 'properties' => ['manager_id' => $manager->id], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -92,9 +92,9 @@ public function testGetAssignableUsersByAssignmentTypeWithSingleManager() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('process_manager'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + $this->assertIsArray($result); $this->assertCount(1, $result); $this->assertContains($manager->id, $result); @@ -108,14 +108,14 @@ public function testGetAssignableUsersByAssignmentTypeWithGroupBasedRules() $manager = User::factory()->create(['status' => 'ACTIVE']); $assignableUser1 = User::factory()->create(['status' => 'ACTIVE']); $assignableUser2 = User::factory()->create(['status' => 'ACTIVE']); - + $process = Process::factory()->create([ - 'properties' => ['manager_id' => $manager->id] + 'properties' => ['manager_id' => $manager->id], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $elementId = 'test_element_' . uniqid(); - + // Create task assignments ProcessTaskAssignment::factory()->create([ 'process_id' => $process->id, @@ -123,14 +123,14 @@ public function testGetAssignableUsersByAssignmentTypeWithGroupBasedRules() 'assignment_id' => $assignableUser1->id, 'assignment_type' => User::class, ]); - + ProcessTaskAssignment::factory()->create([ 'process_id' => $process->id, 'process_task_id' => $elementId, 'assignment_id' => $assignableUser2->id, 'assignment_type' => User::class, ]); - + $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, 'process_request_id' => $request->id, @@ -138,13 +138,13 @@ public function testGetAssignableUsersByAssignmentTypeWithGroupBasedRules() ]); $groupBasedRules = ['user_group', 'process_variable', 'rule_expression']; - + foreach ($groupBasedRules as $rule) { $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn($rule); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should return assignable users + manager $this->assertIsArray($result); $this->assertCount(3, $result); @@ -160,11 +160,11 @@ public function testGetAssignableUsersByAssignmentTypeWithGroupBasedRules() public function testGetAssignableUsersByAssignmentTypeWithEmptyElementId() { $manager = User::factory()->create(['status' => 'ACTIVE']); - + $process = Process::factory()->create([ - 'properties' => ['manager_id' => $manager->id] + 'properties' => ['manager_id' => $manager->id], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -174,9 +174,9 @@ public function testGetAssignableUsersByAssignmentTypeWithEmptyElementId() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('user_group'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should only return manager when element_id is empty $this->assertIsArray($result); $this->assertCount(1, $result); @@ -189,9 +189,9 @@ public function testGetAssignableUsersByAssignmentTypeWithEmptyElementId() public function testGetAssignableUsersByAssignmentTypeWithNullManager() { $process = Process::factory()->create([ - 'properties' => ['manager_id' => null] + 'properties' => ['manager_id' => null], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -200,9 +200,9 @@ public function testGetAssignableUsersByAssignmentTypeWithNullManager() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('process_manager'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should return empty array when manager_id is null $this->assertIsArray($result); $this->assertEmpty($result); @@ -214,11 +214,11 @@ public function testGetAssignableUsersByAssignmentTypeWithNullManager() public function testGetAssignableUsersByAssignmentTypeWithUnknownRule() { $manager = User::factory()->create(['status' => 'ACTIVE']); - + $process = Process::factory()->create([ - 'properties' => ['manager_id' => $manager->id] + 'properties' => ['manager_id' => $manager->id], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -227,9 +227,9 @@ public function testGetAssignableUsersByAssignmentTypeWithUnknownRule() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('unknown_rule'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should return empty array for unknown rules $this->assertIsArray($result); $this->assertEmpty($result); @@ -242,13 +242,13 @@ public function testGetAssignableUsersByAssignmentTypeHandlesNestedArrays() { $manager1 = User::factory()->create(['status' => 'ACTIVE']); $manager2 = User::factory()->create(['status' => 'ACTIVE']); - + // Simulate nested array scenario (shouldn't happen but test the normalization) // Note: The accessor will normalize this, but we test the normalization logic $process = Process::factory()->create([ - 'properties' => ['manager_id' => [[$manager1->id], [$manager2->id]]] + 'properties' => ['manager_id' => [[$manager1->id], [$manager2->id]]], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, @@ -257,9 +257,9 @@ public function testGetAssignableUsersByAssignmentTypeHandlesNestedArrays() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('process_manager'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should flatten and return both managers $this->assertIsArray($result); $this->assertCount(2, $result); @@ -278,14 +278,14 @@ public function testGetAssignableUsersByAssignmentTypeRemovesDuplicates() { $manager = User::factory()->create(['status' => 'ACTIVE']); $assignableUser = User::factory()->create(['status' => 'ACTIVE']); - + $process = Process::factory()->create([ - 'properties' => ['manager_id' => $manager->id] + 'properties' => ['manager_id' => $manager->id], ]); - + $request = ProcessRequest::factory()->create(['process_id' => $process->id]); $elementId = 'test_element_' . uniqid(); - + // Create task assignment with manager as assignable user (duplicate scenario) ProcessTaskAssignment::factory()->create([ 'process_id' => $process->id, @@ -293,14 +293,14 @@ public function testGetAssignableUsersByAssignmentTypeRemovesDuplicates() 'assignment_id' => $manager->id, // Same as manager 'assignment_type' => User::class, ]); - + ProcessTaskAssignment::factory()->create([ 'process_id' => $process->id, 'process_task_id' => $elementId, 'assignment_id' => $assignableUser->id, 'assignment_type' => User::class, ]); - + $token = ProcessRequestToken::factory()->create([ 'process_id' => $process->id, 'process_request_id' => $request->id, @@ -309,9 +309,9 @@ public function testGetAssignableUsersByAssignmentTypeRemovesDuplicates() $tokenMock = Mockery::mock($token)->makePartial(); $tokenMock->shouldReceive('getAssignmentRule')->andReturn('user_group'); - + $result = $process->getAssignableUsersByAssignmentType($tokenMock); - + // Should return unique values (manager should appear only once) $this->assertIsArray($result); $this->assertCount(2, $result); // manager + assignableUser (no duplicates) From 72d3190d408eeecd0538d6f5fa745f8ac5019ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:34:13 -0300 Subject: [PATCH 03/14] Add microserviceHost to layout --- resources/views/layouts/layout.blade.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/views/layouts/layout.blade.php b/resources/views/layouts/layout.blade.php index 9843813b0f..253d4343c4 100644 --- a/resources/views/layouts/layout.blade.php +++ b/resources/views/layouts/layout.blade.php @@ -141,7 +141,8 @@ class="main flex-grow-1 h-100 window.ProcessMaker.packages = @json(\App::make(ProcessMaker\Managers\PackageManager::class)->listPackages()); window.ProcessMaker.ai = { ragCollections: @json(config('ai.rag_collections.enabled')), - genieClientTimeout: @json(config('ai.genie_client.timeout')) + genieClientTimeout: @json(config('ai.genie_client.timeout')), + microserviceHost: @json(config('app.ai_microservice_host')) }; From a49554193a28c91cba7870cddbb49acb3838233b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:18:19 -0300 Subject: [PATCH 04/14] Move logs logic from email start event package. Add base logs ui for Agents --- .../Http/Controllers/Admin/LogsController.php | 20 ++ .../Http/Middleware/GenerateMenus.php | 8 + .../components/Logs/BaseTable/BaseTable.vue | 61 ++++++ .../logs/components/Logs/BaseTable/index.js | 3 + .../components/Logs/HeaderBar/HeaderBar.vue | 122 ++++++++++++ .../logs/components/Logs/HeaderBar/index.js | 3 + .../Logs/LogContainer/LogContainer.vue | 104 +++++++++++ .../components/Logs/LogContainer/index.js | 3 + .../components/Logs/LogTable/LogTable.vue | 176 ++++++++++++++++++ .../logs/components/Logs/LogTable/index.js | 3 + .../components/Logs/Pagination/Pagination.vue | 89 +++++++++ .../logs/components/Logs/Pagination/index.js | 3 + .../logs/components/Logs/Sidebar/Sidebar.vue | 173 +++++++++++++++++ .../logs/components/Logs/Sidebar/index.js | 3 + .../js/admin/logs/components/Logs/routes.js | 41 ++++ resources/js/admin/logs/index.js | 26 +++ resources/js/admin/logs/utils/date.js | 74 ++++++++ resources/views/admin/logs/index.blade.php | 28 +++ routes/web.php | 7 + webpack.mix.js | 1 + 20 files changed, 948 insertions(+) create mode 100644 ProcessMaker/Http/Controllers/Admin/LogsController.php create mode 100644 resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue create mode 100644 resources/js/admin/logs/components/Logs/BaseTable/index.js create mode 100644 resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue create mode 100644 resources/js/admin/logs/components/Logs/HeaderBar/index.js create mode 100644 resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue create mode 100644 resources/js/admin/logs/components/Logs/LogContainer/index.js create mode 100644 resources/js/admin/logs/components/Logs/LogTable/LogTable.vue create mode 100644 resources/js/admin/logs/components/Logs/LogTable/index.js create mode 100644 resources/js/admin/logs/components/Logs/Pagination/Pagination.vue create mode 100644 resources/js/admin/logs/components/Logs/Pagination/index.js create mode 100644 resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue create mode 100644 resources/js/admin/logs/components/Logs/Sidebar/index.js create mode 100644 resources/js/admin/logs/components/Logs/routes.js create mode 100644 resources/js/admin/logs/index.js create mode 100644 resources/js/admin/logs/utils/date.js create mode 100644 resources/views/admin/logs/index.blade.php diff --git a/ProcessMaker/Http/Controllers/Admin/LogsController.php b/ProcessMaker/Http/Controllers/Admin/LogsController.php new file mode 100644 index 0000000000..37ef170c8c --- /dev/null +++ b/ProcessMaker/Http/Controllers/Admin/LogsController.php @@ -0,0 +1,20 @@ + "data:image/svg+xml;base64,{$devlinkIcon}", ]); } + if (\Auth::user()->canAny('view-settings|edit-settings') && + (hasPackage('package-email-start-event') || hasPackage('package-ai'))) { + $submenu->add(__('Logs'), [ + 'route' => 'admin.logs', + 'icon' => 'fa-bars', + 'id' => 'admin-logs', + ]); + } }); Menu::make('sidebar_task', function ($menu) { $submenu = $menu->add(__('Tasks')); diff --git a/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue b/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue new file mode 100644 index 0000000000..120ae42de4 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue @@ -0,0 +1,61 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/BaseTable/index.js b/resources/js/admin/logs/components/Logs/BaseTable/index.js new file mode 100644 index 0000000000..f8430fb194 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/BaseTable/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as BaseTable } from './BaseTable.vue'; + diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue new file mode 100644 index 0000000000..d33b721828 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue @@ -0,0 +1,122 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/index.js b/resources/js/admin/logs/components/Logs/HeaderBar/index.js new file mode 100644 index 0000000000..c2176bf01c --- /dev/null +++ b/resources/js/admin/logs/components/Logs/HeaderBar/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as HeaderBar } from './HeaderBar.vue'; + diff --git a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue new file mode 100644 index 0000000000..1fd434353c --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue @@ -0,0 +1,104 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/LogContainer/index.js b/resources/js/admin/logs/components/Logs/LogContainer/index.js new file mode 100644 index 0000000000..d0ad929b7e --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogContainer/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as LogContainer } from './LogContainer.vue'; + diff --git a/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue b/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue new file mode 100644 index 0000000000..dc688bbbd7 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue @@ -0,0 +1,176 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/LogTable/index.js b/resources/js/admin/logs/components/Logs/LogTable/index.js new file mode 100644 index 0000000000..d07b1072e7 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogTable/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as LogTable } from './LogTable.vue'; + diff --git a/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue b/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue new file mode 100644 index 0000000000..33b47377b4 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/resources/js/admin/logs/components/Logs/Pagination/index.js b/resources/js/admin/logs/components/Logs/Pagination/index.js new file mode 100644 index 0000000000..d2d0ae6aa6 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Pagination/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as Pagination } from './Pagination.vue'; + diff --git a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue new file mode 100644 index 0000000000..b47e9561f2 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue @@ -0,0 +1,173 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/Sidebar/index.js b/resources/js/admin/logs/components/Logs/Sidebar/index.js new file mode 100644 index 0000000000..a4f8b5ce94 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Sidebar/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as Sidebar } from './Sidebar.vue'; + diff --git a/resources/js/admin/logs/components/Logs/routes.js b/resources/js/admin/logs/components/Logs/routes.js new file mode 100644 index 0000000000..cee148dcc5 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/routes.js @@ -0,0 +1,41 @@ +import { LogTable } from './LogTable'; + +export default {}; + +export const routes = [ + { + name: 'logs.index', + path: '/', + redirect: '/email/errors', + }, + // Email logs routes + { + name: 'logs.email', + path: '/email/:logType', + component: LogTable, + props(route) { + return { + category: 'email', + logType: route.params.logType, + }; + }, + }, + // FlowGenie Agents logs routes + { + name: 'logs.agents.redirect', + path: '/agents', + redirect: '/agents/design', + }, + { + name: 'logs.agents', + path: '/agents/:logType', + component: LogTable, + props(route) { + return { + category: 'agents', + logType: route.params.logType, + }; + }, + }, +]; + diff --git a/resources/js/admin/logs/index.js b/resources/js/admin/logs/index.js new file mode 100644 index 0000000000..bdf9dc220b --- /dev/null +++ b/resources/js/admin/logs/index.js @@ -0,0 +1,26 @@ +import { LogContainer } from './components/Logs/LogContainer'; +import { routes } from './components/Logs/routes'; + +// eslint-disable-next-line no-undef +Vue.use(VueRouter); + +// eslint-disable-next-line no-undef +const router = new VueRouter({ + mode: 'history', + base: '/admin/logs', + routes, +}); + +window.Vue.component('admin-logs', LogContainer); + +document.addEventListener('DOMContentLoaded', () => { + new window.Vue({ + el: '#admin-logs-main', + router, + components: { + LogContainer, + }, + render: (h) => h(LogContainer), + }); +}); + diff --git a/resources/js/admin/logs/utils/date.js b/resources/js/admin/logs/utils/date.js new file mode 100644 index 0000000000..491fa1a8d8 --- /dev/null +++ b/resources/js/admin/logs/utils/date.js @@ -0,0 +1,74 @@ +/* eslint-disable import/prefer-default-export */ +import { DateTime } from 'luxon'; + +/** + * Convert PHP/moment date format to Luxon format + * @param {string} format - PHP/moment format string + * @returns {string} - Luxon format string + */ +const convertToLuxonFormat = (format) => { + // Common conversions from PHP/moment to Luxon + const replacements = { + YYYY: 'yyyy', + YY: 'yy', + MM: 'LL', + M: 'L', + DD: 'dd', + D: 'd', + HH: 'HH', + hh: 'hh', + H: 'H', + h: 'h', + mm: 'mm', + m: 'm', + ss: 'ss', + s: 's', + A: 'a', + a: 'a', + }; + + let luxonFormat = format; + Object.entries(replacements).forEach(([from, to]) => { + luxonFormat = luxonFormat.replace(new RegExp(from, 'g'), to); + }); + + return luxonFormat; +}; + +/** + * Format date to user's date format + * @param {string} value - The date to format + * @returns {string} - The formatted date + */ +export const dateFormatter = (value) => { + let datetimeConfig = 'dd/LL/yyyy hh:mm'; + let timezoneConfig = 'UTC'; + + if ( + typeof ProcessMaker !== 'undefined' + && ProcessMaker.user + && ProcessMaker.user.datetime_format + ) { + timezoneConfig = ProcessMaker.user.timezone; + datetimeConfig = convertToLuxonFormat(ProcessMaker.user.datetime_format); + } + + if (value) { + const date = DateTime.fromISO(value, { zone: 'utc' }).setZone(timezoneConfig); + + if (date.isValid) { + return date.toFormat(datetimeConfig); + } + + // Try parsing as SQL format + const sqlDate = DateTime.fromSQL(value, { zone: 'utc' }).setZone(timezoneConfig); + if (sqlDate.isValid) { + return sqlDate.toFormat(datetimeConfig); + } + + return value; + } + + return '-'; +}; + diff --git a/resources/views/admin/logs/index.blade.php b/resources/views/admin/logs/index.blade.php new file mode 100644 index 0000000000..9a7ca90a61 --- /dev/null +++ b/resources/views/admin/logs/index.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.layout') + +@section('title') + {{__('Logs')}} +@endsection + +@section('sidebar') + @include('layouts.sidebar', ['sidebar'=> Menu::get('sidebar_admin')]) +@endsection + +@section('breadcrumbs') + @include('shared.breadcrumbs', ['routes' => [ + __('Admin') => route('admin.index'), + __('Logs') => null, + ]]) +@endsection + +@section('content') +
+@endsection + +@section('js') + + +@endsection + diff --git a/routes/web.php b/routes/web.php index 30f1cdaa35..589cb3995f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -8,6 +8,7 @@ use ProcessMaker\Http\Controllers\Admin\DevLinkController; use ProcessMaker\Http\Controllers\Admin\GroupController; use ProcessMaker\Http\Controllers\Admin\LdapLogsController; +use ProcessMaker\Http\Controllers\Admin\LogsController; use ProcessMaker\Http\Controllers\Admin\QueuesController; use ProcessMaker\Http\Controllers\Admin\ScriptExecutorController; use ProcessMaker\Http\Controllers\Admin\SettingsController; @@ -81,6 +82,12 @@ // temporary, should be removed Route::get('security-logs/download/all', [ProcessMaker\Http\Controllers\Api\SecurityLogController::class, 'downloadForAllUsers'])->middleware('can:view-security-logs'); Route::get('security-logs/download/{user}', [ProcessMaker\Http\Controllers\Api\SecurityLogController::class, 'downloadForUser'])->middleware('can:view-security-logs'); + + // Logs - available when package-email-start-event or package-ai is installed + if (hasPackage('package-email-start-event') || hasPackage('package-ai')) { + Route::get('logs', [LogsController::class, 'index'])->name('admin.logs')->middleware('can:view-settings'); + Route::get('logs/{any}', [LogsController::class, 'index'])->name('admin.logs-any')->middleware('can:view-settings')->where('any', '.*'); + } }); Route::get('admin', [AdminController::class, 'index'])->name('admin.index'); diff --git a/webpack.mix.js b/webpack.mix.js index 01253ace95..c93f125c10 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -104,6 +104,7 @@ mix .js("resources/js/admin/cssOverride/edit.js", "public/js/admin/cssOverride/edit.js") .js("resources/js/admin/script-executors/index.js", "public/js/admin/script-executors/index.js") .js("resources/js/admin/tenant-queues/index.js", "public/js/admin/tenant-queues/index.js") + .js("resources/js/admin/logs/index.js", "public/js/admin/logs/index.js") .js("resources/js/processes/index.js", "public/js/processes") .js("resources/js/processes/edit.js", "public/js/processes") From 7be12fb221750302c5e203b5b1580a1b36f80dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:27:17 -0300 Subject: [PATCH 05/14] Add translation strings --- .../logs/components/Logs/HeaderBar/HeaderBar.vue | 12 ++++++------ .../components/Logs/LogContainer/LogContainer.vue | 2 +- resources/lang/en.json | 6 ++++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue index d33b721828..4085475684 100644 --- a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue +++ b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue @@ -10,21 +10,21 @@ class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base" :class="tabClasses('errors')" > - Error Logs + {{ $t('Error Logs') }} - Matched Logs + {{ $t('Matched Logs') }} - Total Logs + {{ $t('Total Logs') }} @@ -38,14 +38,14 @@ class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base" :class="tabClasses('design')" > - Design Mode Logs + {{ $t('Design Mode Logs') }} - Execution Logs + {{ $t('Execution Logs') }} @@ -68,7 +68,7 @@ tw-ring-0 placeholder:tw-text-zinc-400 " - placeholder="Search here" + :placeholder="$t('Search here')" :value="value" @input="onInput" @keypress="onKeypress" diff --git a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue index 1fd434353c..3aa08d07ca 100644 --- a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue +++ b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue @@ -12,7 +12,7 @@

- {{ title }} + {{ $t(title) }}

Date: Wed, 7 Jan 2026 12:50:14 -0300 Subject: [PATCH 06/14] Fix routes issue --- routes/web.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/routes/web.php b/routes/web.php index 589cb3995f..11287a4a62 100644 --- a/routes/web.php +++ b/routes/web.php @@ -86,6 +86,12 @@ // Logs - available when package-email-start-event or package-ai is installed if (hasPackage('package-email-start-event') || hasPackage('package-ai')) { Route::get('logs', [LogsController::class, 'index'])->name('admin.logs')->middleware('can:view-settings'); + // Export route must be before the wildcard route + if (hasPackage('package-email-start-event')) { + Route::get('logs/export/csv', [ProcessMaker\Package\PackageEmailStartEvent\Http\Controllers\EmailListenerLogController::class, 'exportToCsv']) + ->name('admin.logs.export.csv') + ->middleware('can:view-settings'); + } Route::get('logs/{any}', [LogsController::class, 'index'])->name('admin.logs-any')->middleware('can:view-settings')->where('any', '.*'); } }); From d43dc69dc80a1c7a44fd61ee1c701e03ef652a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:28:59 -0300 Subject: [PATCH 07/14] Add validation to check if packages are installed --- .../logs/components/Logs/Sidebar/Sidebar.vue | 65 ++++++++++----- .../js/admin/logs/components/Logs/routes.js | 79 +++++++++++++++---- 2 files changed, 109 insertions(+), 35 deletions(-) diff --git a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue index b47e9561f2..9b93c63759 100644 --- a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue +++ b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue @@ -5,8 +5,11 @@ Logs