diff --git a/.gitignore b/.gitignore index 7a47d39bab..5c4cbe56fd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ storage/ssl storage/api/* storage/data-sources/logs/* storage/decision-tables/* +storage/saved_search_advanced_configuration/* npm.sh laravel-echo-server.lock public/.htaccess @@ -51,4 +52,7 @@ devhub/pm-font/dist test-db-snapshot.db snapshot_*.db storage/transitions -.envrc \ No newline at end of file +.envrc +**/caddy +frankenphp +frankenphp-worker.php diff --git a/ProcessMaker/Assets/DataSourcesInProcess.php b/ProcessMaker/Assets/DataSourcesInProcess.php new file mode 100644 index 0000000000..0a44452214 --- /dev/null +++ b/ProcessMaker/Assets/DataSourcesInProcess.php @@ -0,0 +1,72 @@ +getDefinitions()); + $xpath->registerNamespace('pm', WorkflowServiceProvider::PROCESS_MAKER_NS); + + // Find all nodes with pm:config + $nodes = $xpath->query("//*[@pm:config!='']"); + foreach ($nodes as $node) { + $configString = $node->getAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'config'); + $config = json_decode($configString, true); + if (isset($config['dataSource']) && is_numeric($config['dataSource'])) { + $dataSources[] = [$this->type, (int) $config['dataSource']]; + } + } + + return $dataSources; + } + + /** + * Update references used in an imported process + * + * @param Process $process + * @param array $references + * + * @return void + */ + public function updateReferences(Process $process, array $references = []) + { + $definitions = $process->getDefinitions(); + $xpath = new DOMXPath($definitions); + $xpath->registerNamespace('pm', WorkflowServiceProvider::PROCESS_MAKER_NS); + + $nodes = $xpath->query("//*[@pm:config!='']"); + foreach ($nodes as $node) { + $configString = $node->getAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'config'); + $config = json_decode($configString, true); + if (isset($config['dataSource']) && is_numeric($config['dataSource'])) { + $oldRef = $config['dataSource']; + if (isset($references[$this->type][$oldRef])) { + $newRef = $references[$this->type][$oldRef]->getKey(); + $config['dataSource'] = $newRef; + $node->setAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'config', json_encode($config)); + } + } + } + $process->bpmn = $definitions->saveXML(); + $process->save(); + } +} diff --git a/ProcessMaker/Assets/DataSourcesInScreen.php b/ProcessMaker/Assets/DataSourcesInScreen.php new file mode 100644 index 0000000000..4a7494d74f --- /dev/null +++ b/ProcessMaker/Assets/DataSourcesInScreen.php @@ -0,0 +1,131 @@ +watchers; + if (is_array($watchers)) { + $this->findInArray($watchers, function ($item) use (&$dataSources) { + if (is_array($item)) { + $scriptId = (string) ($item['script_id'] ?? ''); + $id = (string) ($item['script']['id'] ?? ''); + if (str_starts_with($scriptId, 'data_source-') || str_starts_with($id, 'data_source-')) { + $numericId = str_replace('data_source-', '', str_starts_with($scriptId, 'data_source-') ? $scriptId : $id); + if (is_numeric($numericId)) { + $dataSources[] = [$this->type, (int) $numericId]; + } + } + } + }); + } + + $config = $screen->versionFor(null)->config; + if (is_array($config)) { + $this->findInArray($config, function ($item) use (&$dataSources) { + if (is_array($item) && isset($item['component']) && $item['component'] === 'FormSelectList' && !empty($item['config']['options']['selectedDataSource'])) { + $dataSources[] = [$this->type, (int) $item['config']['options']['selectedDataSource']]; + } + }); + } + + return $dataSources; + } + + /** + * Update references used in an imported screen + * + * @param Screen $screen + * @param array $references + * @param ExportManager $exportManager + * + * @return void + */ + public function updateReferences(Screen $screen, array $references, ExportManager $exportManager) + { + $watches = $screen->watchers; + if (is_array($watches)) { + foreach ($watches as &$watcher) { + $id = (string) ($watcher['script']['id'] ?? ''); + if (str_starts_with($id, 'data_source-')) { + $oldRef = str_replace('data_source-', '', $id); + if (isset($references[$this->type][$oldRef])) { + $newRef = $references[$this->type][$oldRef]->getKey(); + } else { + $newRef = null; + $exportManager->addLogMessage( + 'DataSourcesInScreen:references', + __( + 'Imported file does not contain the data source #:dataSource assigned to a watcher', + ['dataSource' => $oldRef] + ), + false, + __("Missing watcher's data source") + ); + } + if ($newRef) { + $watcher['script_id'] = $newRef; + $watcher['script']['id'] = "data_source-$newRef"; + $watcher['script']['title'] = $references[$this->type][$oldRef]->name; + } + } + } + } + $screen->watchers = $watches; + + $config = $screen->config; + if (is_array($config)) { + $this->findInArray($config, function ($item, $keyDotNotation) use ($references, &$config) { + if (is_array($item) && isset($item['component']) && $item['component'] === 'FormSelectList' && !empty($item['config']['options']['selectedDataSource'])) { + $oldRef = $item['config']['options']['selectedDataSource']; + if (isset($references[$this->type][$oldRef])) { + $newRef = $references[$this->type][$oldRef]->getKey(); + Arr::set($config, "$keyDotNotation.config.options.selectedDataSource", $newRef); + } + } + }); + $screen->config = $config; + } + + $screen->save(); + } + + /** + * Find recursively in an array + * + * @param array $array + * @param callable $callback + * @param array $path + * + * @return void + */ + private function findInArray(array $array, callable $callback, array $path = []) + { + call_user_func($callback, $array, implode('.', $path)); + foreach ($array as $key => $item) { + if (is_array($item)) { + $this->findInArray($item, $callback, array_merge($path, [$key])); + } else { + call_user_func($callback, $item, implode('.', array_merge($path, [$key]))); + } + } + } +} diff --git a/ProcessMaker/Assets/ScriptsInProcess.php b/ProcessMaker/Assets/ScriptsInProcess.php index 5b36ff0dfa..0886099bde 100644 --- a/ProcessMaker/Assets/ScriptsInProcess.php +++ b/ProcessMaker/Assets/ScriptsInProcess.php @@ -30,7 +30,10 @@ public function referencesToExport(Process $process, array $scripts = []) // Used in scriptRef $nodes = $xpath->query("//*[@pm:scriptRef!='']"); foreach ($nodes as $node) { - $scripts[] = [Script::class, $node->getAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'scriptRef')]; + $scriptId = $node->getAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'scriptRef'); + if (!str_starts_with((string) $scriptId, 'data_source-')) { + $scripts[] = [Script::class, $scriptId]; + } } return $scripts; @@ -54,6 +57,9 @@ public function updateReferences(Process $process, array $references = []) $nodes = $xpath->query("//*[@pm:scriptRef!='']"); foreach ($nodes as $node) { $oldRef = $node->getAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'scriptRef'); + if (str_starts_with((string) $oldRef, 'data_source-')) { + continue; + } $newRef = $references[Script::class][$oldRef]->getKey(); $node->setAttributeNS(WorkflowServiceProvider::PROCESS_MAKER_NS, 'scriptRef', $newRef); } diff --git a/ProcessMaker/Assets/ScriptsInScreen.php b/ProcessMaker/Assets/ScriptsInScreen.php index 9cf41025dd..1d6be01ac9 100644 --- a/ProcessMaker/Assets/ScriptsInScreen.php +++ b/ProcessMaker/Assets/ScriptsInScreen.php @@ -26,7 +26,11 @@ public function referencesToExport(Screen $screen, array $scripts = []) if (is_array($config)) { $this->findInArray($config, function ($item) use (&$scripts) { if (is_array($item) && !empty($item['script_id'])) { - $scripts[] = [Script::class, $item['script_id']]; + $scriptId = (string) $item['script_id']; + $id = (string) ($item['script']['id'] ?? ''); + if (!str_starts_with($scriptId, 'data_source-') && !str_starts_with($id, 'data_source-')) { + $scripts[] = [Script::class, $item['script_id']]; + } } }); } @@ -47,11 +51,12 @@ public function updateReferences(Screen $screen, array $references, ExportManage $watches = $screen->watchers; if (is_array($watches)) { foreach ($watches as &$watcher) { - $refParts = explode('-', $watcher['script_id']); - if ($refParts[0] === 'data_source') { + $scriptId = (string) ($watcher['script_id'] ?? ''); + $id = (string) ($watcher['script']['id'] ?? ''); + if (str_starts_with($scriptId, 'data_source-') || str_starts_with($id, 'data_source-')) { continue; } - $oldRef = $refParts[1]; + $oldRef = $scriptId; if (isset($references[Script::class][$oldRef])) { $newRef = $references[Script::class][$oldRef]->getKey(); } else { @@ -82,17 +87,18 @@ public function updateReferences(Screen $screen, array $references, ExportManage * * @param array $array * @param callable $callback + * @param array $path * * @return void */ - private function findInArray(array $array, callable $callback) + private function findInArray(array $array, callable $callback, array $path = []) { - call_user_func($callback, $array); - foreach ($array as $item) { + call_user_func($callback, $array, implode('.', $path)); + foreach ($array as $key => $item) { if (is_array($item)) { - $this->findInArray($item, $callback); + $this->findInArray($item, $callback, array_merge($path, [$key])); } else { - call_user_func($callback, $item); + call_user_func($callback, $item, implode('.', array_merge($path, [$key]))); } } } diff --git a/ProcessMaker/Console/Commands/SyncGuidedTemplates.php b/ProcessMaker/Console/Commands/SyncGuidedTemplates.php deleted file mode 100644 index 2849a9be8a..0000000000 --- a/ProcessMaker/Console/Commands/SyncGuidedTemplates.php +++ /dev/null @@ -1,41 +0,0 @@ -option('queue')) { - $randomDelay = random_int(10, 120); - Job::dispatch()->delay(now()->addMinutes($randomDelay)); - - return 0; - } - - Job::dispatchSync(); - - return 0; - } -} diff --git a/ProcessMaker/Console/Commands/UpdateAnonymousUserTimezone.php b/ProcessMaker/Console/Commands/UpdateAnonymousUserTimezone.php new file mode 100644 index 0000000000..ae83216480 --- /dev/null +++ b/ProcessMaker/Console/Commands/UpdateAnonymousUserTimezone.php @@ -0,0 +1,55 @@ +option('timezone') ?: config('app.anonymous_user_timezone', 'UTC'); + + $user = User::where('username', AnonymousUser::ANONYMOUS_USERNAME)->first(); + + if (!$user) { + $this->error('Anonymous user not found.'); + + return self::FAILURE; + } + + if ($user->timezone === $timezone) { + $this->info("Anonymous user timezone is already set to [{$timezone}]."); + + return self::SUCCESS; + } + + $previousTimezone = $user->timezone; + $user->timezone = $timezone; + $user->save(); + + $this->info("Anonymous user timezone updated from [{$previousTimezone}] to [{$timezone}]."); + + return self::SUCCESS; + } +} diff --git a/ProcessMaker/Console/Kernel.php b/ProcessMaker/Console/Kernel.php index 03fc408949..df4ce44dd8 100644 --- a/ProcessMaker/Console/Kernel.php +++ b/ProcessMaker/Console/Kernel.php @@ -37,9 +37,6 @@ protected function schedule(Schedule $schedule) $schedule->command('processmaker:sync-default-templates --queue') ->daily(); - $schedule->command('processmaker:sync-guided-templates --queue') - ->daily(); - $schedule->command('processmaker:sync-screen-templates --queue') ->daily(); diff --git a/ProcessMaker/Events/ImportLog.php b/ProcessMaker/Events/ImportLog.php index 96cbfe6aec..9168795f9d 100644 --- a/ProcessMaker/Events/ImportLog.php +++ b/ProcessMaker/Events/ImportLog.php @@ -16,7 +16,8 @@ public function __construct( public $userId, public $type, public $message, - public $additionalParams = [] + public $additionalParams = [], + public $operationId = null, ) { } diff --git a/ProcessMaker/Exception/BundleIntegrityException.php b/ProcessMaker/Exception/BundleIntegrityException.php new file mode 100644 index 0000000000..f2d8f853cd --- /dev/null +++ b/ProcessMaker/Exception/BundleIntegrityException.php @@ -0,0 +1,45 @@ +invalidAssets = $invalidAssets + ->map(fn (BundleAsset $asset) => $asset->integrityDetails()) + ->values() + ->all(); + + parent::__construct(__( + 'The bundle :bundle contains unavailable assets and cannot be exported.', + ['bundle' => $bundle->name] + )); + } + + public function invalidAssets(): array + { + return $this->invalidAssets; + } + + public function render(): JsonResponse + { + return response()->json([ + 'error' => [ + 'code' => 422, + 'message' => $this->getMessage(), + ], + 'errors' => [ + 'assets' => $this->invalidAssets, + ], + ], 422); + } +} diff --git a/ProcessMaker/Exception/DevLinkRemoteBundleException.php b/ProcessMaker/Exception/DevLinkRemoteBundleException.php new file mode 100644 index 0000000000..3d97f0c31c --- /dev/null +++ b/ProcessMaker/Exception/DevLinkRemoteBundleException.php @@ -0,0 +1,25 @@ +map(function (array $asset) { + $type = class_basename($asset['asset_type'] ?? 'Asset'); + $id = $asset['asset_id'] ?? '?'; + $bundleAssetId = $asset['bundle_asset_id'] ?? '?'; + + return "$type #$id (bundle asset #$bundleAssetId)"; + })->implode(', '); + + parent::__construct(__( + 'The remote bundle contains unavailable assets: :assets. Repair the bundle on the source instance and try again.', + ['assets' => $assets] + ), 0, $previous); + } +} diff --git a/ProcessMaker/Http/Controllers/Api/DevLinkController.php b/ProcessMaker/Http/Controllers/Api/DevLinkController.php index 29825f4d38..e1fd7f3b52 100644 --- a/ProcessMaker/Http/Controllers/Api/DevLinkController.php +++ b/ProcessMaker/Http/Controllers/Api/DevLinkController.php @@ -7,6 +7,7 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; use ProcessMaker\Events\CustomizeUiUpdated; use ProcessMaker\Exception\ValidationException; @@ -105,10 +106,20 @@ public function destroy(DevLink $devLink) public function ping(DevLink $devLink) { try { - return $devLink->client()->get(route('api.devlink.pong', [], false)); - } catch (\Exception $e) { - return response()->json(['error' => 'DevLink connection error'], $e->getCode()); + $response = $devLink->client()->get(route('api.devlink.pong', [], false)); + } catch (RequestException $e) { + $status = $e->response->status(); + + return response()->json([ + 'status' => in_array($status, [401, 403], true) ? 'authorization_required' : 'error', + ]); + } catch (\Throwable $e) { + return response()->json(['status' => 'error']); } + + return response()->json([ + 'status' => $response->json('status') === 'ok' ? 'ok' : 'error', + ]); } public function pong() @@ -225,6 +236,7 @@ public function deleteBundle(Bundle $bundle) public function installRemoteBundle(Request $request, DevLink $devLink, int $remoteBundleId) { $updateType = $request->input('updateType', DevLinkInstall::MODE_UPDATE); + $operationId = $this->operationId($request); DevLinkInstall::dispatch( $request->user()->id, $devLink->id, @@ -232,6 +244,7 @@ public function installRemoteBundle(Request $request, DevLink $devLink, int $rem $remoteBundleId, $updateType, DevLinkInstall::TYPE_INSTALL_BUNDLE, + $operationId, ); return [ @@ -242,6 +255,7 @@ public function installRemoteBundle(Request $request, DevLink $devLink, int $rem public function reinstallBundle(Request $request, Bundle $bundle) { $updateType = $request->input('updateType', DevLinkInstall::MODE_UPDATE); + $operationId = $this->operationId($request); DevLinkInstall::dispatch( $request->user()->id, $bundle->dev_link_id, @@ -249,6 +263,7 @@ public function reinstallBundle(Request $request, Bundle $bundle) $bundle->id, $updateType, DevLinkInstall::TYPE_REINSTALL_BUNDLE, + $operationId, ); return [ @@ -370,6 +385,7 @@ public function removeSharedAsset(int $id) public function installRemoteAsset(Request $request, DevLink $devLink) { $updateType = $request->input('updateType', DevLinkInstall::MODE_UPDATE); + $operationId = $this->operationId($request); DevLinkInstall::dispatch( $request->user()->id, @@ -377,7 +393,8 @@ public function installRemoteAsset(Request $request, DevLink $devLink) $request->input('class'), $request->input('id'), $updateType, - DevLinkInstall::TYPE_IMPORT_ASSET + DevLinkInstall::TYPE_IMPORT_ASSET, + $operationId, ); return [ @@ -424,6 +441,15 @@ private function normalizeUrl(string $url): string public function deleteBundleAsset(BundleAsset $bundleAsset) { + if ( + !$bundleAsset->bundle->editable() + && $bundleAsset->integrity_status === BundleAsset::INTEGRITY_VALID + ) { + throw ValidationException::withMessages([ + '*' => __('Only unavailable assets can be removed from an installed bundle.'), + ]); + } + $bundleAsset->delete(); return response()->json(['message' => 'Bundle asset association deleted.'], 200); @@ -502,4 +528,13 @@ public function refreshUi(Request $request) CompileUI::dispatch($request->user()?->id); CustomizeUiUpdated::dispatch([], [], false); } + + private function operationId(Request $request): string + { + $validated = $request->validate([ + 'operation_id' => ['nullable', 'string', 'max:100'], + ]); + + return $validated['operation_id'] ?? (string) Str::uuid(); + } } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php index 15746d08cd..0a4daa0991 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestController.php @@ -792,7 +792,7 @@ public function getRequestToken(Request $httpRequest, ProcessRequest $request) public function screenRequested(Request $httpRequest, ProcessRequest $request) { $query = ProcessRequestToken::query(); - $query->select('id', 'element_id', 'process_id', 'process_request_id', 'data') + $query->select('id', 'element_id', 'process_id', 'process_request_id', 'data', 'token_properties') ->where('process_request_id', $request->id) ->whereNotIn('element_type', ['end_event', 'scriptTask']) ->whereIn('status', ['CLOSED', 'TRIGGERED']) diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/ClipboardController.php b/ProcessMaker/Http/Controllers/Api/V1_1/ClipboardController.php index 96a62cb40a..f7bcfd7b35 100644 --- a/ProcessMaker/Http/Controllers/Api/V1_1/ClipboardController.php +++ b/ProcessMaker/Http/Controllers/Api/V1_1/ClipboardController.php @@ -30,7 +30,12 @@ public function show(int $clipboardId): Resource public function showByUserId(): Resource { $userId = Auth::id(); - $clipboard = Clipboard::where('user_id', $userId)->firstOrFail(); + $clipboard = Clipboard::firstOrCreate( + ['user_id' => $userId], + ['config' => [], 'type' => 'FORM'] + ); + // JsonResource responds with 201 for new models, but this GET should remain 200. + $clipboard->wasRecentlyCreated = false; return new Resource($clipboard); } diff --git a/ProcessMaker/Http/Controllers/Api/WizardTemplateController.php b/ProcessMaker/Http/Controllers/Api/WizardTemplateController.php deleted file mode 100644 index 0e71f2b66b..0000000000 --- a/ProcessMaker/Http/Controllers/Api/WizardTemplateController.php +++ /dev/null @@ -1,42 +0,0 @@ -input('per_page', 10); - $column = $request->input('order_by', 'id'); - $filter = $request->input('filter', ''); - - $direction = $request->input('order_direction', 'asc'); - - $query = WizardTemplate::with('process')->filter($filter) - ->orderBy($column, $direction); - - $data = $query->paginate($perPage); - - return new ApiCollection($data); - } - - public function getHelperProcess($wizardTemplateUuid) - { - $helperProcessID = WizardTemplate::select('helper_process_id')->where('uuid', $wizardTemplateUuid)->value('helper_process_id'); - $start_events = Process::select('start_events')->where('id', $helperProcessID)->value('start_events'); - - return json_encode([ - 'helper_process_id' => $helperProcessID, - 'start_events' => json_encode($start_events), - ]); - } -} diff --git a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php index 596c5821ed..3348a20fd1 100644 --- a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php +++ b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php @@ -31,12 +31,6 @@ class ProcessesCatalogueController extends Controller public function index(Request $request, Process $process = null) { - if ($request->has('guided_templates')) { - return redirect()->route('process.browser.index', [ - 'categoryId' => 'guided_templates', - 'template' => $request->input('template'), - ]); - } $manager = app(ScreenBuilderManager::class); event(new ScreenBuilderStarting($manager, 'DISPLAY')); $launchpad = null; diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php index 213d9374ab..2672a3abf6 100755 --- a/ProcessMaker/Http/Controllers/TaskController.php +++ b/ProcessMaker/Http/Controllers/TaskController.php @@ -27,6 +27,7 @@ use ProcessMaker\Models\TaskDraft; use ProcessMaker\Models\UserResourceView; use ProcessMaker\Nayra\Contracts\Bpmn\ScriptTaskInterface; +use ProcessMaker\Services\SmartExtractConfiguration; use ProcessMaker\Traits\HasControllerAddons; use ProcessMaker\Traits\SearchAutocompleteTrait; use ProcessMaker\Traits\TaskControllerIndexMethods; @@ -43,6 +44,10 @@ class TaskController extends Controller 'overdue' => 'Due', ]; + public function __construct(private readonly SmartExtractConfiguration $smartExtractConfiguration) + { + } + public function index() { $routerPath = Request::route('router'); @@ -190,25 +195,7 @@ public function edit(ProcessRequestToken $task, string $preview = '') 'datetime_format', ]); $userConfiguration = (new UserConfigurationController())->index(); - $hitlEnabled = config('smart-extract.hitl_enabled', false) && $isSmartExtractTask; - - // Build the iframe source - $iframeSrc = null; - if ($hitlEnabled) { - $dashboardUrl = config('smart-extract.dashboard_url'); - $requestData = $task->processRequest->data ?? []; - - $documentToken = $requestData['documentToken'] ?? null; - $fileId = $requestData['fileId'] ?? null; - - if ($documentToken && $fileId && !empty($dashboardUrl)) { - $queryParams = http_build_query([ - 'documentToken' => $documentToken, - 'fileId' => $fileId, - ]); - $iframeSrc = $dashboardUrl . '?' . $queryParams; - } - } + [$hitlEnabled, $iframeSrc] = $this->smartExtractHitlConfiguration($task, $isSmartExtractTask); return view('tasks.edit', [ 'task' => $task, @@ -231,6 +218,30 @@ public function edit(ProcessRequestToken $task, string $preview = '') } } + private function smartExtractHitlConfiguration( + ProcessRequestToken $task, + bool $isSmartExtractTask + ): array { + $hitlEnabled = $this->smartExtractConfiguration->hitlEnabled() && $isSmartExtractTask; + if (!$hitlEnabled) { + return [false, null]; + } + + $dashboardUrl = $this->smartExtractConfiguration->dashboardUrl(); + $requestData = $task->processRequest->data ?? []; + $documentToken = $requestData['documentToken'] ?? null; + $fileId = $requestData['fileId'] ?? null; + + if (!$documentToken || !$fileId || empty($dashboardUrl)) { + return [true, null]; + } + + return [true, $dashboardUrl . '?' . http_build_query([ + 'documentToken' => $documentToken, + 'fileId' => $fileId, + ])]; + } + public function quickFillEdit(ProcessRequestToken $task) { $screenVersion = $task->getScreenVersion(); diff --git a/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php b/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php index 96ade2bded..2a15e84a6d 100644 --- a/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php +++ b/ProcessMaker/Http/Middleware/ServerTimingMiddleware.php @@ -28,6 +28,8 @@ public function handle(Request $request, Closure $next): Response return $next($request); } + ProcessMakerServiceProvider::beginRequestTiming(); + // Start time for controller execution $startController = microtime(true); diff --git a/ProcessMaker/Http/Resources/Task.php b/ProcessMaker/Http/Resources/Task.php index fe19ce85b5..7bf2eae8ff 100644 --- a/ProcessMaker/Http/Resources/Task.php +++ b/ProcessMaker/Http/Resources/Task.php @@ -11,6 +11,7 @@ use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; use ProcessMaker\Models\User; +use ProcessMaker\Services\SmartExtractConfiguration; use ProcessMaker\Traits\TaskResourceIncludes; class Task extends ApiResource @@ -118,7 +119,7 @@ private function addAssignableUsers(&$array, $include) private function mergeHitlCaseNumber(array &$array): void { - if (!config('smart-extract.hitl_enabled')) { + if (!app(SmartExtractConfiguration::class)->hitlEnabled()) { return; } diff --git a/ProcessMaker/ImportExport/Exporters/ScriptExecutorExporter.php b/ProcessMaker/ImportExport/Exporters/ScriptExecutorExporter.php index 0f3e8a1444..7f19142a6f 100644 --- a/ProcessMaker/ImportExport/Exporters/ScriptExecutorExporter.php +++ b/ProcessMaker/ImportExport/Exporters/ScriptExecutorExporter.php @@ -5,6 +5,7 @@ use Illuminate\Support\Facades\Auth; use ProcessMaker\Events\ScriptExecutorUpdated; use ProcessMaker\Jobs\BuildScriptExecutor; +use ProcessMaker\Jobs\MoveScriptExecutorToMicroservice; use ProcessMaker\Models\ScriptExecutor; use ProcessMaker\Models\User; @@ -28,7 +29,11 @@ public function import() : bool case 'copy': case 'new': // afterCommit is needed because we are in a db transaction - BuildScriptExecutor::dispatch($this->model->id, $userId)->afterCommit(); + if (!config('script-runner-microservice.enabled')) { + BuildScriptExecutor::dispatch($this->model->id, $userId)->afterCommit(); + } else { + MoveScriptExecutorToMicroservice::dispatch($this->model->uuid)->afterCommit(); + } break; case 'update': if (!empty($this->model->getChanges())) { @@ -40,7 +45,11 @@ public function import() : bool $user = User::where('is_administrator', 1)->first(); } // afterCommit is needed because we are in a db transaction - BuildScriptExecutor::dispatch($this->model->id, $user->id)->afterCommit(); + if (!config('script-runner-microservice.enabled')) { + BuildScriptExecutor::dispatch($this->model->id, $userId)->afterCommit(); + } else { + MoveScriptExecutorToMicroservice::dispatch($this->model->uuid)->afterCommit(); + } } break; diff --git a/ProcessMaker/ImportExport/Logger.php b/ProcessMaker/ImportExport/Logger.php index f77b7adea4..2764d65eda 100644 --- a/ProcessMaker/ImportExport/Logger.php +++ b/ProcessMaker/ImportExport/Logger.php @@ -14,16 +14,19 @@ class Logger public $userId = null; + public $operationId = null; + private $warnings = []; private int $totalSteps = 1; private int $currentStep = 1; - public function __construct($userId = null) + public function __construct($userId = null, $operationId = null) { $this->pid = getmypid(); $this->userId = $userId; + $this->operationId = $operationId; if ($userId) { Event::listen(MessageLogged::class, function (MessageLogged $e) { @@ -71,7 +74,13 @@ private function dispatch($type, $message, $additionalParams = []) return; } - ImportLog::dispatch($this->userId, $type, substr($message, 0, 1000), $additionalParams); + ImportLog::dispatch( + $this->userId, + $type, + substr($message, 0, 1000), + $additionalParams, + $this->operationId, + ); $this->logToFile($type, $message, $additionalParams); } diff --git a/ProcessMaker/Jobs/DevLinkInstall.php b/ProcessMaker/Jobs/DevLinkInstall.php index c30519e201..ae4339a736 100644 --- a/ProcessMaker/Jobs/DevLinkInstall.php +++ b/ProcessMaker/Jobs/DevLinkInstall.php @@ -9,6 +9,8 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use ProcessMaker\Exception\DevLinkRemoteBundleException; use ProcessMaker\ImportExport\Logger; use ProcessMaker\Jobs\ImportV2; use ProcessMaker\Models\Bundle; @@ -33,6 +35,14 @@ class DevLinkInstall implements ShouldQueue public $maxExceptions = 1; + /** + * Correlates progress events with the DevLink operation that started them. + * + * This remains nullable so jobs queued before this property was introduced + * can still be processed after an application upgrade. + */ + public $operationId = null; + public function __construct( public int $userId, public int $devLinkId, @@ -40,7 +50,9 @@ public function __construct( public int $id, public string $importMode, public string $type, + $operationId = null, ) { + $this->operationId = $operationId; } /** @@ -49,9 +61,9 @@ public function __construct( public function handle(): void { //log - \Log::info('DevLinkInstall job started: ' . $this->devLinkId); + Log::info('DevLinkInstall job started: ' . $this->devLinkId); $devLink = DevLink::findOrFail($this->devLinkId); - $logger = new Logger($this->userId); + $logger = new Logger($this->userId, $this->operationId); $lock = Cache::lock(ImportV2::CACHE_LOCK_KEY, ImportV2::RELEASE_LOCK_AFTER); @@ -82,7 +94,13 @@ public function handle(): void public function failed(Throwable $exception): void { - (new Logger($this->userId))->exception($exception); + $logger = new Logger($this->userId, $this->operationId); + if ($exception instanceof DevLinkRemoteBundleException) { + Log::error($exception->getMessage(), ['exception' => $exception]); + $logger->error($exception->getMessage()); + } else { + $logger->exception($exception); + } // Unlock the job // We can't use $this->lock->release() here because this is run in a new instance diff --git a/ProcessMaker/Jobs/ExportProcess.php b/ProcessMaker/Jobs/ExportProcess.php index f7f03ecf8a..116a6c8ed2 100644 --- a/ProcessMaker/Jobs/ExportProcess.php +++ b/ProcessMaker/Jobs/ExportProcess.php @@ -217,6 +217,37 @@ public function packageScripts() } } + /** + * Package any data sources referred to in our screen or process. + * + * @return void + */ + public function packageDataSources() + { + $this->package['data_sources'] = []; + $dataSourceClass = 'ProcessMaker\Packages\Connectors\DataSources\Models\DataSource'; + + if (!class_exists($dataSourceClass)) { + return; + } + + if (!isset($this->screen)) { + $dataSourceIds = $this->manager->getDependenciesOfType($dataSourceClass, $this->process, []); + } else { + $dataSourceIds = $this->manager->getDependenciesOfType($dataSourceClass, $this->screen, []); + } + + if (count($dataSourceIds)) { + $dataSources = $dataSourceClass::whereIn('id', $dataSourceIds)->get(); + + $dataSources->each(function ($dataSource) { + $dataSourceArray = $dataSource->toArray(); + $dataSourceArray['categories'] = $dataSource->categories->toArray(); + $this->package['data_sources'][] = $dataSourceArray; + }); + } + } + /** * Package the metadata (NOT THE VALUE) of any environment variables * referred to in our scripts. @@ -253,6 +284,7 @@ private function packageFile() $this->packageProcessCategory(); $this->packageScreens(); $this->packageScripts(); + $this->packageDataSources(); $this->packageEnvironmentVariables(); } diff --git a/ProcessMaker/Jobs/ExportScreen.php b/ProcessMaker/Jobs/ExportScreen.php index 7bf930323b..3702a63331 100644 --- a/ProcessMaker/Jobs/ExportScreen.php +++ b/ProcessMaker/Jobs/ExportScreen.php @@ -38,6 +38,7 @@ private function packageFile() $this->package['version'] = '2'; $this->packageScreens(); $this->packageScripts(); + $this->packageDataSources(); } /** diff --git a/ProcessMaker/Jobs/ImportProcess.php b/ProcessMaker/Jobs/ImportProcess.php index 51ca215d7c..03b4768916 100644 --- a/ProcessMaker/Jobs/ImportProcess.php +++ b/ProcessMaker/Jobs/ImportProcess.php @@ -990,7 +990,7 @@ protected function watcherScriptsToSave($screen) $watcherList = []; foreach ($screen->watchers as $watcher) { $script = $watcher->script; - $watcher->script_id = $script->id; + $watcher->script_id = str_replace(['script-', 'data_source-'], '', $script->id); $watcher->script->title = $script->title; $watcherList[] = $watcher; } diff --git a/ProcessMaker/Jobs/MoveScriptExecutorToMicroservice.php b/ProcessMaker/Jobs/MoveScriptExecutorToMicroservice.php new file mode 100644 index 0000000000..584b7b6f01 --- /dev/null +++ b/ProcessMaker/Jobs/MoveScriptExecutorToMicroservice.php @@ -0,0 +1,27 @@ +uuid ); + } +} diff --git a/ProcessMaker/Jobs/SyncGuidedTemplates.php b/ProcessMaker/Jobs/SyncGuidedTemplates.php deleted file mode 100644 index a0494fcaab..0000000000 --- a/ProcessMaker/Jobs/SyncGuidedTemplates.php +++ /dev/null @@ -1,384 +0,0 @@ - 'Guided Templates', - ], [ - 'status' => 'ACTIVE', - 'is_system' => 1, - ])->getKey(); - - // Fetch the guided template list from Github - $response = Http::get($url); - - // Check if the request was successful - if (!$response->successful()) { - throw new Exception('Unable to fetch guided template list.'); - } - - // Extract the JSON data from the response - $data = $response->json(); - - // Iterate over categories and templates to retrieve them - foreach ($data as $templateCategory => $guidedTemplates) { - if (!in_array($templateCategory, $categories) && !in_array('all', $categories)) { - continue; - } - - try { - // Import templates from the index.json file. - foreach ($guidedTemplates as $template) { - $this->importTemplate($template, $config, $guidedTemplateCategoryId); - } - } catch (Exception $e) { - Log::error("Error Importing Guided Templates: {$e->getMessage()}"); - } - } - } catch (Exception $e) { - Log::error("Error Syncing Guided Templates: {$e->getMessage()}"); - } - } - - /** - * Import a guided template into the database. - * - * @param array $template - * @param array $config - * @param int $guidedTemplateCategoryId - * @return void - */ - private function importTemplate($template, $config, $guidedTemplateCategoryId) - { - // Check for template changes and determine if helper process and template process need to be imported - [$importHelperProcess, $importTemplateProcess] = $this->checkForTemplateChanges($template); - - // Check for template asset hash changes - $assetsHashChanged = $this->checkForTemplateAssetChanges($template); - - // Fetch payloads if necessary - $helperProcessPayload = $importHelperProcess ? - $this->fetchPayload($this->buildTemplateUrl($config, $template['helper_process'])) : null; - $templateProcessPayload = $importTemplateProcess ? - $this->fetchPayload($this->buildTemplateUrl($config, $template['template_process'])) : null; - - // Update process categories for the helper process and process template - $this->updateProcessCategories($helperProcessPayload, $templateProcessPayload, $guidedTemplateCategoryId); - - // Initialize variables for new process IDs - $newHelperProcessId = null; - $newProcessTemplateId = null; - - // Import helper process if necessary and get new ID - if ($importHelperProcess) { - $newHelperProcessId = $this->importProcess($helperProcessPayload, 'GUIDED_HELPER_PROCESS'); - } - - // Import template process if necessary and get new ID - if ($importTemplateProcess) { - $newProcessTemplateId = $this->importProcess($templateProcessPayload, 'GUIDED_PROCESS_TEMPLATE'); - } - - // Update or create the guided template in the database - $guidedTemplate = $this->updateOrCreateGuidedTemplate($template, $newHelperProcessId, $newProcessTemplateId); - - // Create a media collection for template assets - $mediaCollectionName = $this->createMediaCollection($guidedTemplate); - - if ($assetsHashChanged) { - // Import template assets and associate with the media collection - $this->importTemplateAssets($template, $config, $mediaCollectionName, $guidedTemplate); - } - - // Save the media collection name to the guided template and persist changes - $guidedTemplate->media_collection = $mediaCollectionName; - - $guidedTemplate->save(); - } - - // Helper functions used within importTemplate - private function buildTemplateUrl($config, $templatePath) - { - // Build the URL for a template based on the configuration and template path - if (empty($templatePath)) { - return null; - } - - return $config['base_url'] . - $config['template_repo'] . '/' . - $config['template_branch'] . '/' . - Str::replace('./', '', $templatePath); - } - - private function fetchPayload($url) - { - // Fetch the JSON payload from a given URL - return Http::get($url)->json(); - } - - private function updateProcessCategories(&$helperProcessPayload, &$templateProcessPayload, - $guidedTemplateCategoryId) - { - // Update process categories for both the helper process and process template - if ($helperProcessPayload !== null) { - data_set( - $helperProcessPayload, - "export.{$helperProcessPayload['root']}.attributes.process_category_id", - $guidedTemplateCategoryId - ); - } - - if ($templateProcessPayload !== null) { - data_set( - $templateProcessPayload, - "export.{$templateProcessPayload['root']}.attributes.process_category_id", - $guidedTemplateCategoryId - ); - } - } - - private function importProcess($payload, $assetType) - { - // Import a process and return the new ID - $postOptions = []; - foreach ($payload['export'] as $key => $asset) { - $postOptions[$key] = [ - 'mode' => 'update', - 'is_template' => true, - 'asset_type' => $assetType, - 'saveAssetsMode' => 'saveAllAssets', - ]; - if (in_array($asset['type'], ['Process', 'Screen', 'Script', - 'Collections', 'DataConnector', 'ProcessTemplates'])) { - $payload['export'][$key]['attributes']['asset_type'] = $assetType; - } - - if (Arr::get($asset, 'type') === 'Screen' && Arr::get($asset, 'attributes.key') === 'interstitial') { - Arr::set($payload, "export.{$key}.attributes.key", null); - } - } - - $options = new Options($postOptions); - try { - $importer = new Importer($payload, $options); - $manifest = $importer->doImport(); - $rootLog = $manifest[$payload['root']]->log; - - return $rootLog['newId']; - } catch (Exception $e) { - throw new Exception('Error: ' . $e->getMessage()); - } - } - - private function updateOrCreateGuidedTemplate($template, $newHelperProcessId, $newProcessTemplateId) - { - $templateDetails = $template['template_details']; - $uniqueTemplateId = $templateDetails['unique-template-id']; - $cardTitle = $templateDetails['card-title']; - $cardExcerpt = $templateDetails['card-excerpt']; - $templateDetailsJson = json_encode($templateDetails); - - // Check if the wizard template exists - $guidedTemplate = WizardTemplate::where('unique_template_id', $uniqueTemplateId)->first(); - - if ($guidedTemplate) { - // Update existing wizard template - $guidedTemplate->update([ - 'name' => $cardTitle, - 'description' => $cardExcerpt, - 'media_collection' => '', - 'template_details' => $templateDetailsJson, - ]); - - if ($newHelperProcessId !== null) { - $guidedTemplate['helper_process_id'] = $newHelperProcessId; - $guidedTemplate->save(); - } - if ($newProcessTemplateId !== null) { - $guidedTemplate['process_template_id'] = $newProcessTemplateId; - $guidedTemplate->save(); - } - } else { - // Create new wizard template - $guidedTemplate = WizardTemplate::create([ - 'unique_template_id' => $uniqueTemplateId, - 'name' => $cardTitle, - 'description' => $cardExcerpt, - 'helper_process_id' => $newHelperProcessId, - 'process_template_id' => $newProcessTemplateId, - 'media_collection' => '', - 'template_details' => $templateDetailsJson, - ]); - } - - return $guidedTemplate; - } - - private function createMediaCollection($guidedTemplate) - { - // Create a media collection for template assets and return the collection name - $mediaCollectionName = 'wt-' . $guidedTemplate->uuid . '-media'; - $guidedTemplate->addMediaCollection($mediaCollectionName); - - return $mediaCollectionName; - } - - private function importTemplateAssets($template, $config, $mediaCollectionName, $guidedTemplate) - { - // Clear the collection to prevent duplicate images - $guidedTemplate->clearMediaCollection($mediaCollectionName); - - // Build asset urls - $templateIconUrl = $this->buildTemplateUrl($config, $template['assets']['icon']); - $templateCardBackgroundUrl = $this->buildTemplateUrl($config, $template['assets']['card-background']); - $templateListIconUrl = $this->buildTemplateUrl($config, $template['assets']['list-icon']); - // Import template assets and associate with the media collection - $this->importMedia($templateIconUrl, 'icon', $mediaCollectionName, $guidedTemplate); - $this->importMedia($templateCardBackgroundUrl, 'cardBackground', $mediaCollectionName, $guidedTemplate); - $this->importMedia($templateListIconUrl, 'listIcon', $mediaCollectionName, $guidedTemplate); - - if (!empty($template['assets']['launchpad']['process-card-background'])) { - $templateProcessCardBackgroundUrl = - $this->buildTemplateUrl($config, $template['assets']['launchpad']['process-card-background']); - $this->importMedia($templateProcessCardBackgroundUrl, 'launchpadProcessCardBackground', - $mediaCollectionName, $guidedTemplate); - } - - foreach ($template['assets']['slides'] as $slide) { - $templateSlideUrl = $this->buildTemplateUrl($config, $slide); - $this->importMedia($templateSlideUrl, 'slide', $mediaCollectionName, $guidedTemplate); - } - - if (!empty($template['assets']['launchpad']['slides'])) { - foreach ($template['assets']['launchpad']['slides'] as $slide) { - $templateSlideUrl = $this->buildTemplateUrl($config, $slide); - $this->importMedia($templateSlideUrl, 'launchpadSlides', $mediaCollectionName, $guidedTemplate); - } - } - } - - private function importMedia($assetUrl, $customProperty, $mediaCollectionName, $guidedTemplate) - { - // Import a media asset and associate it with the media collection - if (!is_null($assetUrl)) { - $guidedTemplate - ->addMediaFromUrl($assetUrl) - ->withCustomProperties(['media_type' => $customProperty]) - ->toMediaCollection($mediaCollectionName); - } - } - - private function checkForTemplateChanges($template) - { - // Initialize variables to track changes - $helperProcessHashChanged = true; - $templateProcessHashChanged = true; - - // Retrieve wizard template details if it exists - $wizardTemplate = - WizardTemplate::where('unique_template_id', $template['template_details']['unique-template-id']) - ->select('template_details') - ->first(); - - if ($wizardTemplate) { - $wizardTemplateDetails = json_decode($wizardTemplate->template_details, true); - - // Check if helper process hash has changed - if (isset($wizardTemplateDetails['helper_process_hash']) && - $template['template_details']['helper_process_hash'] === - $wizardTemplateDetails['helper_process_hash']) { - $helperProcessHashChanged = false; - } - - // Check if template process hash has changed - if (isset($wizardTemplateDetails['template_process_hash']) && - $template['template_details']['template_process_hash'] === - $wizardTemplateDetails['template_process_hash']) { - $templateProcessHashChanged = false; - } - } - - return [$helperProcessHashChanged, $templateProcessHashChanged]; - } - - private function checkForTemplateAssetChanges($template) - { - // Initialize variables to track changes - $assetHashChanged = true; - // Retrieve wizard template details if it exists - $wizardTemplate = - WizardTemplate::where('unique_template_id', $template['template_details']['unique-template-id']) - ->select('template_details') - ->first(); - - if ($wizardTemplate) { - $wizardTemplateDetails = json_decode($wizardTemplate->template_details, true); - // Check if helper process hash has changed - if (isset($wizardTemplateDetails['asset_hash']) && - $template['template_details']['asset_hash'] === - $wizardTemplateDetails['asset_hash'] || - !isset($wizardTemplateDetails['asset_hash'])) { - $assetHashChanged = false; - } - } - - return $assetHashChanged; - } -} diff --git a/ProcessMaker/Jobs/SyncScreenTemplates.php b/ProcessMaker/Jobs/SyncScreenTemplates.php index 3ea0205fbf..0545ca6a1a 100644 --- a/ProcessMaker/Jobs/SyncScreenTemplates.php +++ b/ProcessMaker/Jobs/SyncScreenTemplates.php @@ -50,7 +50,7 @@ public function handle() if (!$config) { return; } - // Build the URL to fetch the guided templates list from GitHub + // Build the URL to fetch the screen templates list from GitHub $url = $config['base_url'] . $config['template_repo'] . '/' . $config['template_branch'] . '/index.json'; // If there are multiple categories of templates defined in the .env, separate them into an array diff --git a/ProcessMaker/Listeners/HandleRedirectListener.php b/ProcessMaker/Listeners/HandleRedirectListener.php index 78491809a4..7679a73572 100644 --- a/ProcessMaker/Listeners/HandleRedirectListener.php +++ b/ProcessMaker/Listeners/HandleRedirectListener.php @@ -20,6 +20,17 @@ protected function setRedirectTo(ProcessRequest $processRequest, string $method, self::$redirectionParams = $params; } + /** + * Reset the static state for Octane compatibility. + * This prevents data leaks between requests in long-running workers. + */ + public static function reset(): void + { + self::$processRequest = null; + self::$redirectionMethod = ''; + self::$redirectionParams = []; + } + public static function sendRedirectToEvent() { $method = self::$redirectionMethod; diff --git a/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php new file mode 100644 index 0000000000..a87205cdcc --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php @@ -0,0 +1,76 @@ +getHtmlBody(); + $textBody = $email->getTextBody(); + + $message = [ + 'subject' => $email->getSubject() ?? '', + 'body' => [ + 'contentType' => $htmlBody !== null ? 'HTML' : 'Text', + 'content' => $htmlBody ?? $textBody ?? '', + ], + 'toRecipients' => self::convertAddresses($email->getTo()), + ]; + + $ccRecipients = self::convertAddresses($email->getCc()); + if ($ccRecipients) { + $message['ccRecipients'] = $ccRecipients; + } + + $bccRecipients = self::convertAddresses($email->getBcc()); + if ($bccRecipients) { + $message['bccRecipients'] = $bccRecipients; + } + + $attachments = self::convertAttachments($email); + if ($attachments) { + $message['attachments'] = $attachments; + } + + return [ + 'message' => $message, + 'saveToSentItems' => true, + ]; + } + + /** + * @param Address[] $addresses + */ + private static function convertAddresses(array $addresses): array + { + return array_map(function (Address $address) { + $emailAddress = ['address' => $address->getAddress()]; + + if ($address->getName()) { + $emailAddress['name'] = $address->getName(); + } + + return ['emailAddress' => $emailAddress]; + }, $addresses); + } + + private static function convertAttachments(Email $email): array + { + $attachments = []; + + foreach ($email->getAttachments() as $attachment) { + $attachments[] = [ + '@odata.type' => '#microsoft.graph.fileAttachment', + 'name' => $attachment->getFilename() ?: 'attachment', + 'contentType' => $attachment->getMediaType() . '/' . $attachment->getMediaSubtype(), + 'contentBytes' => base64_encode($attachment->getBody()), + ]; + } + + return $attachments; + } +} diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php new file mode 100644 index 0000000000..7f2bf2fa85 --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -0,0 +1,57 @@ +requestAccessToken(); + } + + private function requestAccessToken(): string + { + $tenantId = $this->config['tenant_id'] ?? null; + $clientId = $this->config['key'] ?? null; + $clientSecret = $this->config['secret'] ?? null; + + if (!$tenantId || !$clientId || !$clientSecret) { + throw new RuntimeException('Microsoft Graph credentials are not configured.'); + } + + $client = $this->httpClient ?? new Client(); + $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ + 'form_params' => [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'scope' => self::DEFAULT_SCOPE, + 'grant_type' => 'client_credentials', + ], + 'http_errors' => false, + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() >= 400) { + $message = $body['error_description'] ?? $body['error']['message'] ?? 'Unknown error'; + + throw new RuntimeException('Failed to get Microsoft Graph access token: ' . $message); + } + + return $body['access_token']; + } +} diff --git a/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php b/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php new file mode 100644 index 0000000000..59ff0d1b96 --- /dev/null +++ b/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php @@ -0,0 +1,53 @@ +client = $client ?? new Client([ + 'base_uri' => 'https://graph.microsoft.com/v1.0/', + ]); + } + + protected function doSend(SentMessage $message): void + { + $email = MessageConverter::toEmail($message->getOriginalMessage()); + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + $token = $this->tokenProvider->getAccessToken(); + + $response = $this->client->post('users/' . rawurlencode($this->senderEmail) . '/sendMail', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'json' => $payload, + 'http_errors' => false, + ]); + + if ($response->getStatusCode() >= 400) { + throw new TransportException( + 'Microsoft Graph send failed: ' . $response->getBody()->getContents() + ); + } + } + + public function __toString(): string + { + return 'microsoft-graph'; + } +} diff --git a/ProcessMaker/Managers/OauthMailManager.php b/ProcessMaker/Managers/OauthMailManager.php index 603b174970..58a7d08b12 100644 --- a/ProcessMaker/Managers/OauthMailManager.php +++ b/ProcessMaker/Managers/OauthMailManager.php @@ -7,6 +7,8 @@ use Google\Client as GoogleClient; use GuzzleHttp\Client; use Illuminate\Mail\MailManager; +use ProcessMaker\Mail\MicrosoftGraphTokenProvider; +use ProcessMaker\Mail\Transports\MicrosoftGraphTransport; use ProcessMaker\Models\EnvironmentVariable; use ProcessMaker\Models\Setting; use ProcessMaker\Packages\Connectors\Email\EmailConfig; @@ -77,6 +79,26 @@ protected function createSmtpTransport($config) return $transport; } + public function createTransport(array $config) + { + if ($this->app->config->get('mail.driver') === 'microsoft_graph') { + return $this->createMicrosoftGraphTransport($config); + } + + return parent::createTransport($config); + } + + protected function createMicrosoftGraphTransport(array $config) + { + return new MicrosoftGraphTransport( + new MicrosoftGraphTokenProvider( + $this->app->config->get('services.microsoft_graph', []), + $this->emailServerIndex ?? 0 + ), + $this->fromAddress + ); + } + public function checkForExpiredAccessToken() { switch ($this->authMethod) { diff --git a/ProcessMaker/Managers/TaskSchedulerManager.php b/ProcessMaker/Managers/TaskSchedulerManager.php index 10e26e4fd4..a158118e37 100644 --- a/ProcessMaker/Managers/TaskSchedulerManager.php +++ b/ProcessMaker/Managers/TaskSchedulerManager.php @@ -13,6 +13,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; +use InvalidArgumentException; use PDOException; use ProcessMaker\Facades\WorkflowManager; use ProcessMaker\Jobs\StartEventConditional; @@ -203,10 +204,11 @@ private function processTaskWithAtomicClaim(ScheduledTask $task, DateTime $today { try { $config = json_decode($task->configuration); - $lastExecution = new DateTime($task->last_execution, new DateTimeZone('UTC')); - if ($lastExecution === null) { - return; + // SCHEDULED_JOB rows use last_execution = null until first run; BPMN timers always set last_execution. + $lastExecution = null; + if ($task->last_execution !== null && $task->last_execution !== '') { + $lastExecution = new DateTime($task->last_execution, new DateTimeZone('UTC')); } $owner = $task->processRequestToken ?: $task->processRequest ?: $task->process; @@ -888,6 +890,9 @@ public function scheduleCycle( */ public function scheduleCycleJob($interval, array $config): ScheduledTask { + if (!isset($config['job'])) { + throw new InvalidArgumentException('$config["job"] is required'); + } $configuration = [ 'type' => 'TimeCycle', 'interval' => $interval, @@ -904,6 +909,36 @@ public function scheduleCycleJob($interval, array $config): ScheduledTask return $scheduledTask; } + /** + * Schedule a job for a specific datetime + * + * @param string $datetime in ISO-8601 format + * @param array $config configuration + * + * @return ScheduledTask + */ + public function scheduleDateJob($datetime, array $config): ScheduledTask + { + if (!isset($config['job'])) { + throw new InvalidArgumentException('$config["job"] is required'); + } + + // Must use "interval" so nextDate(TimeDate) picks up the target datetime (same shape as BPMN timer tasks). + $configuration = [ + 'type' => 'TimeDate', + ...$config, + 'interval' => $datetime, + ]; + + $scheduledTask = new ScheduledTask(); + $scheduledTask->configuration = json_encode($configuration); + $scheduledTask->type = 'SCHEDULED_JOB'; + $scheduledTask->last_execution = null; + $scheduledTask->save(); + + return $scheduledTask; + } + /** * Schedule a job execution after a time duration for the given BPMN element, * event definition and an optional Token object diff --git a/ProcessMaker/Models/Bundle.php b/ProcessMaker/Models/Bundle.php index 98f6328ab5..3971c973a3 100644 --- a/ProcessMaker/Models/Bundle.php +++ b/ProcessMaker/Models/Bundle.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Support\Facades\Http; +use ProcessMaker\Exception\BundleIntegrityException; use ProcessMaker\Exception\ExporterNotSupported; use ProcessMaker\Exception\ValidationException; use ProcessMaker\ImportExport\Importer; @@ -78,6 +79,8 @@ public function getAssetCountAttribute() public function export() { + $this->assertIntegrity(); + $exports = []; foreach ($this->assets as $bundleAsset) { @@ -93,6 +96,21 @@ public function export() return $exports; } + public function invalidAssets() + { + return $this->assets->filter( + fn (BundleAsset $asset) => $asset->integrity_status !== BundleAsset::INTEGRITY_VALID + ); + } + + public function assertIntegrity(): void + { + $invalidAssets = $this->invalidAssets(); + if ($invalidAssets->isNotEmpty()) { + throw new BundleIntegrityException($this, $invalidAssets); + } + } + public function exportSettings() { $exports = []; diff --git a/ProcessMaker/Models/BundleAsset.php b/ProcessMaker/Models/BundleAsset.php index d0af2d536b..979e0f4003 100644 --- a/ProcessMaker/Models/BundleAsset.php +++ b/ProcessMaker/Models/BundleAsset.php @@ -3,15 +3,22 @@ namespace ProcessMaker\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Str; use ProcessMaker\Enums\ExporterMap; class BundleAsset extends ProcessMakerModel { use HasFactory; + public const INTEGRITY_VALID = 'valid'; + + public const INTEGRITY_MISSING = 'missing'; + + public const INTEGRITY_TYPE_UNAVAILABLE = 'type_unavailable'; + protected $guarded = ['id']; - protected $appends = ['name', 'url', 'type', 'owner_name', 'categories']; + protected $appends = ['name', 'url', 'type', 'owner_name', 'categories', 'integrity_status']; const DATA_SOURCE_CLASS = 'ProcessMaker\Packages\Connectors\DataSources\Models\DataSource'; @@ -23,9 +30,11 @@ class BundleAsset extends ProcessMakerModel const PM_BLOCK_CLASS = 'ProcessMaker\Package\PackagePmBlocks\Models\PmBlock'; - public static function canExport(ProcessMakerModel $asset) + public static function canExport(?ProcessMakerModel $asset) { - return method_exists($asset, 'export') && ExporterMap::getExporterClassForModel($asset); + return $asset !== null + && method_exists($asset, 'export') + && ExporterMap::getExporterClassForModel($asset); } public function bundle() @@ -50,18 +59,30 @@ public static function makeKey(ProcessMakerModel $asset) public function getNameAttribute() { + $asset = $this->resolvedAsset(); + if ($asset === null) { + return __('Missing :type #:id', [ + 'type' => $this->typeLabel(), + 'id' => $this->asset_id, + ]); + } + if ( $this->asset_type === Screen::class || $this->asset_type === Script::class ) { - return $this->asset->title; + return $asset->title; } - return $this->asset->name; + return $asset->name; } public function getUrlAttribute() { + if ($this->integrity_status !== self::INTEGRITY_VALID) { + return null; + } + switch($this->asset_type) { case Screen::class: return "/designer/screen-builder/{$this->asset_id}/edit"; @@ -110,8 +131,9 @@ public function getTypeAttribute() public function getOwnerNameAttribute() { - if ($this->asset && method_exists($this->asset, 'user') && $this->asset->user) { - return $this->asset->user->firstname . ' ' . $this->asset->user->lastname; + $asset = $this->resolvedAsset(); + if ($asset && method_exists($asset, 'user') && $asset->user) { + return $asset->user->firstname . ' ' . $asset->user->lastname; } return null; @@ -123,10 +145,48 @@ public function getCategoriesAttribute() return []; } - if ($this->asset && method_exists($this->asset, 'categories')) { - return $this->asset->categories->pluck('name')->toArray(); + $asset = $this->resolvedAsset(); + if ($asset && method_exists($asset, 'categories')) { + return $asset->categories->pluck('name')->toArray(); } return []; } + + public function getIntegrityStatusAttribute(): string + { + if (!class_exists($this->asset_type)) { + return self::INTEGRITY_TYPE_UNAVAILABLE; + } + + return $this->resolvedAsset() === null + ? self::INTEGRITY_MISSING + : self::INTEGRITY_VALID; + } + + public function integrityDetails(): array + { + return [ + 'bundle_asset_id' => $this->id, + 'asset_type' => $this->asset_type, + 'asset_id' => $this->asset_id, + 'integrity_status' => $this->integrity_status, + ]; + } + + private function resolvedAsset(): ?ProcessMakerModel + { + if (!class_exists($this->asset_type)) { + return null; + } + + return $this->asset; + } + + private function typeLabel(): string + { + $type = $this->type ?? class_basename($this->asset_type); + + return Str::headline($type); + } } diff --git a/ProcessMaker/Models/DevLink.php b/ProcessMaker/Models/DevLink.php index e7ad53939d..7637d24253 100644 --- a/ProcessMaker/Models/DevLink.php +++ b/ProcessMaker/Models/DevLink.php @@ -3,8 +3,10 @@ namespace ProcessMaker\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Http\Client\RequestException; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; +use ProcessMaker\Exception\DevLinkRemoteBundleException; use ProcessMaker\ImportExport\Importer; use ProcessMaker\ImportExport\Logger; use ProcessMaker\ImportExport\Options; @@ -143,19 +145,28 @@ public function installRemoteBundle($remoteBundleId, $updateType) $this->logger->status(__('Downloading bundle from remote instance')); - $bundleInfo = $this->remoteBundle($remoteBundleId)->json(); + try { + $bundleInfo = $this->remoteBundle($remoteBundleId)->json(); - $bundleExport = $this->client()->get( - route('api.devlink.export-local-bundle', ['bundle' => $remoteBundleId], false) - )->json(); + $bundleExport = $this->client()->get( + route('api.devlink.export-local-bundle', ['bundle' => $remoteBundleId], false) + )->json(); - $bundleSettingsExport = $this->client()->get( - route('api.devlink.export-local-bundle-settings', ['bundle' => $remoteBundleId], false) - )->json(); + $bundleSettingsExport = $this->client()->get( + route('api.devlink.export-local-bundle-settings', ['bundle' => $remoteBundleId], false) + )->json(); - $bundleSettingsPayloads = $this->client()->get( - route('api.devlink.export-local-bundle-setting-payloads', ['bundle' => $remoteBundleId], false) - )->json(); + $bundleSettingsPayloads = $this->client()->get( + route('api.devlink.export-local-bundle-setting-payloads', ['bundle' => $remoteBundleId], false) + )->json(); + } catch (RequestException $exception) { + $invalidAssets = $exception->response->json('errors.assets'); + if (is_array($invalidAssets) && $invalidAssets !== []) { + throw new DevLinkRemoteBundleException($invalidAssets, $exception); + } + + throw $exception; + } $bundle = Bundle::updateOrCreate( [ diff --git a/ProcessMaker/Models/WizardTemplate.php b/ProcessMaker/Models/WizardTemplate.php deleted file mode 100644 index 0e09ff546e..0000000000 --- a/ProcessMaker/Models/WizardTemplate.php +++ /dev/null @@ -1,101 +0,0 @@ -belongsTo(Process::class, 'helper_process_id'); - } - - /** - * Get the process template associated with the wizard template. - */ - public function process_template(): BelongsTo - { - return $this->belongsTo(ProcessTemplates::class, 'process_template_id'); - } - - /** - * Filter settings with a string - * - * @param $query - * - * @param $filter string - */ - public function scopeFilter($query, $filterStr) - { - $filter = '%' . mb_strtolower($filterStr) . '%'; - $query->where(function ($query) use ($filter) { - $query->where('wizard_templates.name', 'like', $filter) - ->orWhere('wizard_templates.description', 'like', $filter); - }); - - return $query; - } - - public function getTemplateMediaAttribute() - { - $mediaCollectionName = 'wt-' . $this->uuid . '-media'; - $slides = $this->getMedia($mediaCollectionName, ['media_type' => 'slide']); - $slideUrls = $slides->map(function ($slide) { - return $slide->getFullUrl(); - }); - $iconMedia = $this->getMedia($mediaCollectionName, ['media_type' => 'icon'])->first(); - $cardBackgroundMedia = $this->getMedia($mediaCollectionName, ['media_type' => 'cardBackground'])->first(); - $listIconMedia = $this->getMedia($mediaCollectionName, ['media_type' => 'listIcon'])->first(); - - return [ - 'icon' => !is_null($iconMedia) ? $iconMedia->getFullUrl() : '', - 'cardBackground' => !is_null($cardBackgroundMedia) ? $cardBackgroundMedia->getFullUrl() : '', - 'listIcon' => !is_null($listIconMedia) ? $listIconMedia->getFullUrl() : '', - 'slides' => $slideUrls, - ]; - } - - /** - * Add files to media collection - */ - public function addFilesToMediaCollection(string $directoryPath) - { - $files = File::allFiles($directoryPath); - $collectionName = basename($directoryPath); - - foreach ($files as $file) { - $this->addMedia($file->getPathname())->toMediaCollection($collectionName); - } - } -} diff --git a/ProcessMaker/Octane/ResetRequestState.php b/ProcessMaker/Octane/ResetRequestState.php new file mode 100644 index 0000000000..45071e97ad --- /dev/null +++ b/ProcessMaker/Octane/ResetRequestState.php @@ -0,0 +1,17 @@ +checkConfigCache(); + // Register Octane listeners if Octane is enabled + $this->registerOctaneListeners(); + // Hook after service providers boot self::$bootTime = (microtime(true) - self::$bootStart) * 1000; // Convert to milliseconds } @@ -122,6 +129,8 @@ public function register(): void // Register our permission services $this->app->register(PermissionServiceProvider::class); + $this->app->scoped(SmartExtractConfiguration::class); + $this->app->singleton(Managers\PackageManager::class, function () { return new Managers\PackageManager(); }); @@ -257,7 +266,7 @@ protected static function registerEvents(): void { // Listen to the events for our core screen // types and add our javascript - Facades\Event::listen(ScreenBuilderStarting::class, function ($event) { + Event::listen(ScreenBuilderStarting::class, function ($event) { // Add any extensions to form builder // and renderer from packages $event->manager->addPackageScripts($event->type); @@ -276,7 +285,7 @@ protected static function registerEvents(): void }); // Log Notifications - Facades\Event::listen(NotificationSent::class, function ($event) { + Event::listen(NotificationSent::class, function ($event) { $id = $event->notifiable->id; $notifiable = get_class($event->notifiable); $notification = get_class($event->notification); @@ -285,24 +294,24 @@ protected static function registerEvents(): void }); // Log Broadcasts (messages sent to laravel-echo-server and redis) - Facades\Event::listen(BroadcastNotificationCreated::class, function ($event) { + Event::listen(BroadcastNotificationCreated::class, function ($event) { $channels = implode(', ', $event->broadcastOn()); Log::debug('Broadcasting Notification ' . $event->broadcastType() . 'on channel(s) ' . $channels); }); // Fire job when task is assigned to a user - Facades\Event::listen(ActivityAssigned::class, function ($event) { + Event::listen(ActivityAssigned::class, function ($event) { $task_id = $event->getProcessRequestToken()->id; // Dispatch the SmartInbox job with the processRequestToken as parameter SmartInbox::dispatch($task_id); }); - Facades\Event::listen(MadeTenantCurrentEvent::class, function ($event) { + Event::listen(MadeTenantCurrentEvent::class, function ($event) { event(new TenantResolved($event->tenant)); }); - Facades\Event::listen(TenantNotFoundForRequestEvent::class, function ($event) { + Event::listen(TenantNotFoundForRequestEvent::class, function ($event) { if (config('app.multitenancy') === false || self::actuallyRunningInConsole()) { // This is expected if multitenancy is disabled. // We also need to check if we are running in a console command because @@ -327,7 +336,7 @@ protected static function registerEvents(): void } }); - Facades\Event::listen(function (CommandStarting $event) { + Event::listen(function (CommandStarting $event) { if ($event->command === 'l5-swagger:generate') { // Set the analyser to use the legacy DocBlockAnnotationFactory. This must // be set here because this config value is not serializable and cannot be cached. @@ -508,6 +517,14 @@ public static function getBootTime(): ?float return self::$bootTime; } + /** + * Reset per-request query timing metrics. + */ + public static function beginRequestTiming(): void + { + self::$queryTime = 0; + } + /** * Get the query time for the request. * @@ -565,6 +582,23 @@ public static function getPackageBootTiming(): array return self::$packageBootTiming; } + /** + * Reset per-request static state between Octane requests. + * + * Octane workers stay alive across requests, so static properties must be + * cleared to avoid leaking data from one request into the next. Singletons + * holding mutable state are handled by the 'flush' list in config/octane.php, + * which Octane applies on its own. + */ + private function registerOctaneListeners(): void + { + if (!class_exists(RequestTerminated::class)) { + return; + } + + Event::listen(RequestTerminated::class, ResetRequestState::class); + } + /** * Find the tenant based on the environment variable */ diff --git a/ProcessMaker/Providers/WorkflowServiceProvider.php b/ProcessMaker/Providers/WorkflowServiceProvider.php index 7bdbc78c28..fffbff740f 100644 --- a/ProcessMaker/Providers/WorkflowServiceProvider.php +++ b/ProcessMaker/Providers/WorkflowServiceProvider.php @@ -9,6 +9,8 @@ use ProcessMaker\Assets\ScreensInScreen; use ProcessMaker\Assets\ScriptsInProcess; use ProcessMaker\Assets\ScriptsInScreen; +use ProcessMaker\Assets\DataSourcesInScreen; +use ProcessMaker\Assets\DataSourcesInProcess; use ProcessMaker\Bpmn\MustacheOptions; use ProcessMaker\BpmnEngine; use ProcessMaker\Contracts\TimerExpressionInterface; @@ -195,6 +197,8 @@ function (ThrowEventInterface $source, EventDefinitionInterface $sourceEventDefi $instance->addDependencyManager(ScreensInScreen::class); $instance->addDependencyManager(ScriptsInProcess::class); $instance->addDependencyManager(ScriptsInScreen::class); + $instance->addDependencyManager(DataSourcesInScreen::class); + $instance->addDependencyManager(DataSourcesInProcess::class); return $instance; }); diff --git a/ProcessMaker/Repositories/SettingsConfigRepository.php b/ProcessMaker/Repositories/SettingsConfigRepository.php index 2f4cf6e0b2..f2707faf2f 100644 --- a/ProcessMaker/Repositories/SettingsConfigRepository.php +++ b/ProcessMaker/Repositories/SettingsConfigRepository.php @@ -39,7 +39,7 @@ public function get($key, $default = null) if ($key === 'session.lifetime') { $settingValue = $this->getFromSettings($key); - return $settingValue ?? $default; + return $settingValue ?: Arr::get($this->items, $key) ?: $default ?: 120; } if (Arr::has($this->items, $key)) { diff --git a/ProcessMaker/ScriptRunners/Base.php b/ProcessMaker/ScriptRunners/Base.php index d261245795..8d6b767053 100644 --- a/ProcessMaker/ScriptRunners/Base.php +++ b/ProcessMaker/ScriptRunners/Base.php @@ -12,6 +12,7 @@ use ProcessMaker\Models\ScriptDockerNayraTrait; use ProcessMaker\Models\ScriptExecutor; use ProcessMaker\Models\User; +use ProcessMaker\Services\SmartExtractConfiguration; use RuntimeException; abstract class Base @@ -177,6 +178,10 @@ private function getEnvironmentVariables($useEscape = true) foreach ($variables as $variable) { // Fix variables that have spaces $variable['name'] = str_replace(' ', '_', $variable['name']); + if ($variable['name'] === SmartExtractConfiguration::API_HOST) { + continue; + } + if ($useEscape) { $variablesParameter[] = escapeshellarg($variable['name']) . '=' . escapeshellarg($variable['value']); } else { @@ -185,14 +190,19 @@ private function getEnvironmentVariables($useEscape = true) } }); + $smartExtractApiHost = app(SmartExtractConfiguration::class)->apiHost(); + if ($smartExtractApiHost !== null) { + $variablesParameter[] = $useEscape + ? SmartExtractConfiguration::API_HOST . '=' . escapeshellarg($smartExtractApiHost) + : SmartExtractConfiguration::API_HOST . '=' . $smartExtractApiHost; + } + // Add the url to the host if ($useEscape) { $variablesParameter[] = 'HOST_URL=' . escapeshellarg(config('app.docker_host_url')); - $variablesParameter[] = 'SMART_EXTRACT_API_HOST=' . escapeshellarg(config('smart-extract.api_host')); $variablesParameter[] = 'SMART_EXTRACT_REQUEST_TIMEOUT=' . escapeshellarg((string) config('smart-extract.request_timeout')); } else { $variablesParameter[] = 'HOST_URL=' . config('app.docker_host_url'); - $variablesParameter[] = 'SMART_EXTRACT_API_HOST=' . config('smart-extract.api_host'); $variablesParameter[] = 'SMART_EXTRACT_REQUEST_TIMEOUT=' . config('smart-extract.request_timeout'); } diff --git a/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php b/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php index 52ab3cb46f..dc4d5f3a96 100644 --- a/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php +++ b/ProcessMaker/ScriptRunners/ScriptMicroserviceRunner.php @@ -13,6 +13,7 @@ use ProcessMaker\Models\Script; use ProcessMaker\Models\User; use ProcessMaker\Services\ScriptMicroserviceService; +use ProcessMaker\Services\SmartExtractConfiguration; use stdClass; class ScriptMicroserviceRunner @@ -85,10 +86,20 @@ private function getEnvironmentVariables(User $user) EnvironmentVariable::chunk(50, function (Collection $variables) use (&$variablesParameter) { foreach ($variables as $variable) { // Fix variables that have spaces - $variablesParameter[str_replace(' ', '_', $variable->name)] = $variable->value; + $name = str_replace(' ', '_', $variable->name); + if ($name === SmartExtractConfiguration::API_HOST) { + continue; + } + + $variablesParameter[$name] = $variable->value; } }); + $smartExtractApiHost = app(SmartExtractConfiguration::class)->apiHost(); + if ($smartExtractApiHost !== null) { + $variablesParameter[SmartExtractConfiguration::API_HOST] = $smartExtractApiHost; + } + // Add the url to the host $variablesParameter['HOST_URL'] = config('app.docker_host_url'); @@ -105,7 +116,6 @@ private function getEnvironmentVariables(User $user) $variablesParameter['API_HOST'] = config('app.docker_host_url') . '/api/1.0'; $variablesParameter['APP_URL'] = config('app.docker_host_url'); $variablesParameter['API_SSL_VERIFY'] = (config('app.api_ssl_verify') ? '1' : '0'); - $variablesParameter['SMART_EXTRACT_API_HOST'] = config('smart-extract.api_host'); $variablesParameter['SMART_EXTRACT_REQUEST_TIMEOUT'] = config('smart-extract.request_timeout'); } diff --git a/ProcessMaker/Services/SmartExtractConfiguration.php b/ProcessMaker/Services/SmartExtractConfiguration.php new file mode 100644 index 0000000000..a55146c56b --- /dev/null +++ b/ProcessMaker/Services/SmartExtractConfiguration.php @@ -0,0 +1,104 @@ + 'smart-extract.api_host', + self::CLIENT_ID => 'smart-extract.client_id', + self::CLIENT_SECRET => 'smart-extract.client_secret', + self::DASHBOARD_URL => 'smart-extract.dashboard_url', + self::HITL_ENABLED => 'smart-extract.hitl_enabled', + ]; + + private ?array $values = null; + + public function apiHost(): ?string + { + return $this->stringValue(self::API_HOST); + } + + public function clientId(): ?string + { + return $this->stringValue(self::CLIENT_ID); + } + + public function clientSecret(): ?string + { + return $this->stringValue(self::CLIENT_SECRET); + } + + public function dashboardUrl(): ?string + { + return $this->stringValue(self::DASHBOARD_URL); + } + + public function hitlEnabled(): bool + { + $value = $this->value(self::HITL_ENABLED); + + if (is_bool($value)) { + return $value; + } + + if (!is_string($value)) { + return false; + } + + return filter_var(trim($value), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? false; + } + + private function stringValue(string $name): ?string + { + $value = $this->value($name); + + return is_string($value) && trim($value) !== '' ? $value : null; + } + + private function value(string $name): mixed + { + $values = $this->values(); + + if (array_key_exists($name, $values)) { + return $values[$name]; + } + + return config(self::CONFIG_KEYS[$name]); + } + + private function values(): array + { + if ($this->values !== null) { + return $this->values; + } + + $this->values = EnvironmentVariable::query() + ->whereIn('name', [ + self::API_HOST, + self::CLIENT_ID, + self::CLIENT_SECRET, + self::DASHBOARD_URL, + self::HITL_ENABLED, + ]) + ->get() + ->mapWithKeys(fn (EnvironmentVariable $variable) => [ + $variable->name => $variable->value, + ]) + ->all(); + + return $this->values; + } +} diff --git a/ProcessMaker/Templates/ProcessTemplate.php b/ProcessMaker/Templates/ProcessTemplate.php index 3d9f6f6aa4..ca24deb766 100644 --- a/ProcessMaker/Templates/ProcessTemplate.php +++ b/ProcessMaker/Templates/ProcessTemplate.php @@ -15,10 +15,8 @@ use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessCategory; use ProcessMaker\Models\ProcessTemplates; -use ProcessMaker\Models\WizardTemplate; use ProcessMaker\Traits\HasControllerAddons; use SebastianBergmann\CodeUnit\Exception; -use Spatie\MediaLibrary\MediaCollections\Models\Media; /** * Summary of ProcessTemplate @@ -257,18 +255,6 @@ public function create($request) : JsonResponse $payload['export'][$key]['attributes']['name'] = $requestData['name']; $payload['export'][$key]['attributes']['description'] = $requestData['description']; $payload['export'][$key]['attributes']['process_category_id'] = $requestData['process_category_id']; - // Store the wizard template uuid on the process to rerun the helper process - if (isset($requestData['wizardTemplateUuid'])) { - $properties = json_decode($payload['export'][$key]['attributes']['properties'], true); - $properties['wizardTemplateUuid'] = $requestData['wizardTemplateUuid']; - $payload['export'][$key]['attributes']['properties'] = json_encode($properties); - } - // Store the helper process request id that initiated the process creation - if (isset($requestData['helperProcessRequestId'])) { - $properties = json_decode($payload['export'][$key]['attributes']['properties'], true); - $properties['helperProcessRequestId'] = $requestData['helperProcessRequestId']; - $payload['export'][$key]['attributes']['properties'] = json_encode($properties); - } $payload['export'][$key]['name'] = $requestData['name']; $payload['export'][$key]['description'] = $requestData['description']; @@ -296,8 +282,6 @@ public function create($request) : JsonResponse $process->user_id = Auth::id(); $process->save(); - $this->syncLaunchpadAssets($request, $process); - if (class_exists(self::PROJECT_ASSET_MODEL_CLASS) && !empty($requestData['projects'])) { $manifest = $this->getManifest('process', $processId); @@ -506,64 +490,6 @@ public function existingTemplate($request) : ?array return null; } - /** - * Syncs launchpad assets from a guided template to the imported process. - * - * @param Illuminate\Http\Request $request - * @param App\Models\Process $process - * @return void - */ - protected function syncLaunchpadAssets($request, $process) - { - if (empty($request->wizardTemplateUuid)) { - return; - } - - // Add media collection for the imported process - $processMediaCollectionName = $process->uuid . '_images_carousel'; - $process->addMediaCollection($processMediaCollectionName); - - // Retrieve the guided template by UUID - $guidedTemplateUuid = $request->input('wizardTemplateUuid'); - $template = WizardTemplate::where('uuid', $guidedTemplateUuid)->first(); - - // Get launchpad slides media from the guided template - $templateLaunchpadSlides = $template->getMedia($template->media_collection, function (Media $media) { - return $media->custom_properties['media_type'] === 'launchpadSlides'; - }); - - // Iterate over each launchpad slide and add to the imported process media collection - foreach ($templateLaunchpadSlides as $slide) { - // Extract order index from file name - $orderIndex = $this->extractOrderIndexFromFileName($slide->getPath()); - - // Add media to the imported process collection - $media = $process->addMedia($slide->getPath())->preservingOriginal()->toMediaCollection($processMediaCollectionName); - - // Set order column if available - if (!is_null($orderIndex)) { - $media->order_column = $orderIndex; - $media->save(); - } - } - } - - /** - * Extracts order index from the file name. - * - * @param string $fileName - * @return int|null - */ - protected function extractOrderIndexFromFileName($fileName) - { - preg_match('/\d+/', basename($fileName), $matches); - if (!empty($matches)) { - return intval($matches[0]) - 1; - } - - return null; - } - /** * Prepare payload for import. * diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index dcfa97bea0..fd76a3065d 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -16,6 +16,7 @@ use ProcessMaker\Models\User; use ProcessMaker\Package\SavedSearch\Models\SavedSearch; use ProcessMaker\Query\SyntaxError; +use ProcessMaker\Services\SmartExtractConfiguration; trait TaskControllerIndexMethods { @@ -161,7 +162,7 @@ private function excludeNonVisibleTasks($query, $request) { $nonSystem = filter_var($request->input('non_system'), FILTER_VALIDATE_BOOLEAN); $allTasks = filter_var($request->input('all_tasks'), FILTER_VALIDATE_BOOLEAN); - $hitlEnabled = filter_var(config('smart-extract.hitl_enabled'), FILTER_VALIDATE_BOOLEAN); + $hitlEnabled = app(SmartExtractConfiguration::class)->hitlEnabled(); $includeScreen = filter_var($request->input('includeScreen'), FILTER_VALIDATE_BOOLEAN); $query->when(!$allTasks, function ($query) use ($includeScreen) { $query->where(function ($query) use ($includeScreen) { diff --git a/composer.json b/composer.json index 1d700325d8..ad11ea335e 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "processmaker/processmaker", - "version": "2026.12.3", + "version": "2026.13.2", "description": "BPM PHP Software", "keywords": [ "php bpm processmaker" @@ -21,12 +21,13 @@ "fakerphp/faker": "^1.24", "google/apiclient": "^2.18", "google/protobuf": "^4.33.6", - "guzzlehttp/guzzle": "^7.13.1", + "guzzlehttp/guzzle": "^7.15.2", "guzzlehttp/psr7": "^2.12.3", "igaster/laravel-theme": "^2.0", "jenssegers/agent": "^2.6", - "laravel/framework": "^13.13", - "laravel/horizon": "^5.47", + "laravel/framework": "^13.0", + "laravel/horizon": "^5.45", + "laravel/octane": "^2.17", "laravel/pail": "^1.2", "laravel/passport": "^13.7", "laravel/scout": "^11.1", @@ -35,7 +36,7 @@ "laravel/ui": "^4.6", "lavary/laravel-menu": "^1.8", "lcobucci/jwt": "^5.6", - "league/commonmark": "^2.8.1", + "league/commonmark": "^2.9.0", "league/flysystem-aws-s3-v3": "^3.25.1", "mateusjunges/laravel-kafka": "^2.11", "mittwald/vault-php": "^2.1", @@ -86,7 +87,7 @@ "laravel/boost": "^2.4", "mockery/mockery": "^1.6", "phpunit/phpunit": "^12.5.8", - "squizlabs/php_codesniffer": "^3.11" + "squizlabs/php_codesniffer": "^3.13.6" }, "autoload": { "files": [ @@ -116,7 +117,7 @@ "Gmail" ], "processmaker": { - "build": "449111cb", + "build": "6557348b", "cicd-enabled": true, "custom": { "package-ellucian-ethos": "1.19.10", @@ -154,21 +155,21 @@ "connector-docusign": "1.11.2", "connector-idp": "1.14.2", "connector-pdf-print": "1.23.3", - "connector-send-email": "1.32.18", + "connector-send-email": "1.32.19", "connector-slack": "1.9.7", "docker-executor-node-ssr": "1.7.4", "package-ab-testing": "1.4.2", "package-actions-by-email": "1.22.17", - "package-advanced-user-manager": "1.13.3", - "package-ai": "1.16.24", + "package-advanced-user-manager": "1.13.4", + "package-ai": "1.16.26", "package-analytics-reporting": "1.11.5", - "package-auth": "1.24.19", - "package-collections": "2.27.9", + "package-auth": "1.24.20", + "package-collections": "2.27.11", "package-comments": "1.16.4", "package-conversational-forms": "1.15.2", - "package-data-sources": "1.34.12", + "package-data-sources": "1.34.13", "package-decision-engine": "1.16.3", - "package-dynamic-ui": "1.28.5", + "package-dynamic-ui": "1.28.6", "package-email-start-event": "1.0.13", "package-files": "1.23.7", "package-googleplaces": "1.12.1", @@ -179,12 +180,12 @@ "package-product-analytics": "1.5.11", "package-projects": "1.12.9", "package-rpa": "1.1.2", - "package-savedsearch": "1.43.14", + "package-savedsearch": "1.43.16", "package-slideshow": "1.4.3", - "package-smart-extract": "1.0.0", + "package-smart-extract": "1.0.1", "package-signature": "1.15.6", "package-testing": "1.8.2", - "package-translations": "2.14.6", + "package-translations": "2.14.7", "package-versions": "1.13.1", "package-vocabularies": "2.17.2", "package-webentry": "2.29.19", @@ -253,4 +254,4 @@ "ignore": [] } } -} +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 69114017d6..3d65b943ce 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "16713528d4523a3285c6fd02776afcb3", + "content-hash": "47d1471b5383bbd226596fd2add4531b", "packages": [ { "name": "aws/aws-crt-php", @@ -2283,16 +2283,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.15.1", + "version": "7.15.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f" + "reference": "744101956d78b7c1384d0cbf379db13e859167bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", - "reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", + "reference": "744101956d78b7c1384d0cbf379db13e859167bf", "shasum": "" }, "require": { @@ -2391,7 +2391,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.1" + "source": "https://github.com/guzzle/guzzle/tree/7.15.2" }, "funding": [ { @@ -2407,7 +2407,7 @@ "type": "tidelift" } ], - "time": "2026-07-18T11:23:11+00:00" + "time": "2026-07-26T23:23:20+00:00" }, { "name": "guzzlehttp/promises", @@ -2896,6 +2896,94 @@ ], "time": "2020-06-13T08:05:20+00:00" }, + { + "name": "laminas/laminas-diactoros", + "version": "3.8.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-diactoros.git", + "reference": "60c182916b2749480895601649563970f3f12ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/60c182916b2749480895601649563970f3f12ec4", + "reference": "60c182916b2749480895601649563970f3f12ec4", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "conflict": { + "amphp/amp": "<2.6.4" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.1 || ^2.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-dom": "*", + "ext-gd": "*", + "ext-libxml": "*", + "http-interop/http-factory-tests": "^2.2.0", + "laminas/laminas-coding-standard": "~3.1.0", + "php-http/psr7-integration-tests": "^1.4.0", + "phpunit/phpunit": "^10.5.36", + "psalm/plugin-phpunit": "^0.19.5", + "vimeo/psalm": "^6.13" + }, + "type": "library", + "extra": { + "laminas": { + "module": "Laminas\\Diactoros", + "config-provider": "Laminas\\Diactoros\\ConfigProvider" + } + }, + "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], + "psr-4": { + "Laminas\\Diactoros\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "PSR HTTP Message implementations", + "homepage": "https://laminas.dev", + "keywords": [ + "http", + "laminas", + "psr", + "psr-17", + "psr-7" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-diactoros/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-diactoros/issues", + "rss": "https://github.com/laminas/laminas-diactoros/releases.atom", + "source": "https://github.com/laminas/laminas-diactoros" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2025-10-12T15:31:36+00:00" + }, { "name": "laravel/framework", "version": "v13.13.0", @@ -3200,6 +3288,95 @@ }, "time": "2026-06-03T15:11:37+00:00" }, + { + "name": "laravel/octane", + "version": "v2.17.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/octane.git", + "reference": "058ae4d7109eed40836dc42960f9388b9bf71f73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/octane/zipball/058ae4d7109eed40836dc42960f9388b9bf71f73", + "reference": "058ae4d7109eed40836dc42960f9388b9bf71f73", + "shasum": "" + }, + "require": { + "laminas/laminas-diactoros": "^3.0", + "laravel/framework": "^10.10.1|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "nesbot/carbon": "^2.66.0|^3.0", + "php": "^8.1.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0|^8.0" + }, + "conflict": { + "spiral/roadrunner": "<2023.1.0", + "spiral/roadrunner-cli": "<2.6.0", + "spiral/roadrunner-http": "<3.3.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.6.1", + "inertiajs/inertia-laravel": "^1.3.2|^2.0", + "laravel/scout": "^10.2.1", + "laravel/socialite": "^5.6.1", + "livewire/livewire": "^2.12.3|^3.0", + "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", + "orchestra/testbench": "^8.21|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.1.7", + "phpunit/phpunit": "^10.4|^11.5|^12.0|^13.0", + "spiral/roadrunner-cli": "^2.6.0", + "spiral/roadrunner-http": "^3.3.0" + }, + "bin": [ + "bin/roadrunner-worker", + "bin/swoole-server" + ], + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Octane": "Laravel\\Octane\\Facades\\Octane" + }, + "providers": [ + "Laravel\\Octane\\OctaneServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Octane\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Supercharge your Laravel application's performance.", + "keywords": [ + "frankenphp", + "laravel", + "octane", + "roadrunner", + "swoole" + ], + "support": { + "issues": "https://github.com/laravel/octane/issues", + "source": "https://github.com/laravel/octane" + }, + "time": "2026-06-04T09:05:08+00:00" + }, { "name": "laravel/pail", "version": "v1.2.6", @@ -4008,16 +4185,16 @@ }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", "shasum": "" }, "require": { @@ -4039,8 +4216,8 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", @@ -4054,7 +4231,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.10-dev" } }, "autoload": { @@ -4111,7 +4288,7 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-08-03T13:42:31+00:00" }, { "name": "league/config", @@ -16292,16 +16469,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.5", + "version": "3.13.6", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", "shasum": "" }, "require": { @@ -16367,7 +16544,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-04T16:30:35+00:00" + "time": "2026-08-06T00:17:32+00:00" }, { "name": "staabm/side-effects-detector", diff --git a/config/app.php b/config/app.php index 2d3855b971..b3c5891f98 100644 --- a/config/app.php +++ b/config/app.php @@ -32,6 +32,9 @@ // The timezone for the application 'timezone' => env('APP_TIMEZONE', 'America/Los_Angeles'), + // The timezone for the anonymous user + 'anonymous_user_timezone' => env('ANONYMOUS_USER_TIMEZONE', 'UTC'), + // The time format for the application 'dateformat' => env('DATE_FORMAT', 'm/d/Y h:i A'), diff --git a/config/filesystems.php b/config/filesystems.php index 809b4718f7..3fa45d5637 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -136,6 +136,13 @@ // Others declared in packages // - translations - package-translations // - 'filesystems.disks.install' configured on the fly + + 'saved_search_advanced_configuration' => [ + 'driver' => 'local', + 'root' => storage_path('saved_search_advanced_configuration'), + 'url' => env('APP_URL') . '/storage/saved_search_advanced_configuration', + 'visibility' => 'private', + ], ], /* diff --git a/config/mail.php b/config/mail.php index d78ea38d62..1b524c46c7 100644 --- a/config/mail.php +++ b/config/mail.php @@ -86,6 +86,10 @@ 'transport' => 'array', ], + 'microsoft_graph' => [ + 'transport' => 'microsoft_graph', + ], + 'failover' => [ 'transport' => 'failover', 'mailers' => [ diff --git a/config/octane.php b/config/octane.php new file mode 100644 index 0000000000..598df4b7c6 --- /dev/null +++ b/config/octane.php @@ -0,0 +1,232 @@ + env('OCTANE_SERVER', 'roadrunner'), + + /* + |-------------------------------------------------------------------------- + | Force HTTPS + |-------------------------------------------------------------------------- + | + | When this configuration value is set to "true", Octane will inform the + | framework that all absolute links must be generated using the HTTPS + | protocol. Otherwise your links may be generated using plain HTTP. + | + */ + + 'https' => env('OCTANE_HTTPS', false), + + /* + |-------------------------------------------------------------------------- + | Octane Listeners + |-------------------------------------------------------------------------- + | + | All of the event listeners for Octane's events are defined below. These + | listeners are responsible for resetting your application's state for + | the next request. You may even add your own listeners to the list. + | + */ + + 'listeners' => [ + WorkerStarting::class => [ + EnsureUploadedFilesAreValid::class, + EnsureUploadedFilesCanBeMoved::class, + ], + + RequestReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + ...Octane::prepareApplicationForNextRequest(), + // + ], + + RequestHandled::class => [ + // + ], + + RequestTerminated::class => [ + // FlushUploadedFiles::class, + ], + + TaskReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TaskTerminated::class => [ + // + ], + + TickReceived::class => [ + ...Octane::prepareApplicationForNextOperation(), + // + ], + + TickTerminated::class => [ + // + ], + + OperationTerminated::class => [ + FlushOnce::class, + FlushTemporaryContainerInstances::class, + // DisconnectFromDatabases::class, + // CollectGarbage::class, + ], + + WorkerErrorOccurred::class => [ + ReportException::class, + StopWorkerIfNecessary::class, + ], + + WorkerStopping::class => [ + CloseMonologHandlers::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Warm / Flush Bindings + |-------------------------------------------------------------------------- + | + | The bindings listed below will either be pre-warmed when a worker boots + | or they will be flushed before every new request. Flushing a binding + | will force the container to resolve that binding again when asked. + | + */ + + 'warm' => [ + ...Octane::defaultServicesToWarm(), + // Services to pre-resolve on worker start + ProcessMaker\Managers\PackageManager::class, + ProcessMaker\Managers\LoginManager::class, + ProcessMaker\Managers\IndexManager::class, + ], + + 'flush' => [ + // Services with mutable state that must be recreated per request + ProcessMaker\Models\AnonymousUser::class, + ProcessMaker\ImportExport\Extension::class, + ProcessMaker\ImportExport\SignalHelper::class, + ProcessMaker\Managers\MenuManager::class, + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Tables + |-------------------------------------------------------------------------- + | + | While using Swoole, you may define additional tables as required by the + | application. These tables can be used to store data that needs to be + | quickly accessed by other workers on the particular Swoole server. + | + */ + + 'tables' => [ + 'example:1000' => [ + 'name' => 'string:1000', + 'votes' => 'int', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Octane Swoole Cache Table + |-------------------------------------------------------------------------- + | + | While using Swoole, you may leverage the Octane cache, which is powered + | by a Swoole table. You may set the maximum number of rows as well as + | the number of bytes per row using the configuration options below. + | + */ + + 'cache' => [ + 'rows' => 1000, + 'bytes' => 10000, + ], + + /* + |-------------------------------------------------------------------------- + | File Watching + |-------------------------------------------------------------------------- + | + | The following list of files and directories will be watched when using + | the --watch option offered by Octane. If any of the directories and + | files are changed, Octane will automatically reload your workers. + | + */ + + 'watch' => [ + 'app', + 'bootstrap', + 'config/**/*.php', + 'database/**/*.php', + 'public/**/*.php', + 'resources/**/*.php', + 'routes', + 'composer.lock', + '.env', + ], + + /* + |-------------------------------------------------------------------------- + | Garbage Collection Threshold + |-------------------------------------------------------------------------- + | + | When executing long-lived PHP scripts such as Octane, memory can build + | up before being cleared by PHP. You can force Octane to run garbage + | collection if your application consumes this amount of megabytes. + | + */ + + 'garbage' => 50, + + /* + |-------------------------------------------------------------------------- + | Maximum Execution Time + |-------------------------------------------------------------------------- + | + | The following setting configures the maximum execution time for requests + | being handled by Octane. You may set this value to 0 to indicate that + | there isn't a specific time limit on Octane request execution time. + | + */ + + 'max_execution_time' => 30, + +]; diff --git a/config/services.php b/config/services.php index fde93f96cd..bcf0b7b9c8 100644 --- a/config/services.php +++ b/config/services.php @@ -46,13 +46,6 @@ 'template_categories' => env('DEFAULT_TEMPLATE_CATEGORIES', 'accounting-and-finance,customer-success,human-resources,marketing-and-sales,operations,it'), ], - 'guided_templates_github' => [ - 'base_url' => 'https://raw.githubusercontent.com/processmaker/', - 'template_repo' => env('GUIDED_TEMPLATE_REPO', 'wizard-templates'), - 'template_branch' => env('GUIDED_TEMPLATE_BRANCH', '2023-winter'), - 'template_categories' => env('GUIDED_TEMPLATE_CATEGORIES', 'all'), - ], - 'screen_templates_github' => [ 'base_url' => 'https://raw.githubusercontent.com/processmaker/', 'template_repo' => env('SCREEN_TEMPLATE_REPO', 'screen-templates'), diff --git a/database/factories/ProcessMaker/Models/WizardTemplateFactory.php b/database/factories/ProcessMaker/Models/WizardTemplateFactory.php deleted file mode 100644 index 6860dabca3..0000000000 --- a/database/factories/ProcessMaker/Models/WizardTemplateFactory.php +++ /dev/null @@ -1,26 +0,0 @@ - $this->faker->uuid, - 'unique_template_id' => $this->faker->uuid, - 'process_template_id' => null, - 'name' => $this->faker->unique()->name(), - 'description' => $this->faker->unique()->name(), - 'media_collection' => $this->faker->unique()->name(), - 'template_details' => '{}', - 'helper_process_id' => Process::factory()->create()->id, - ]; - } -} diff --git a/database/migrations/2023_11_27_215150_create_wizard_templates_table.php b/database/migrations/2023_11_27_215150_create_wizard_templates_table.php deleted file mode 100644 index d7a34c4789..0000000000 --- a/database/migrations/2023_11_27_215150_create_wizard_templates_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->uuid('uuid')->unique(); - $table->unsignedBigInteger('process_template_id')->nullable(); - $table->unsignedInteger('process_id'); - $table->string('media_collection')->nullable(); - $table->timestamps(); - - // Foreign keys - $table->foreign('process_id') - ->references('id') - ->on('processes') - ->onDelete('cascade') - ->constrained('processes'); - }); - } - - public function down() - { - Schema::dropIfExists('wizard_templates'); - } -} diff --git a/database/migrations/2023_12_04_210217_modify_wizard_templates_table.php b/database/migrations/2023_12_04_210217_modify_wizard_templates_table.php deleted file mode 100644 index 776d4e4b20..0000000000 --- a/database/migrations/2023_12_04_210217_modify_wizard_templates_table.php +++ /dev/null @@ -1,55 +0,0 @@ -renameColumn('process_id', 'helper_process_id'); - $table->string('name')->after('uuid'); - $table->string('description')->after('name'); - $table->json('template_details')->after('description'); - $table->unsignedInteger('config_collection_id')->nullable()->after('template_details'); - }); - - // Change the foreign key reference to helper_process_id - Schema::table('wizard_templates', function (Blueprint $table) { - $table->foreign('helper_process_id') - ->references('id') - ->on('processes') - ->onDelete('cascade') - ->constrained('processes'); - }); - - // Add the foreign key reference to process_template_id - Schema::table('wizard_templates', function (Blueprint $table) { - $table->foreign('process_template_id') - ->references('id') - ->on('process_templates') - ->onDelete('cascade') - ->constrained('process_templates'); - }); - } - - public function down() - { - // Reverse the changes in the down method if needed - Schema::table('wizard_templates', function (Blueprint $table) { - $table->renameColumn('helper_process_id', 'process_id'); - - // Reverse the foreign key changes - $table->dropForeign(['helper_process_id']); - $table->dropForeign(['process_template_id']); - - $table->dropColumn('name'); - $table->dropColumn('description'); - $table->dropColumn('template_details'); - $table->dropColumn('config_collection_id'); - }); - } -} diff --git a/database/migrations/2024_01_16_181933_add_unique_template_id_column_to_wizard_templates_table.php b/database/migrations/2024_01_16_181933_add_unique_template_id_column_to_wizard_templates_table.php deleted file mode 100644 index 8af6d8371b..0000000000 --- a/database/migrations/2024_01_16_181933_add_unique_template_id_column_to_wizard_templates_table.php +++ /dev/null @@ -1,27 +0,0 @@ -string('unique_template_id')->unique()->after('uuid'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('wizard_templates', function (Blueprint $table) { - $table->dropColumn('unique_template_id'); - }); - } -}; diff --git a/database/migrations/2026_07_13_145000_add_created_at_to_notifications_notifiable_index.php b/database/migrations/2026_07_13_145000_add_created_at_to_notifications_notifiable_index.php new file mode 100644 index 0000000000..9245d4c0f9 --- /dev/null +++ b/database/migrations/2026_07_13_145000_add_created_at_to_notifications_notifiable_index.php @@ -0,0 +1,48 @@ +dropIndex(self::INDEX_NAME); + }); + } + + if (!Schema::hasIndex(self::TABLE, self::INDEX_NAME)) { + DB::statement( + 'CREATE INDEX ' . self::INDEX_NAME . ' ON ' . self::TABLE + . ' (notifiable_type, notifiable_id, read_at, created_at DESC)' + ); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasIndex(self::TABLE, self::INDEX_NAME)) { + Schema::table(self::TABLE, function (Blueprint $table) { + $table->dropIndex(self::INDEX_NAME); + }); + } + } +}; diff --git a/database/migrations/2026_07_27_000000_add_saved_search_indexes.php b/database/migrations/2026_07_27_000000_add_saved_search_indexes.php new file mode 100644 index 0000000000..765c2693a1 --- /dev/null +++ b/database/migrations/2026_07_27_000000_add_saved_search_indexes.php @@ -0,0 +1,101 @@ +addIndex( + 'process_request_tokens', + 'process_request_tokens_prt_task_name_id', + ['element_type', 'element_name', 'process_id', 'user_id', 'process_request_id'] + ); + $this->addIndex( + 'process_request_tokens', + 'process_request_tokens_prt_type_id_proc', + ['element_type', 'process_id', 'user_id', 'process_request_id'] + ); + $this->addIndex( + 'process_request_tokens', + 'process_request_tokens_prt_self_service_status_user', + ['is_self_service', 'status', 'user_id', 'id'] + ); + $this->addIndex( + 'process_request_tokens', + 'process_request_tokens_processid_elem_stat_self_user_elname', + ['process_id', 'element_type', 'status', 'is_self_service', 'user_id', 'element_name'] + ); + + $this->addIndex( + 'category_assignments', + 'category_assignments_assignabletyid_categorytyid', + ['assignable_type', 'assignable_id', 'category_type', 'category_id'] + ); + + $this->addIndex( + 'process_versions', + 'process_versions_processid_draft', + ['process_id', 'draft'] + ); + + // Leading `id` removed from the original proposal because the primary key + // already covers it and InnoDB secondary indexes include it automatically. + $this->addIndex( + 'processes', + 'process_idx_proc_deletedat_categoryid', + ['deleted_at', 'process_category_id'] + ); + + // Tail `id` removed for the same reason. + $this->addIndex( + 'process_categories', + 'process_categories_issystem_id', + ['is_system'] + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $this->dropIndex('process_request_tokens', 'process_request_tokens_prt_task_name_id'); + $this->dropIndex('process_request_tokens', 'process_request_tokens_prt_type_id_proc'); + $this->dropIndex('process_request_tokens', 'process_request_tokens_prt_self_service_status_user'); + $this->dropIndex('process_request_tokens', 'process_request_tokens_processid_elem_stat_self_user_elname'); + $this->dropIndex('category_assignments', 'category_assignments_assignabletyid_categorytyid'); + $this->dropIndex('process_versions', 'process_versions_processid_draft'); + $this->dropIndex('processes', 'process_idx_proc_deletedat_categoryid'); + $this->dropIndex('process_categories', 'process_categories_issystem_id'); + } + + private function addIndex(string $table, string $name, array $columns): void + { + if (Schema::hasIndex($table, $name)) { + return; + } + + Schema::table($table, function (Blueprint $table) use ($name, $columns) { + $table->index($columns, $name); + }); + } + + private function dropIndex(string $table, string $name): void + { + if (!Schema::hasIndex($table, $name)) { + return; + } + + try { + DB::statement("ALTER TABLE `{$table}` DROP INDEX `{$name}`"); + } catch (Exception $e) { + // Ignore so rollback continues (e.g. index required by a foreign key). + } + } +}; diff --git a/package-lock.json b/package-lock.json index b751211351..661ddeb413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@processmaker/processmaker", - "version": "2026.12.3", + "version": "2026.13.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@processmaker/processmaker", - "version": "2026.12.3", + "version": "2026.13.2", "hasInstallScript": true, "license": "ISC", "dependencies": { @@ -20,10 +20,10 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^0.1.9", "@panter/vue-i18next": "^0.15.2", - "@processmaker/modeler": "1.69.42", + "@processmaker/modeler": "1.69.43", "@processmaker/processmaker-bpmn-moddle": "0.16.1", - "@processmaker/screen-builder": "3.8.36", - "@processmaker/vue-form-elements": "0.65.9", + "@processmaker/screen-builder": "3.8.37", + "@processmaker/vue-form-elements": "0.65.10", "@processmaker/vue-multiselect": "2.3.2", "@tinymce/tinymce-vue": "2.0.0", "axios": "^0.27.2", @@ -2279,31 +2279,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.6" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -2311,9 +2311,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, "node_modules/@fontsource/poppins": { @@ -3748,9 +3748,9 @@ "license": "MIT" }, "node_modules/@processmaker/modeler": { - "version": "1.69.42", - "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.42.tgz", - "integrity": "sha512-t/0PL1ntTBng6FmL9aFYG2ioLZcGKPwK69LlwFcjWATU/PA/LUESRz05hA5InvAOjm2W1zwkxrGezLRxXGkN/Q==", + "version": "1.69.43", + "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.43.tgz", + "integrity": "sha512-5b2WvbQw49eoHUADmqAi1/Fxkndc6n8vAqMnr1bHTph6LEgVUzn4C2UyGgqMz+hlldhrBGEKw/XEtePXnmUxTQ==", "dependencies": { "@babel/plugin-proposal-private-methods": "^7.12.1", "@fortawesome/fontawesome-free": "^5.11.2", @@ -3758,8 +3758,8 @@ "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/vue-fontawesome": "^0.1.8", - "@processmaker/screen-builder": "3.8.36", - "@processmaker/vue-form-elements": "0.65.9", + "@processmaker/screen-builder": "3.8.37", + "@processmaker/vue-form-elements": "0.65.10", "@processmaker/vue-multiselect": "2.3.2", "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.4", @@ -3932,9 +3932,9 @@ } }, "node_modules/@processmaker/screen-builder": { - "version": "3.8.36", - "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.36.tgz", - "integrity": "sha512-jubdmvBiG+5oreeY2E90Cnk+AjF7kNJ3V8i2Lz7QCSx8/47cXtoBAnGmctXiKkG+R96TGAA3Vyxh+3AC7iDBAQ==", + "version": "3.8.37", + "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.37.tgz", + "integrity": "sha512-KwAAiXa/7x9tp/eYexH7BAdCbjCyEUPng6Mg+ctEIHz1XTUai8bkSGWmR2JOcDSNjrYCPrTSx172qU5E7EVzxg==", "dependencies": { "@chantouchsek/validatorjs": "1.2.3", "@storybook/addon-docs": "^7.6.13", @@ -3959,7 +3959,7 @@ }, "peerDependencies": { "@panter/vue-i18next": "^0.15.0", - "@processmaker/vue-form-elements": "0.65.9", + "@processmaker/vue-form-elements": "0.65.10", "i18next": "^15.0.8", "vue": "^2.6.12", "vuex": "^3.1.1" @@ -4008,9 +4008,9 @@ "license": "MIT" }, "node_modules/@processmaker/vue-form-elements": { - "version": "0.65.9", - "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.9.tgz", - "integrity": "sha512-h1y6TlmQL+NIJ2iGjDd6nMQ7ex3SYQuLfh2EMM0XhDKpFJ+UCjWCSaxLqPc3WMRKBquXoQkN1k+O9zi2Yc3L1w==", + "version": "0.65.10", + "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.10.tgz", + "integrity": "sha512-uPemEhLPPx2uN8wxktGSjMzmZtzYLIRHsBFoGv/OozOobtrx4iw0h7zkb1PcB84bHqh3Y0Xx/EagENJ/nltnPQ==", "license": "MIT", "dependencies": { "@chantouchsek/validatorjs": "1.2.3", @@ -4412,18 +4412,18 @@ } }, "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.15.tgz", - "integrity": "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-toggle-group": "1.1.15" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" }, "peerDependencies": { "@types/react": "*", @@ -4441,15 +4441,15 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/primitive": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", - "integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", "license": "MIT" }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4462,9 +4462,9 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-context": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", - "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4477,9 +4477,9 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4492,12 +4492,12 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4510,12 +4510,12 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "license": "MIT", "dependencies": { - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -4533,22 +4533,22 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", - "integrity": "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4566,15 +4566,15 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", - "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" }, "peerDependencies": { "@types/react": "*", @@ -4592,12 +4592,12 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-separator": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" }, "peerDependencies": { "@types/react": "*", @@ -4615,12 +4615,12 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.3" + "@radix-ui/react-compose-refs": "1.1.5" }, "peerDependencies": { "@types/react": "*", @@ -4633,18 +4633,18 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.15.tgz", - "integrity": "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4662,14 +4662,14 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-toggle": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.14.tgz", - "integrity": "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "peerDependencies": { "@types/react": "*", @@ -4687,9 +4687,9 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4702,13 +4702,14 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4721,9 +4722,9 @@ } }, "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4773,12 +4774,12 @@ } }, "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "peerDependencies": { "@types/react": "*", @@ -4791,9 +4792,9 @@ } }, "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -4825,9 +4826,9 @@ } }, "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "license": "MIT", "peerDependencies": { "@types/react": "*", @@ -5149,9 +5150,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", "license": "MIT" }, "node_modules/@sindresorhus/is": { @@ -5364,9 +5365,9 @@ } }, "node_modules/@storybook/core-common/node_modules/brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6097,9 +6098,9 @@ } }, "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", "license": "MIT" }, "node_modules/@types/mdx": { @@ -18493,9 +18494,9 @@ } }, "node_modules/react-colorful": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.7.0.tgz", - "integrity": "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", + "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", "license": "MIT", "peerDependencies": { "react": ">=16.8.0", @@ -18767,9 +18768,9 @@ } }, "node_modules/recast": { - "version": "0.23.12", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", - "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", + "version": "0.23.19", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.19.tgz", + "integrity": "sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==", "license": "MIT", "dependencies": { "ast-types": "^0.16.1", @@ -20908,9 +20909,9 @@ } }, "node_modules/synchronous-promise": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", - "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==", + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.18.tgz", + "integrity": "sha512-4EEtGWYLkSoy/DjlKpHR6LT2AjmORt8HM+CUCSdKiKyuhL3PFBksgcTibSOj7JIoCvuqRVnZ7P9QFBMXR5kW6Q==", "license": "BSD-3-Clause" }, "node_modules/tailwindcss": { @@ -25020,34 +25021,34 @@ "dev": true }, "@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", "requires": { - "@floating-ui/utils": "^0.2.11" + "@floating-ui/utils": "^0.2.12" } }, "@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", "requires": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", "requires": { - "@floating-ui/dom": "^1.7.6" + "@floating-ui/dom": "^1.8.0" } }, "@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==" + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" }, "@fontsource/poppins": { "version": "5.0.8", @@ -26007,9 +26008,9 @@ "dev": true }, "@processmaker/modeler": { - "version": "1.69.42", - "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.42.tgz", - "integrity": "sha512-t/0PL1ntTBng6FmL9aFYG2ioLZcGKPwK69LlwFcjWATU/PA/LUESRz05hA5InvAOjm2W1zwkxrGezLRxXGkN/Q==", + "version": "1.69.43", + "resolved": "https://registry.npmjs.org/@processmaker/modeler/-/modeler-1.69.43.tgz", + "integrity": "sha512-5b2WvbQw49eoHUADmqAi1/Fxkndc6n8vAqMnr1bHTph6LEgVUzn4C2UyGgqMz+hlldhrBGEKw/XEtePXnmUxTQ==", "requires": { "@babel/plugin-proposal-private-methods": "^7.12.1", "@fortawesome/fontawesome-free": "^5.11.2", @@ -26017,8 +26018,8 @@ "@fortawesome/free-brands-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/vue-fontawesome": "^0.1.8", - "@processmaker/screen-builder": "3.8.36", - "@processmaker/vue-form-elements": "0.65.9", + "@processmaker/screen-builder": "3.8.37", + "@processmaker/vue-form-elements": "0.65.10", "@processmaker/vue-multiselect": "2.3.2", "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.4", @@ -26133,9 +26134,9 @@ } }, "@processmaker/screen-builder": { - "version": "3.8.36", - "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.36.tgz", - "integrity": "sha512-jubdmvBiG+5oreeY2E90Cnk+AjF7kNJ3V8i2Lz7QCSx8/47cXtoBAnGmctXiKkG+R96TGAA3Vyxh+3AC7iDBAQ==", + "version": "3.8.37", + "resolved": "https://registry.npmjs.org/@processmaker/screen-builder/-/screen-builder-3.8.37.tgz", + "integrity": "sha512-KwAAiXa/7x9tp/eYexH7BAdCbjCyEUPng6Mg+ctEIHz1XTUai8bkSGWmR2JOcDSNjrYCPrTSx172qU5E7EVzxg==", "requires": { "@chantouchsek/validatorjs": "1.2.3", "@storybook/addon-docs": "^7.6.13", @@ -26189,9 +26190,9 @@ } }, "@processmaker/vue-form-elements": { - "version": "0.65.9", - "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.9.tgz", - "integrity": "sha512-h1y6TlmQL+NIJ2iGjDd6nMQ7ex3SYQuLfh2EMM0XhDKpFJ+UCjWCSaxLqPc3WMRKBquXoQkN1k+O9zi2Yc3L1w==", + "version": "0.65.10", + "resolved": "https://registry.npmjs.org/@processmaker/vue-form-elements/-/vue-form-elements-0.65.10.tgz", + "integrity": "sha512-uPemEhLPPx2uN8wxktGSjMzmZtzYLIRHsBFoGv/OozOobtrx4iw0h7zkb1PcB84bHqh3Y0Xx/EagENJ/nltnPQ==", "requires": { "@chantouchsek/validatorjs": "1.2.3", "@tinymce/tinymce-vue": "2.0.0", @@ -26401,150 +26402,151 @@ } }, "@radix-ui/react-toolbar": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.15.tgz", - "integrity": "sha512-t/iEuVjUnXXtrsGK40AA43uIx37sn3AqZ7oAVnPICK6lFJP6dzMzWR3U9b6eCfFjb6wtSEqkJ9Rn9xDjiOx20g==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.19.tgz", + "integrity": "sha512-Ph0IvtYw4VB12ZnZg+YtrGs8yJQsnizwo/zu0R4Y/nWugtJzA7Pg1eWeuDR9+LSqn+xjamss+UOSOJJJ4gx8jw==", "requires": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-separator": "1.1.11", - "@radix-ui/react-toggle-group": "1.1.15" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-separator": "1.1.15", + "@radix-ui/react-toggle-group": "1.1.19" }, "dependencies": { "@radix-ui/primitive": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.5.tgz", - "integrity": "sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==" }, "@radix-ui/react-compose-refs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", - "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", "requires": {} }, "@radix-ui/react-context": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.0.tgz", - "integrity": "sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", "requires": {} }, "@radix-ui/react-direction": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", - "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", "requires": {} }, "@radix-ui/react-id": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", - "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", "requires": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" } }, "@radix-ui/react-primitive": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz", - "integrity": "sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==", + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", "requires": { - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-slot": "1.3.3" } }, "@radix-ui/react-roving-focus": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz", - "integrity": "sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==", - "requires": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-collection": "1.1.12", - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-id": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-callback-ref": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.2.3", - "@radix-ui/react-use-is-hydrated": "0.1.1", - "@radix-ui/react-use-layout-effect": "1.1.2" + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "requires": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.1.4" }, "dependencies": { "@radix-ui/react-collection": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.12.tgz", - "integrity": "sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", "requires": { - "@radix-ui/react-compose-refs": "1.1.3", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-slot": "1.3.0" + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" } } } }, "@radix-ui/react-separator": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.11.tgz", - "integrity": "sha512-jRhe86+8PF7VZ1u14eOWVOuh2BuAhALg/FT1VcMC4OHedMTRUazDnDlKTt+yxo5cRNKHMfmvZ4sSQtWDeMV4CQ==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", "requires": { - "@radix-ui/react-primitive": "2.1.7" + "@radix-ui/react-primitive": "2.1.10" } }, "@radix-ui/react-slot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", - "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", "requires": { - "@radix-ui/react-compose-refs": "1.1.3" + "@radix-ui/react-compose-refs": "1.1.5" } }, "@radix-ui/react-toggle-group": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.15.tgz", - "integrity": "sha512-gIC5Q+Xljg7lmUdzSuDoy0t97yZn1sZl00Ra37ZvKrYdWnQLU6sWLd09yG8cIB9jUAlQfHgJ2ACAG00MFwsqSQ==", - "requires": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-context": "1.2.0", - "@radix-ui/react-direction": "1.1.2", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-roving-focus": "1.1.15", - "@radix-ui/react-toggle": "1.1.14", - "@radix-ui/react-use-controllable-state": "1.2.3" + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.19.tgz", + "integrity": "sha512-OtnwuSVjd1Ofi+AdnvhsjQdyuhCDwYs1w9RyB5BN/OavXOVQo42SYqQjwUnbPnaiPFBpQ9aX70dWeee+v2oBLA==", + "requires": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-toggle": "1.1.18", + "@radix-ui/react-use-controllable-state": "1.2.6" }, "dependencies": { "@radix-ui/react-toggle": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.14.tgz", - "integrity": "sha512-QI/hB65XKWACA66P64A+aHxtLUgHJeJLkaQa+awUNXT6T3swndtY5DojeHA+vldrTspMTtFBd7HfZ9QGbM1Qrw==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.18.tgz", + "integrity": "sha512-7lonPlKfSacd20GlOBx2ltuVKz9oqWYZz+oMQyOltw6t1y2nyftj2ZmwwUHYn49kqfDWcp8dNZm5NgV+5Z+mug==", "requires": { - "@radix-ui/primitive": "1.1.5", - "@radix-ui/react-primitive": "2.1.7", - "@radix-ui/react-use-controllable-state": "1.2.3" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" } } } }, "@radix-ui/react-use-callback-ref": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", - "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", "requires": {} }, "@radix-ui/react-use-controllable-state": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", - "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", "requires": { - "@radix-ui/react-use-effect-event": "0.0.3", - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" } }, "@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "requires": {} } } @@ -26567,17 +26569,17 @@ } }, "@radix-ui/react-use-effect-event": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", - "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", "requires": { - "@radix-ui/react-use-layout-effect": "1.1.2" + "@radix-ui/react-use-layout-effect": "1.1.4" }, "dependencies": { "@radix-ui/react-use-layout-effect": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", - "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", "requires": {} } } @@ -26592,9 +26594,9 @@ } }, "@radix-ui/react-use-is-hydrated": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", - "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", "requires": {} }, "@radix-ui/react-use-layout-effect": { @@ -26740,9 +26742,9 @@ "optional": true }, "@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==" + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==" }, "@sindresorhus/is": { "version": "4.6.0", @@ -26897,9 +26899,9 @@ } }, "brace-expansion": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", - "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "requires": { "balanced-match": "^1.0.0" } @@ -27494,9 +27496,9 @@ } }, "@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==" + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==" }, "@types/mdx": { "version": "2.0.14", @@ -36715,9 +36717,9 @@ } }, "react-colorful": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.7.0.tgz", - "integrity": "sha512-fuesYIemttah97XmsIHmz4OORDHiSFzyc9HMAIrCHJou2jaRQmL8cFJ76K4zQhhj8jzwOBlOi4BaGTjjOZCfTg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.8.0.tgz", + "integrity": "sha512-Wy9OzPfjSN9bF12OB8N7UQvlsZ0I+7wHxpN+bV5BjNQGxOj6IiwkRjevJK9yOBjJWGQvAaf1OXtn8rUeEatAng==", "requires": {} }, "react-dom": { @@ -36909,9 +36911,9 @@ } }, "recast": { - "version": "0.23.12", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", - "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", + "version": "0.23.19", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.19.tgz", + "integrity": "sha512-T98lym7kH+pnZmRaD8yDRdaNqyUbwnbEBx0MuchrzMFOEMray4AO3ZJoTUZ5r78Ao78X/OhzW0DL8GB85w/I2w==", "requires": { "ast-types": "^0.16.1", "esprima": "~4.0.0", @@ -38543,9 +38545,9 @@ } }, "synchronous-promise": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", - "integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.18.tgz", + "integrity": "sha512-4EEtGWYLkSoy/DjlKpHR6LT2AjmORt8HM+CUCSdKiKyuhL3PFBksgcTibSOj7JIoCvuqRVnZ7P9QFBMXR5kW6Q==" }, "tailwindcss": { "version": "3.4.13", diff --git a/package.json b/package.json index 42cd7070a1..d861d535c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@processmaker/processmaker", - "version": "2026.12.3", + "version": "2026.13.2", "description": "ProcessMaker 4", "author": "DevOps ", "license": "ISC", @@ -61,10 +61,10 @@ "@fortawesome/free-solid-svg-icons": "^5.15.1", "@fortawesome/vue-fontawesome": "^0.1.9", "@panter/vue-i18next": "^0.15.2", - "@processmaker/modeler": "1.69.42", + "@processmaker/modeler": "1.69.43", "@processmaker/processmaker-bpmn-moddle": "0.16.1", - "@processmaker/screen-builder": "3.8.36", - "@processmaker/vue-form-elements": "0.65.9", + "@processmaker/screen-builder": "3.8.37", + "@processmaker/vue-form-elements": "0.65.10", "@processmaker/vue-multiselect": "2.3.2", "@tinymce/tinymce-vue": "2.0.0", "axios": "^0.27.2", diff --git a/phpunit.xml b/phpunit.xml index 8c16d0ae0e..6d8310e71c 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -7,7 +7,10 @@ extensionsDirectory="tests/Extensions" displayDetailsOnAllIssues="true" > - + + + + tests/Feature tests/Managers @@ -24,6 +27,9 @@ + + + diff --git a/public/images/wizard-icon.svg b/public/images/wizard-icon.svg deleted file mode 100644 index a2be69d748..0000000000 --- a/public/images/wizard-icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/public/images/wizard-template-icon.svg b/public/images/wizard-template-icon.svg deleted file mode 100644 index 9ea25abd12..0000000000 --- a/public/images/wizard-template-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/resources/img/processmaker-icon.svg b/resources/img/processmaker-icon.svg index 944136ba27..50a156dae0 100644 --- a/resources/img/processmaker-icon.svg +++ b/resources/img/processmaker-icon.svg @@ -1,11 +1,3 @@ - - - - - - - - - - - \ No newline at end of file + + + diff --git a/resources/img/wizard-icon.svg b/resources/img/wizard-icon.svg deleted file mode 100644 index a2be69d748..0000000000 --- a/resources/img/wizard-icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/resources/img/wizard-template-icon.svg b/resources/img/wizard-template-icon.svg deleted file mode 100644 index 9ea25abd12..0000000000 --- a/resources/img/wizard-template-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/resources/js/Mobile/FilterMobile.vue b/resources/js/Mobile/FilterMobile.vue index c7452454a0..1df7b30c56 100644 --- a/resources/js/Mobile/FilterMobile.vue +++ b/resources/js/Mobile/FilterMobile.vue @@ -269,7 +269,7 @@ export default { }, }; - diff --git a/resources/js/components/templates/WizardHelperProcessModal.vue b/resources/js/components/templates/WizardHelperProcessModal.vue deleted file mode 100644 index 5e141ccccd..0000000000 --- a/resources/js/components/templates/WizardHelperProcessModal.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/js/components/templates/WizardTemplateCard.vue b/resources/js/components/templates/WizardTemplateCard.vue deleted file mode 100644 index 06f3737ba3..0000000000 --- a/resources/js/components/templates/WizardTemplateCard.vue +++ /dev/null @@ -1,86 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/js/components/templates/WizardTemplateDetails.vue b/resources/js/components/templates/WizardTemplateDetails.vue deleted file mode 100644 index 28d39a1971..0000000000 --- a/resources/js/components/templates/WizardTemplateDetails.vue +++ /dev/null @@ -1,162 +0,0 @@ - - - - - \ No newline at end of file diff --git a/resources/js/components/templates/mixins/wizardHelperProcessModal.js b/resources/js/components/templates/mixins/wizardHelperProcessModal.js deleted file mode 100644 index a7be3a36a7..0000000000 --- a/resources/js/components/templates/mixins/wizardHelperProcessModal.js +++ /dev/null @@ -1,144 +0,0 @@ -export default { - data() { - return { - task: null, - currentUserId: null, - formData: {}, - importingProcessTemplate: false, - }; - }, - methods: { - getHelperProcessStartEvent(triggeredBy = null) { - if (triggeredBy === "wizard-details-modal") { - this.startEvents = this.template.process.start_events - .filter((event) => !event.eventDefinitions || event.eventDefinitions.length === 0); - this.helperProcessId = this.template.process.id; - - this.triggerHelperProcessStartEvent(); - } else if (this.wizardTemplateUuid !== null) { - ProcessMaker.apiClient.get(`wizard-templates/${this.wizardTemplateUuid}/get-helper-process`) - .then((response) => { - if (response.data) { - this.helperProcessId = response.data.helper_process_id; - this.startEvents = JSON.parse(response.data.start_events) - .filter((event) => !event.eventDefinitions || event.eventDefinitions.length === 0); - - this.triggerHelperProcessStartEvent(); - } - }); - } - }, - triggerHelperProcessStartEvent() { - const startEventId = this.startEvents[0].id; - const url = `/process_events/${this.helperProcessId}?event=${startEventId}`; - // Start the helper process - window.ProcessMaker.apiClient.post(url, { - process_launchpad_id: this.processLaunchpadId ? this.processLaunchpadId : null - }).then((response) => { - const processRequestId = response.data.id; - this.getFirstTask(processRequestId); - }).catch((error) => { - ProcessMaker.alert(error.message, "danger"); - }); - }, - getFirstTask(processRequestId) { - ProcessMaker.apiClient.get("tasks", { - params: { - page: 1, - include: "user,assignableUsers", - process_request_id: processRequestId, - status: "ACTIVE", - per_page: 10, - order_by: "due_at", - order_direction: "asc", - }, - }).then((response) => { - const [firstTask] = response.data.data; - if (firstTask) { - this.task = firstTask; - this.currentUserId = parseInt(document.head.querySelector("meta[name=\"user-id\"]").content, 10); - this.$bvModal.show("processWizard"); - this.showHelperProcess = true; - } else { - // No task found close modal - this.showHelperProcess = false; - this.close(); - } - }).catch((error) => { - ProcessMaker.alert(error.message, "danger"); - }); - }, - close() { - this.$bvModal.hide("processWizard"); - // Cancels the associated process request to prevent orphaned processes. - this.cancelHelperProcessRequest(); - }, - cancelHelperProcessRequest() { - const processRequestId = this.task.process_request.id; - ProcessMaker.apiClient.put(`requests/${processRequestId}`, { - status: "CANCELED", - }).then(() => { - this.showHelperProcess = false; - }).catch((error) => { - ProcessMaker.alert(error.message, "danger"); - }); - }, - taskUpdated(task) { - this.task = task; - }, - completed() { - if (!this.importingProcessTemplate && this.shouldImportProcessTemplate) { - this.importProcessTemplate(); - } else if (!this.importingProcessTemplate && !this.shouldImportProcessTemplate) { - this.showHelperProcess = false; - this.$bvModal.hide("processWizard"); - } - }, - submit(task) { - const { id: taskId } = task; - ProcessMaker.apiClient.put(`tasks/${taskId}`, { - status: "COMPLETED", - data: this.formData, - }).catch((error) => { - ProcessMaker.alert(error.message, "danger"); - }); - }, - importProcessTemplate() { - this.importingProcessTemplate = true; - ProcessMaker.apiClient.post(`template/create/process/${this.template.process_template_id}`, { - name: this.template.name, - description: this.template.description, - version: "1.0.0", // TODO: Wizards should have a versions property - process_category_id: this.template.process.process_category_id, - projects: null, - wizardTemplateUuid: this.template.uuid, - helperProcessRequestId: this.task.process_request_id, - }).then((response) => { - this.importingProcessTemplate = false; - if (response.data?.existingAssets) { - this.handleExistingAssets(response.data); - } else { - // redirect to the new process launchpad - window.location = `/process-browser/${response.data.processId}`; - } - }).catch((error) => { - ProcessMaker.alert(error.message, "danger"); - }); - }, - handleExistingAssets(data) { - // Use local storage to pass the data to the assets page. - const stateData = { - assets: JSON.stringify(data.existingAssets), - name: this.template.name, - responseId: data.id, - request: JSON.stringify(data.request), - redirectTo: "process-launchpad", - wizardTemplateUuid: this.template.uuid, - }; - localStorage.setItem("templateAssetsState", JSON.stringify(stateData)); - - // Redirect to the assets page. - window.location = "/template/assets"; - }, - }, -}; diff --git a/resources/js/processes-catalogue/components/CatalogueEmpty.vue b/resources/js/processes-catalogue/components/CatalogueEmpty.vue index 6e5a5994a3..5555e7b4ae 100644 --- a/resources/js/processes-catalogue/components/CatalogueEmpty.vue +++ b/resources/js/processes-catalogue/components/CatalogueEmpty.vue @@ -21,7 +21,7 @@ @@ -39,16 +39,13 @@ export default { components: { EmptySearch }, props: ["showEmpty", "isBookmarkEmpty"], methods: { - /** - * go to wizard templates section - */ - wizardLinkSelected() { + showTemplates() { window.ProcessMaker.EventBus.$emit( - "wizard-templates-selected", + "all-templates-selected", { - label: this.$t("Guided Templates"), + label: this.$t("All Templates"), selected: false, - id: "guided_templates", + id: "all_templates", }, ); }, diff --git a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue index 2b076d345a..fc491ef35c 100644 --- a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue +++ b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue @@ -154,15 +154,9 @@ export default { }; }, computed: { - createdFromWizardTemplate() { - return !!this.process?.properties?.wizardTemplateUuid; - }, isArchived() { return this.process?.status === "ARCHIVED"; }, - wizardTemplateUuid() { - return this.process?.properties?.wizardTemplateUuid; - }, }, mounted() { this.verifyDescription(); @@ -200,9 +194,6 @@ export default { activateReadMore() { this.readActivated = true; }, - getHelperProcess() { - this.$refs.wizardHelperProcessModal.getHelperProcessStartEvent(); - }, toggleInfo() { this.showProcessInfo = !this.showProcessInfo; this.$emit("toggle-info"); @@ -261,26 +252,10 @@ export default { .custom-class { margin-top: -13px; } -.wizard-link { - text-transform: none; -} -.wizard-container { - display: flex; - flex-direction: column; - align-items: center; -} -.wizard { - display: flex; - justify-items: end; - width: 294px; -} @media (width < 1200px) { .process-options { margin-top: 32px; } - .wizard { - width: 170px; - } } @media (1460px <= width < 1600px) { .col-pm-9 { diff --git a/resources/js/processes-catalogue/components/ProcessHeader.vue b/resources/js/processes-catalogue/components/ProcessHeader.vue index 89745b9a85..da85136b7e 100644 --- a/resources/js/processes-catalogue/components/ProcessHeader.vue +++ b/resources/js/processes-catalogue/components/ProcessHeader.vue @@ -42,22 +42,6 @@ -
-
- - - {{ $t('Re-run Wizard') }} - -
-
-
@@ -116,7 +91,6 @@ import EllipsisMenu from "../../components/shared/EllipsisMenu.vue"; import ellipsisMenuMixin from "../../components/shared/ellipsisMenuActions"; import Bookmark from "./Bookmark.vue"; import ProcessCounter from "./optionsMenu/ProcessCounter.vue"; -import WizardHelperProcessModal from "../../components/templates/WizardHelperProcessModal.vue"; export default { components: { @@ -124,7 +98,6 @@ export default { EllipsisMenu, Bookmark, ProcessCounter, - WizardHelperProcessModal, }, mixins: [ProcessesMixin, ellipsisMenuMixin], props: { @@ -140,10 +113,6 @@ export default { type: Boolean, default: false, }, - iconWizardTemplate: { - type: Boolean, - default: false, - }, }, data() { return { @@ -155,14 +124,6 @@ export default { mounted() { this.getStartEvents(); }, - computed: { - createdFromWizardTemplate() { - return !!this.process?.properties?.wizardTemplateUuid; - }, - wizardTemplateUuid() { - return this.process?.properties?.wizardTemplateUuid; - }, - }, methods: { ellipsisNavigate(action, data) { this.$emit("onProcessNavigate", action, data); @@ -191,9 +152,6 @@ export default { ProcessMaker.alert(err, "danger"); }); }, - getHelperProcess() { - this.$refs.wizardHelperProcessModal.getHelperProcessStartEvent(); - }, }, }; @@ -256,10 +214,6 @@ export default { cursor: pointer; } -.custom-align-wizard { - margin-left: auto; -} - .custom-text { font-family: 'Open Sans', sans-serif; font-size: 16px; @@ -268,8 +222,4 @@ export default { letter-spacing: -0.02; color: #556271; } - -.icon-wizard-class { - z-index: 5; -} diff --git a/resources/js/processes-catalogue/components/ProcessInfo.vue b/resources/js/processes-catalogue/components/ProcessInfo.vue index 689ed743b2..309d9eec04 100644 --- a/resources/js/processes-catalogue/components/ProcessInfo.vue +++ b/resources/js/processes-catalogue/components/ProcessInfo.vue @@ -25,8 +25,6 @@ :title="title" :process="process" :full-carousel="fullCarousel" - :is-wizard-template="createdFromWizardTemplate" - @getHelperProcess="getHelperProcess" @closeCarousel="closeFullCarousel" @close="closeProcessInfo" > @@ -44,13 +42,6 @@
- @@ -60,7 +51,6 @@ import ProcessTab from "./ProcessTab.vue"; import CarouselSlide from "./CarouselSlide.vue"; import SlideProcessInfo from "./slideProcessInfo/SlideProcessInfo.vue"; import ProcessOptions from "./ProcessOptions.vue"; -import WizardHelperProcessModal from "../../components/templates/WizardHelperProcessModal.vue"; export default { components: { @@ -69,7 +59,6 @@ export default { SlideProcessInfo, ProcessOptions, CarouselSlide, - WizardHelperProcessModal, }, props: ["process", "currentUserId", "currentUser", "ellipsisPermission"], data() { @@ -94,12 +83,6 @@ export default { ? this.process.name : this.$t("Process Information"); }, - createdFromWizardTemplate() { - return !!this.process?.properties?.wizardTemplateUuid; - }, - wizardTemplateUuid() { - return this.process?.properties?.wizardTemplateUuid; - }, }, mounted() { this.dataOptions = { @@ -146,9 +129,6 @@ export default { showFullCarousel() { this.fullCarousel = true; }, - getHelperProcess() { - this.$refs.wizardHelperProcessModal.getHelperProcessStartEvent(); - }, }, }; diff --git a/resources/js/processes-catalogue/components/ProcessListing.vue b/resources/js/processes-catalogue/components/ProcessListing.vue index 8a303c2a3a..13aa31218e 100644 --- a/resources/js/processes-catalogue/components/ProcessListing.vue +++ b/resources/js/processes-catalogue/components/ProcessListing.vue @@ -2,25 +2,16 @@
- - -
diff --git a/resources/js/processes-catalogue/components/ProcessesCatalogue.vue b/resources/js/processes-catalogue/components/ProcessesCatalogue.vue index 95f7115307..1f9d1dac7d 100644 --- a/resources/js/processes-catalogue/components/ProcessesCatalogue.vue +++ b/resources/js/processes-catalogue/components/ProcessesCatalogue.vue @@ -5,7 +5,6 @@ ref="breadcrumb" :category="category ? category.name : ''" :process="selectedProcess ? selectedProcess.name : ''" - :template="guidedTemplates ? 'Guided Templates' : ''" />
- -
- - - - - diff --git a/resources/js/processes-catalogue/components/menuCatologue.vue b/resources/js/processes-catalogue/components/menuCatologue.vue index e043959878..448ad4c5a4 100644 --- a/resources/js/processes-catalogue/components/menuCatologue.vue +++ b/resources/js/processes-catalogue/components/menuCatologue.vue @@ -64,7 +64,7 @@ @@ -124,7 +124,7 @@ export default { modalProcess: true, countCategories: 0, showCatalogue: false, - showGuidedTemplates: false, + showTemplates: false, selectedProcessItem: null, selectedTemplateItem: null, templateOptions: [ @@ -133,11 +133,6 @@ export default { selected: false, id: "all_templates", }, - { - label: this.$t("Guided Templates"), - selected: false, - id: "guided_templates", - }, ], comeFromProcess: false, }; @@ -147,7 +142,7 @@ export default { this.openTemplate(obj); }); - window.ProcessMaker.EventBus.$on("wizard-templates-selected", (obj) => { + window.ProcessMaker.EventBus.$on("all-templates-selected", (obj) => { this.selectTemplateItem(obj); }); }, @@ -235,7 +230,7 @@ export default { selectTemplateItem(item = null) { if (item === null) { item = this.templateOptions.find((obj) => { - return obj.id === "guided_templates"; + return obj.id === "all_templates"; }); } this.selectedTemplateItem = item; @@ -269,7 +264,7 @@ export default { this.showCatalogue = !this.showCatalogue; }, onToggleTemplates() { - this.showGuidedTemplates = !this.showGuidedTemplates; + this.showTemplates = !this.showTemplates; }, /** * Filter categories diff --git a/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue b/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue index 449e2b674b..41fd12db39 100644 --- a/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue +++ b/resources/js/processes-catalogue/components/optionsMenu/ProcessCounter.vue @@ -150,8 +150,4 @@ export default { margin-top: 10px; padding-left: 15px; } -.icon-wizard-class { - border-left: 1px solid rgba(0, 0, 0, 0.125); - z-index: 5; -} diff --git a/resources/js/processes-catalogue/components/slideProcessInfo/SlideProcessInfo.vue b/resources/js/processes-catalogue/components/slideProcessInfo/SlideProcessInfo.vue index 681a3eb123..624f9a1068 100644 --- a/resources/js/processes-catalogue/components/slideProcessInfo/SlideProcessInfo.vue +++ b/resources/js/processes-catalogue/components/slideProcessInfo/SlideProcessInfo.vue @@ -20,21 +20,6 @@ {{ title }}
-
-
- - {{ $t('Re-run Wizard') }} -
-
@endsection diff --git a/routes/api.php b/routes/api.php index 9cc716d4d4..a94349b9b8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -43,7 +43,6 @@ use ProcessMaker\Http\Controllers\Api\UserConfigurationController; use ProcessMaker\Http\Controllers\Api\UserController; use ProcessMaker\Http\Controllers\Api\UserTokenController; -use ProcessMaker\Http\Controllers\Api\WizardTemplateController; use ProcessMaker\Http\Controllers\Auth\TwoFactorAuthController; use ProcessMaker\Http\Controllers\TestStatusController; @@ -378,10 +377,6 @@ Route::post('template/{type}/{id}/apply', [TemplateController::class, 'applyTemplate'])->name('template.applyTemplate')->middleware('template-authorization'); Route::get('screen-builder/{type}/{id}', [TemplateController::class, 'show'])->name('screenBuilder.template.show')->middleware('template-authorization'); - // Wizard Templates - Route::get('wizard-templates', [WizardTemplateController::class, 'index'])->name('wizard-templates.index'); - Route::get('wizard-templates/{template_uuid}/get-helper-process', [WizardTemplateController::class, 'getHelperProcess'])->name('wizard-templates.getHelperProcess'); - // debugging javascript errors Route::post('debug', [DebugController::class, 'store'])->name('debug.store')->middleware('throttle'); diff --git a/tests/Extensions/RealTimeOutputExtension.php b/tests/Extensions/RealTimeOutputExtension.php new file mode 100644 index 0000000000..8a317305b7 --- /dev/null +++ b/tests/Extensions/RealTimeOutputExtension.php @@ -0,0 +1,137 @@ + */ + private static array $startedAt = []; + + /** @var array */ + private static array $preparedAt = []; + + public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void + { + $facade->registerSubscriber(new class implements PreparationStartedSubscriber { + public function notify(PreparationStarted $event): void + { + $id = $event->test()->id(); + RealTimeOutputExtension::markStarted($id); + RealTimeOutputExtension::write('START', $id, "\033[1;33m", true); + } + }); + + $facade->registerSubscriber(new class implements PreparedSubscriber { + public function notify(Prepared $event): void + { + $id = $event->test()->id(); + RealTimeOutputExtension::markPrepared($id); + $setupDuration = RealTimeOutputExtension::elapsedSinceStart($id); + RealTimeOutputExtension::write( + 'PREPARED', + $id, + "\033[1;36m", + false, + $setupDuration !== null ? sprintf('setup=%.2fs', $setupDuration) : null + ); + } + }); + + $facade->registerSubscriber(new class implements PassedSubscriber { + public function notify(Passed $event): void + { + RealTimeOutputExtension::writeFinished('PASS', $event->test()->id(), "\033[1;32m"); + } + }); + + $facade->registerSubscriber(new class implements FailedSubscriber { + public function notify(Failed $event): void + { + RealTimeOutputExtension::writeFinished('FAIL', $event->test()->id(), "\033[1;31m"); + } + }); + + $facade->registerSubscriber(new class implements ErroredSubscriber { + public function notify(Errored $event): void + { + RealTimeOutputExtension::writeFinished('ERROR', $event->test()->id(), "\033[1;31m"); + } + }); + + $facade->registerSubscriber(new class implements SkippedSubscriber { + public function notify(Skipped $event): void + { + RealTimeOutputExtension::writeFinished('SKIP', $event->test()->id(), "\033[1;34m"); + } + }); + } + + public static function markStarted(string $id): void + { + self::$startedAt[$id] = microtime(true); + unset(self::$preparedAt[$id]); + } + + public static function markPrepared(string $id): void + { + self::$preparedAt[$id] = microtime(true); + } + + public static function elapsedSinceStart(string $id): ?float + { + if (!isset(self::$startedAt[$id])) { + return null; + } + + return microtime(true) - self::$startedAt[$id]; + } + + public static function writeFinished(string $label, string $id, string $color): void + { + $total = self::elapsedSinceStart($id); + $body = null; + if ($total !== null) { + $body = sprintf('total=%.2fs', $total); + if (isset(self::$preparedAt[$id])) { + $body .= sprintf(' body=%.2fs', microtime(true) - self::$preparedAt[$id]); + } + } + + self::write($label, $id, $color, false, $body); + unset(self::$startedAt[$id], self::$preparedAt[$id]); + } + + public static function write(string $label, string $id, string $color, bool $leadingNewline = false, ?string $extra = null): void + { + $timestamp = date('H:i:s'); + $prefix = $leadingNewline ? "\n" : ''; + $suffix = $extra ? " ({$extra})" : ''; + fwrite(STDERR, sprintf( + "%s%s[%s]%s [%s] %s%s\n", + $prefix, + $color, + $label, + "\033[0m", + $timestamp, + $id, + $suffix + )); + } +} diff --git a/tests/Feature/Api/DevLinkTest.php b/tests/Feature/Api/DevLinkTest.php index f0a0af7b0e..d88bab6ed8 100644 --- a/tests/Feature/Api/DevLinkTest.php +++ b/tests/Feature/Api/DevLinkTest.php @@ -3,12 +3,17 @@ namespace Tests\Feature\Api; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; use PHPUnit\Framework\Attributes\DataProvider; use ProcessMaker\Http\Controllers\Api\DevLinkController; +use ProcessMaker\Jobs\DevLinkInstall; use ProcessMaker\Models\Bundle; +use ProcessMaker\Models\BundleAsset; use ProcessMaker\Models\DevLink; +use ProcessMaker\Models\Process; use ProcessMaker\Models\Screen; use ProcessMaker\Package\PackageDynamicUI\Models\Dashboard; use ProcessMaker\Package\PackageDynamicUI\Models\Menu; @@ -257,6 +262,154 @@ public function testShowBundle() $this->assertEquals($bundle->id, $response->json()['id']); } + public function testShowBundleReportsUnavailableAssetsWithoutFailing() + { + $bundle = Bundle::factory()->create(); + $missingProcessId = Process::max('id') + 1000; + $bundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + ]); + + $response = $this->apiCall('GET', route('api.devlink.local-bundle', ['bundle' => $bundle->id])); + + $response->assertOk() + ->assertJsonPath('assets.0.id', $bundleAsset->id) + ->assertJsonPath('assets.0.name', "Missing Process #$missingProcessId") + ->assertJsonPath('assets.0.url', null) + ->assertJsonPath('assets.0.integrity_status', BundleAsset::INTEGRITY_MISSING); + } + + public function testExportBundleRejectsUnavailableAssets() + { + $bundle = Bundle::factory()->create(['name' => 'Corrupt Bundle']); + $missingProcessId = Process::max('id') + 1000; + $bundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + ]); + + $response = $this->apiCall('GET', route('api.devlink.export-local-bundle', [ + 'bundle' => $bundle->id, + ])); + + $response->assertStatus(422) + ->assertJsonPath('error.code', 422) + ->assertJsonPath( + 'error.message', + 'The bundle Corrupt Bundle contains unavailable assets and cannot be exported.' + ) + ->assertJsonPath('errors.assets.0.bundle_asset_id', $bundleAsset->id) + ->assertJsonPath('errors.assets.0.asset_type', Process::class) + ->assertJsonPath('errors.assets.0.asset_id', $missingProcessId) + ->assertJsonPath('errors.assets.0.integrity_status', BundleAsset::INTEGRITY_MISSING); + } + + public function testInstalledBundleAllowsRemovingOnlyUnavailableAssets() + { + $devLink = DevLink::factory()->create(); + $bundle = Bundle::factory()->create([ + 'dev_link_id' => $devLink->id, + 'remote_id' => 123, + ]); + $screen = Screen::factory()->create(); + $validBundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Screen::class, + 'asset_id' => $screen->id, + ]); + $missingProcessId = Process::max('id') + 1000; + $invalidBundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + ]); + + $this->apiCall('DELETE', route('api.devlink.delete-bundle-asset', [ + 'bundle_asset' => $validBundleAsset->id, + ]))->assertStatus(422); + $this->assertDatabaseHas('bundle_assets', ['id' => $validBundleAsset->id]); + + $this->apiCall('DELETE', route('api.devlink.delete-bundle-asset', [ + 'bundle_asset' => $invalidBundleAsset->id, + ]))->assertOk(); + $this->assertDatabaseMissing('bundle_assets', ['id' => $invalidBundleAsset->id]); + } + + public function testLocalBundlesCanOrderNewestBundlesFirst() + { + $oldBundle = Bundle::factory()->create([ + 'created_at' => now()->subDays(2), + ]); + $newBundle = Bundle::factory()->create([ + 'created_at' => now(), + ]); + + $response = $this->apiCall('GET', route('api.devlink.local-bundles', [ + 'order_by' => 'created_at', + 'order_direction' => 'desc', + ])); + + $response->assertStatus(200); + $this->assertEquals($newBundle->id, $response->json('data.0.id')); + $this->assertNotEquals($oldBundle->id, $response->json('data.0.id')); + } + + public function testLocalBundlesCanReturnOneHundredRecords() + { + Bundle::factory()->count(101)->create(); + + $response = $this->apiCall('GET', route('api.devlink.local-bundles', [ + 'per_page' => 100, + ])); + + $response->assertStatus(200); + $this->assertCount(100, $response->json('data')); + $this->assertEquals(100, $response->json('meta.per_page')); + } + + public function testLocalBundlesFilterFindsBundleOutsideFirstPage() + { + $targetBundle = Bundle::factory()->create([ + 'name' => 'FOUR-31727 Search Target', + 'created_at' => now()->subDays(2), + ]); + Bundle::factory()->count(15)->create([ + 'created_at' => now(), + ]); + + $response = $this->apiCall('GET', route('api.devlink.local-bundles', [ + 'filter' => 'FOUR-31727 Search Target', + ])); + + $response->assertStatus(200); + $this->assertEquals($targetBundle->id, $response->json('data.0.id')); + $this->assertCount(1, $response->json('data')); + } + + public function testLocalBundlesEditableFilterExcludesRemoteBundles() + { + $devLink = DevLink::factory()->create(); + $localBundle = Bundle::factory()->create([ + 'dev_link_id' => null, + ]); + $remoteBundle = Bundle::factory()->create([ + 'dev_link_id' => $devLink->id, + ]); + + $response = $this->apiCall('GET', route('api.devlink.local-bundles', [ + 'editable' => true, + 'per_page' => 100, + ])); + + $response->assertStatus(200); + $bundleIds = collect($response->json('data'))->pluck('id'); + $this->assertTrue($bundleIds->contains($localBundle->id)); + $this->assertFalse($bundleIds->contains($remoteBundle->id)); + } + public function testGetBundleAllSettingsReturnsDashboardAndMenuOptions() { if (!hasPackage('package-dynamic-ui')) { @@ -433,6 +586,93 @@ public function testAddAssets() $this->assertEquals('Asset already exists in bundle', $response->json()['error']['message']); } + public function testPingReturnsOkWhenRemotePongSucceeds() + { + $devLink = DevLink::factory()->create([ + 'url' => 'https://remote-instance.test', + 'access_token' => 'token', + ]); + + Http::fake([ + 'remote-instance.test/*' => Http::response(['status' => 'ok'], 200), + ]); + + $response = $this->apiCall('GET', route('api.devlink.ping', ['devLink' => $devLink->id])); + + $response->assertStatus(200); + $response->assertJson(['status' => 'ok']); + } + + public function testPingReturnsAuthorizationRequiredWhenRemotePongReturnsUnauthorized() + { + $devLink = DevLink::factory()->create([ + 'url' => 'https://remote-instance.test', + 'access_token' => 'token', + ]); + + Http::fake([ + 'remote-instance.test/*' => Http::response(['message' => 'Unauthorized'], 401), + ]); + + $response = $this->apiCall('GET', route('api.devlink.ping', ['devLink' => $devLink->id])); + + $response->assertStatus(200); + $response->assertJson(['status' => 'authorization_required']); + } + + public function testPingReturnsAuthorizationRequiredWhenRemotePongReturnsForbidden() + { + $devLink = DevLink::factory()->create([ + 'url' => 'https://remote-instance.test', + 'access_token' => 'token', + ]); + + Http::fake([ + 'remote-instance.test/*' => Http::response(['message' => 'Forbidden'], 403), + ]); + + $response = $this->apiCall('GET', route('api.devlink.ping', ['devLink' => $devLink->id])); + + $response->assertStatus(200); + $response->assertJson(['status' => 'authorization_required']); + } + + public function testPingReturnsErrorWhenRemotePongFails() + { + $devLink = DevLink::factory()->create([ + 'url' => 'https://remote-instance.test', + 'access_token' => 'token', + ]); + + Http::fake([ + 'remote-instance.test/*' => Http::response(['message' => 'Server error'], 500), + ]); + + $response = $this->apiCall('GET', route('api.devlink.ping', ['devLink' => $devLink->id])); + + $response->assertStatus(200); + $response->assertJson(['status' => 'error']); + } + + public function testPingReturnsErrorWhenRemotePongCannotConnect() + { + $devLink = DevLink::factory()->create([ + 'url' => 'https://remote-instance.test', + 'access_token' => 'token', + ]); + + Http::fake([ + 'remote-instance.test/*' => function () { + throw new ConnectionException('Connection failed'); + }, + ]); + + $response = $this->apiCall('GET', route('api.devlink.ping', ['devLink' => $devLink->id])); + + $response->assertStatus(200); + $response->assertJson(['status' => 'error']); + } + public function testInstallRemoteAsset() { $screen = Screen::factory()->create(); @@ -468,4 +708,75 @@ public function testInstallRemoteAsset() $screen->refresh(); $this->assertEquals('Modified title', $screen->title); } + + public function testInstallEndpointsPassOperationIdToQueuedJobs() + { + Bus::fake(); + + $operationId = (string) Str::uuid(); + $devLink = DevLink::factory()->create(); + $bundle = Bundle::factory()->create([ + 'dev_link_id' => $devLink->id, + ]); + + $installResponse = $this->apiCall( + 'POST', + route('api.devlink.install-remote-bundle', [ + 'devLink' => $devLink->id, + 'remoteBundleId' => 123, + ]), + ['operation_id' => $operationId] + ); + $reinstallResponse = $this->apiCall( + 'POST', + route('api.devlink.reinstall-bundle', ['bundle' => $bundle->id]), + ['operation_id' => $operationId] + ); + $assetResponse = $this->apiCall( + 'POST', + route('api.devlink.install-remote-asset', ['devLink' => $devLink->id]), + [ + 'id' => 456, + 'class' => Screen::class, + 'operation_id' => $operationId, + ] + ); + + $installResponse->assertOk()->assertExactJson(['status' => 'queued']); + $reinstallResponse->assertOk()->assertExactJson(['status' => 'queued']); + $assetResponse->assertOk()->assertExactJson(['status' => 'queued']); + + Bus::assertDispatched(DevLinkInstall::class, function (DevLinkInstall $job) use ($operationId) { + return $job->type === DevLinkInstall::TYPE_INSTALL_BUNDLE + && $job->operationId === $operationId; + }); + Bus::assertDispatched(DevLinkInstall::class, function (DevLinkInstall $job) use ($operationId) { + return $job->type === DevLinkInstall::TYPE_REINSTALL_BUNDLE + && $job->operationId === $operationId; + }); + Bus::assertDispatched(DevLinkInstall::class, function (DevLinkInstall $job) use ($operationId) { + return $job->type === DevLinkInstall::TYPE_IMPORT_ASSET + && $job->operationId === $operationId; + }); + } + + public function testInstallEndpointGeneratesOperationIdWhenMissing() + { + Bus::fake(); + + $devLink = DevLink::factory()->create(); + $response = $this->apiCall( + 'POST', + route('api.devlink.install-remote-asset', ['devLink' => $devLink->id]), + [ + 'id' => 456, + 'class' => Screen::class, + ] + ); + + $response->assertOk()->assertExactJson(['status' => 'queued']); + Bus::assertDispatched(DevLinkInstall::class, function (DevLinkInstall $job) { + return Str::isUuid($job->operationId); + }); + } } diff --git a/tests/Feature/Api/ProcessRequestsTest.php b/tests/Feature/Api/ProcessRequestsTest.php index 930805cead..1556b07c90 100644 --- a/tests/Feature/Api/ProcessRequestsTest.php +++ b/tests/Feature/Api/ProcessRequestsTest.php @@ -14,6 +14,7 @@ use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\ProcessRequestToken; +use ProcessMaker\Models\Screen; use ProcessMaker\Models\User; use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface; use Tests\Feature\Shared\RequestHelper; @@ -1133,6 +1134,70 @@ public function testScreenRequested() $this->assertEmpty($data); } + public function testScreenRequestedReturnsMultiInstanceTokenPropertiesData() + { + $screen = Screen::factory()->create([ + 'config' => [[ + 'component' => 'FormTextArea', + 'config' => [ + 'label' => 'New Textarea', + 'name' => 'form_text_area_1', + ], + ]], + ]); + $bpmn = file_get_contents(base_path('tests/Feature/Api/processes/Timer_BoundaryEvent_MultiInstance.bpmn')); + $bpmn = str_replace('pm:screenRef="19"', 'pm:screenRef="' . $screen->id . '"', $bpmn); + $process = Process::factory()->create([ + 'bpmn' => $bpmn, + 'user_id' => $this->user->id, + ]); + $request = ProcessRequest::factory()->create([ + 'process_id' => $process->id, + 'process_version_id' => $process->getLatestVersion()->id, + 'callable_id' => 'ProcessId', + 'data' => [ + 'array' => [ + ['form_input_1' => 'lulu1'], + ], + ], + ]); + + ProcessRequestToken::factory()->create([ + 'process_id' => $process->id, + 'process_request_id' => $request->id, + 'element_id' => 'node_2', + 'element_type' => 'task', + 'status' => 'CLOSED', + 'data' => [ + 'loopCounter' => 1, + 'form_input_1' => 'lulu1', + 'form_text_area_1' => 'lorem ipsum', + ], + 'token_properties' => [ + 'data' => [ + 'loopCounter' => 1, + 'form_input_1' => 'lulu1', + 'form_text_area_1' => 'lorem ipsum', + ], + ], + ]); + + $route = route('api.requests.detail.screen', ['request' => $request->id]); + $response = $this->apiCall('GET', $route, [ + 'page' => 1, + 'per_page' => 10, + 'order_by' => 'completed_at', + 'order_direction' => 'asc', + 'filter' => '', + ]); + + $response->assertStatus(200); + $data = $response->json('data'); + $this->assertCount(1, $data); + $this->assertSame('lorem ipsum', $data[0]['data']['form_text_area_1']); + $this->assertSame('lorem ipsum', $data[0]['data']['_parent']['form_text_area_1']); + } + /** * Get a list of Requests by Cases. */ diff --git a/tests/Feature/Api/V1_1/ClipboardControllerTest.php b/tests/Feature/Api/V1_1/ClipboardControllerTest.php new file mode 100644 index 0000000000..374b9b0133 --- /dev/null +++ b/tests/Feature/Api/V1_1/ClipboardControllerTest.php @@ -0,0 +1,60 @@ +user->id)->delete(); + + $response = $this->apiCall('GET', '/api/1.1/clipboard/get_by_user'); + + $response->assertStatus(200) + ->assertJson([ + 'user_id' => $this->user->id, + 'type' => 'FORM', + 'config' => [], + ]); + + $this->assertDatabaseHas('clipboards', [ + 'user_id' => $this->user->id, + 'type' => 'FORM', + ]); + $this->assertSame(1, Clipboard::where('user_id', $this->user->id)->count()); + } + + public function test_get_by_user_returns_existing_clipboard_without_creating_duplicate(): void + { + Clipboard::where('user_id', $this->user->id)->delete(); + + $clipboard = Clipboard::factory()->create([ + 'user_id' => $this->user->id, + 'config' => [ + [ + 'component' => 'FormInput', + 'config' => ['label' => 'Existing Clipboard Item'], + ], + ], + 'type' => 'FORM', + ]); + + $response = $this->apiCall('GET', '/api/1.1/clipboard/get_by_user'); + + $response->assertStatus(200) + ->assertJson([ + 'id' => $clipboard->id, + 'user_id' => $this->user->id, + 'type' => 'FORM', + 'config' => $clipboard->config, + ]); + + $this->assertSame(1, Clipboard::where('user_id', $this->user->id)->count()); + } +} diff --git a/tests/Feature/Api/WizardTemplatesTest.php b/tests/Feature/Api/WizardTemplatesTest.php deleted file mode 100644 index 38e2abcd5e..0000000000 --- a/tests/Feature/Api/WizardTemplatesTest.php +++ /dev/null @@ -1,61 +0,0 @@ -count($total)->create(); - - $params = [ - 'order_by' => 'id', - 'order_direction' => 'asc', - 'per_page' => 10, - ]; - $route = route('api.wizard-templates.index', $params); - $response = $this->apiCall('GET', $route); - - $response->assertStatus(200); - $response->assertJsonCount(10, 'data'); - $response->assertJson([ - 'meta' => [ - 'per_page' => $params['per_page'], - 'total' => $total, - ], - ]); - } - - public function testItCanAddFilesFromUrlToMediaCollection() - { - // Create fake files. - Storage::fake('public'); - $directoryName = 'images'; - $filesToCreate = 3; - for ($i = 0; $i < $filesToCreate; $i++) { - UploadedFile::fake()->image("test_image{$i}.jpg")->storeAs($directoryName, "test_image{$i}.jpg", 'public'); - } - - // Add files to media collection. - $directory = Storage::disk('public')->path($directoryName); - $wizardTemplate = WizardTemplate::factory()->create(); - $wizardTemplate->addFilesToMediaCollection($directory); - - $this->assertCount($filesToCreate, $wizardTemplate->getMedia($directoryName)); - $this->assertDatabaseHas('media', [ - 'model_type' => WizardTemplate::class, - 'model_id' => $wizardTemplate->id, - 'collection_name' => $directoryName, - 'name' => 'test_image0', - ]); - } -} diff --git a/tests/Feature/Console/UpdateAnonymousUserTimezoneTest.php b/tests/Feature/Console/UpdateAnonymousUserTimezoneTest.php new file mode 100644 index 0000000000..51d7ca39bb --- /dev/null +++ b/tests/Feature/Console/UpdateAnonymousUserTimezoneTest.php @@ -0,0 +1,82 @@ +firstOrFail(); + $user->timezone = 'America/Chicago'; + $user->save(); + + config(['app.anonymous_user_timezone' => 'America/New_York']); + + $this->artisan('processmaker:update-anonymous-user-timezone') + ->expectsOutput('Anonymous user timezone updated from [America/Chicago] to [America/New_York].') + ->assertExitCode(0); + + $this->assertEquals('America/New_York', $user->fresh()->timezone); + } + + public function testDoesNothingWhenTimezoneAlreadyMatches(): void + { + $user = User::where('username', AnonymousUser::ANONYMOUS_USERNAME)->firstOrFail(); + $user->timezone = 'UTC'; + $user->save(); + + config(['app.anonymous_user_timezone' => 'UTC']); + + $this->artisan('processmaker:update-anonymous-user-timezone') + ->expectsOutput('Anonymous user timezone is already set to [UTC].') + ->assertExitCode(0); + + $this->assertEquals('UTC', $user->fresh()->timezone); + } + + public function testUpdatesTimezoneFromOption(): void + { + $user = User::where('username', AnonymousUser::ANONYMOUS_USERNAME)->firstOrFail(); + $user->timezone = 'UTC'; + $user->save(); + + config(['app.anonymous_user_timezone' => 'UTC']); + + $this->artisan('processmaker:update-anonymous-user-timezone', [ + '--timezone' => 'Europe/Madrid', + ]) + ->expectsOutput('Anonymous user timezone updated from [UTC] to [Europe/Madrid].') + ->assertExitCode(0); + + $this->assertEquals('Europe/Madrid', $user->fresh()->timezone); + } + + public function testCanBeRunMultipleTimes(): void + { + $user = User::where('username', AnonymousUser::ANONYMOUS_USERNAME)->firstOrFail(); + $user->timezone = 'America/Chicago'; + $user->save(); + + config(['app.anonymous_user_timezone' => 'America/Los_Angeles']); + + $this->artisan('processmaker:update-anonymous-user-timezone')->assertExitCode(0); + $this->artisan('processmaker:update-anonymous-user-timezone') + ->expectsOutput('Anonymous user timezone is already set to [America/Los_Angeles].') + ->assertExitCode(0); + + $this->assertEquals('America/Los_Angeles', $user->fresh()->timezone); + } + + public function testFailsWhenAnonymousUserDoesNotExist(): void + { + User::where('username', AnonymousUser::ANONYMOUS_USERNAME)->forceDelete(); + + $this->artisan('processmaker:update-anonymous-user-timezone') + ->expectsOutput('Anonymous user not found.') + ->assertExitCode(1); + } +} diff --git a/tests/Feature/ImportExport/Exporters/ProcessExporterWithScreenDataSourceTest.php b/tests/Feature/ImportExport/Exporters/ProcessExporterWithScreenDataSourceTest.php new file mode 100644 index 0000000000..f02fc39898 --- /dev/null +++ b/tests/Feature/ImportExport/Exporters/ProcessExporterWithScreenDataSourceTest.php @@ -0,0 +1,98 @@ +markTestSkipped('DataSource package not installed'); + } + + // Create a script first to consume some IDs + $script = Script::factory()->create(['title' => 'A Script']); + + $category = new $dataSourceCategoryClass(); + $category->name = 'Test Category ' . uniqid(); + $category->save(); + + $dataSource = new $dataSourceClass(); + $dataSource->name = 'Test Data Source ' . uniqid(); + $dataSource->description = 'Description'; + $dataSource->data_source_category_id = $category->id; + $dataSource->save(); + + // Let's try to make a script with the same ID as the data source + // This might be tricky due to auto-increment, but we can try to force it or just use a high ID + $duplicateScript = null; + try { + $duplicateScript = Script::factory()->create(['id' => $dataSource->id, 'title' => 'Duplicate ID Script']); + } catch (\Exception $e) { + // If ID is already taken, just create one normally + $duplicateScript = Script::factory()->create(['title' => 'Other Script']); + } + + $screen = Screen::factory()->create([ + 'title' => 'Screen with data source watcher', + 'watchers' => [ + [ + 'name' => 'Data Source Watcher', + 'script_id' => 'data_source-' . $dataSource->id, + 'script' => [ + 'id' => 'data_source-' . $dataSource->id, + 'title' => 'My Data Source', + ], + ], + ], + ]); + + $bpmn = ' + + + + +'; + + $process = Process::factory()->create([ + 'name' => 'Process with screen using data source', + 'bpmn' => $bpmn + ]); + + $exportJob = new ExportProcess($process); + $exportJob->handle(); + + // Access private package property via reflection + $reflection = new \ReflectionClass($exportJob); + $property = $reflection->getProperty('package'); + $property->setAccessible(true); + $package = $property->getValue($exportJob); + + // Check if data source is in package + $this->assertArrayHasKey('data_sources', $package); + $this->assertCount(1, $package['data_sources']); + $this->assertEquals($dataSource->id, $package['data_sources'][0]['id']); + + // Check that it is NOT in scripts + foreach ($package['scripts'] as $script) { + $this->assertNotEquals('data_source-' . $dataSource->id, $script['id']); + $this->assertNotEquals($dataSource->id, $script['id']); + // Also check that it's not present as a numeric ID if it's the same + if (is_numeric($script['id']) && (int)$script['id'] === $dataSource->id) { + $this->fail('Data source ID ' . $dataSource->id . ' found in scripts section'); + } + } + } +} diff --git a/tests/Feature/ImportExport/Exporters/ScriptsInScreenDataSourceTest.php b/tests/Feature/ImportExport/Exporters/ScriptsInScreenDataSourceTest.php new file mode 100644 index 0000000000..f22ac082ce --- /dev/null +++ b/tests/Feature/ImportExport/Exporters/ScriptsInScreenDataSourceTest.php @@ -0,0 +1,116 @@ +create([ + 'title' => 'Screen with data source watcher', + 'watchers' => [ + [ + 'name' => 'Data Source Watcher', + 'script_id' => 'data_source-3', + 'script' => [ + 'id' => 'data_source-3', + 'title' => 'My Data Source', + ], + ], + ], + ]); + + $scriptsInScreen = new ScriptsInScreen(); + $scripts = $scriptsInScreen->referencesToExport($screen); + + // After the fix, this should be 0 because the only watcher is a data source + $this->assertCount(0, $scripts, 'Should not identify data_source as a script'); + } + + /** + * Test that DataSourcesInScreen identifies data_source- prefixed IDs as data sources. + */ + public function testReferencesToExportWithDataSourceAsset() + { + $screen = Screen::factory()->create([ + 'title' => 'Screen with data source watcher', + 'watchers' => [ + [ + 'name' => 'Data Source Watcher', + 'script_id' => 'data_source-3', + 'script' => [ + 'id' => 'data_source-3', + 'title' => 'My Data Source', + ], + ], + ], + ]); + + $dataSourcesInScreen = new DataSourcesInScreen(); + $dataSources = $dataSourcesInScreen->referencesToExport($screen); + + $this->assertCount(1, $dataSources); + $this->assertEquals('ProcessMaker\Packages\Connectors\DataSources\Models\DataSource', $dataSources[0][0]); + $this->assertEquals(3, $dataSources[0][1]); + } + + /** + * Test that ExportScreen includes data sources in the package. + */ + public function testExportScreenIncludesDataSources() + { + $dataSourceClass = 'ProcessMaker\Packages\Connectors\DataSources\Models\DataSource'; + $dataSourceCategoryClass = 'ProcessMaker\Packages\Connectors\DataSources\Models\DataSourceCategory'; + if (!class_exists($dataSourceClass) || !class_exists($dataSourceCategoryClass)) { + $this->markTestSkipped('DataSource package not installed'); + } + + $category = new $dataSourceCategoryClass(); + $category->name = 'Test Category'; + $category->save(); + + $dataSource = new $dataSourceClass(); + $dataSource->name = 'Test Data Source'; + $dataSource->description = 'Description'; + $dataSource->data_source_category_id = $category->id; + $dataSource->save(); + + $screen = Screen::factory()->create([ + 'title' => 'Screen with data source watcher', + 'watchers' => [ + [ + 'name' => 'Data Source Watcher', + 'script_id' => 'data_source-' . $dataSource->id, + 'script' => [ + 'id' => 'data_source-' . $dataSource->id, + 'title' => 'My Data Source', + ], + ], + ], + ]); + + $exportJob = new ExportScreen($screen); + $exportJob->handle(); + + // Access private package property via reflection + $reflection = new \ReflectionClass($exportJob); + // package is protected in ExportProcess + $property = $reflection->getParentClass()->getProperty('package'); + $property->setAccessible(true); + $package = $property->getValue($exportJob); + + $this->assertArrayHasKey('data_sources', $package); + $this->assertCount(1, $package['data_sources']); + $this->assertEquals($dataSource->id, $package['data_sources'][0]['id']); + } +} diff --git a/tests/Feature/ImportExport/LoggerTest.php b/tests/Feature/ImportExport/LoggerTest.php new file mode 100644 index 0000000000..4da4ae3cdc --- /dev/null +++ b/tests/Feature/ImportExport/LoggerTest.php @@ -0,0 +1,52 @@ +{$method}('Test message'); + + Event::assertDispatched(ImportLog::class, function (ImportLog $event) use ($expectedType) { + return $event->userId === 123 + && $event->type === $expectedType + && $event->message === 'Test message' + && $event->operationId === 'operation-123'; + }); + } + + public static function messageMethods(): array + { + return [ + 'log' => ['log', 'log'], + 'warning' => ['warn', 'warn'], + 'error' => ['error', 'error'], + 'status' => ['status', 'status'], + ]; + } + + public function testOperationIdRemainsOptional() + { + Event::fake([ImportLog::class]); + Storage::fake('local'); + + (new Logger(123))->status('Test message'); + + Event::assertDispatched(ImportLog::class, function (ImportLog $event) { + return $event->operationId === null; + }); + } +} diff --git a/tests/Feature/Jobs/DevLinkInstallTest.php b/tests/Feature/Jobs/DevLinkInstallTest.php new file mode 100644 index 0000000000..67cc45dd66 --- /dev/null +++ b/tests/Feature/Jobs/DevLinkInstallTest.php @@ -0,0 +1,86 @@ +shouldReceive('forceRelease')->once(); + Cache::shouldReceive('lock') + ->once() + ->with(ImportV2::CACHE_LOCK_KEY) + ->andReturn($lock); + + $job = new DevLinkInstall( + 123, + 456, + Bundle::class, + 789, + DevLinkInstall::MODE_UPDATE, + DevLinkInstall::TYPE_INSTALL_BUNDLE, + 'operation-123', + ); + + $job->failed(new RuntimeException('Installation failed')); + + Event::assertDispatched(ImportLog::class, function (ImportLog $event) { + return $event->type === 'error' + && str_contains($event->message, 'Installation failed') + && $event->operationId === 'operation-123'; + }); + } + + public function testFailedRemoteBundleJobIncludesOperationIdInActionableErrorEvent() + { + Event::fake([ImportLog::class]); + Storage::fake('local'); + + $lock = Mockery::mock(); + $lock->shouldReceive('forceRelease')->once(); + Cache::shouldReceive('lock') + ->once() + ->with(ImportV2::CACHE_LOCK_KEY) + ->andReturn($lock); + + $job = new DevLinkInstall( + 123, + 456, + Bundle::class, + 789, + DevLinkInstall::MODE_UPDATE, + DevLinkInstall::TYPE_INSTALL_BUNDLE, + 'operation-456', + ); + $exception = new DevLinkRemoteBundleException([[ + 'asset_type' => Bundle::class, + 'asset_id' => 789, + 'bundle_asset_id' => 321, + ]]); + + $job->failed($exception); + + Event::assertDispatched(ImportLog::class, function (ImportLog $event) { + return $event->type === 'error' + && str_contains($event->message, 'The remote bundle contains unavailable assets') + && !str_contains($event->message, DevLinkRemoteBundleException::class) + && $event->operationId === 'operation-456'; + }); + } +} diff --git a/tests/Feature/ServerTimingMiddlewareTest.php b/tests/Feature/ServerTimingMiddlewareTest.php index b9449ebfed..040401d227 100644 --- a/tests/Feature/ServerTimingMiddlewareTest.php +++ b/tests/Feature/ServerTimingMiddlewareTest.php @@ -6,6 +6,8 @@ use Illuminate\Support\Facades\Route; use ProcessMaker\Http\Middleware\ServerTimingMiddleware; use ProcessMaker\Models\User; +use ProcessMaker\Providers\ProcessMakerServiceProvider; +use ReflectionClass; use Tests\Feature\Shared\RequestHelper; use Tests\TestCase; @@ -41,6 +43,28 @@ public function testServerTimingHeaderIncludesAllMetrics() $this->assertStringContainsString('db;dur=', $serverTiming[2]); } + public function testBeginRequestTimingClearsAccumulatedQueryTime() + { + $reflection = new ReflectionClass(ProcessMakerServiceProvider::class); + $property = $reflection->getProperty('queryTime'); + $property->setAccessible(true); + $property->setValue(null, 500); + + Route::middleware(ServerTimingMiddleware::class)->get('/timing-reset-test', function () { + DB::select('SELECT 1'); + + return response()->json(['message' => 'Timing reset test']); + }); + + $response = $this->get('/timing-reset-test'); + $serverTiming = $this->getHeader($response, 'server-timing'); + + preg_match('/db;dur=([\d.]+)/', implode(',', $serverTiming), $matches); + $dbTime = (float) ($matches[1] ?? 500); + + $this->assertLessThan(500, $dbTime); + } + public function testQueryTimeIsMeasured() { // Mock a route with a query diff --git a/tests/Model/BundleAssetTest.php b/tests/Model/BundleAssetTest.php index 88aa6b0184..42179b1042 100644 --- a/tests/Model/BundleAssetTest.php +++ b/tests/Model/BundleAssetTest.php @@ -6,6 +6,7 @@ use ProcessMaker\Models\Bundle; use ProcessMaker\Models\BundleAsset; use ProcessMaker\Models\Group; +use ProcessMaker\Models\Process; use ProcessMaker\Models\Screen; use Tests\TestCase; @@ -16,6 +17,7 @@ public function testCanExport() $screen = Screen::factory()->create(); $this->assertTrue(BundleAsset::canExport($screen)); + $this->assertFalse(BundleAsset::canExport(null)); } public function testExporterNotSupported() @@ -26,4 +28,55 @@ public function testExporterNotSupported() $this->expectException(ExporterNotSupported::class); $bundle->addAsset($group); } + + public function testMissingAssetCanBeSerialized() + { + $bundle = Bundle::factory()->create(); + $missingProcessId = Process::max('id') + 1000; + $bundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + ]); + + $serialized = $bundleAsset->toArray(); + + $this->assertSame(BundleAsset::INTEGRITY_MISSING, $serialized['integrity_status']); + $this->assertSame("Missing Process #$missingProcessId", $serialized['name']); + $this->assertNull($serialized['url']); + $this->assertNull($serialized['owner_name']); + $this->assertSame([], $serialized['categories']); + } + + public function testUnavailableAssetTypeCanBeSerialized() + { + $bundleAsset = BundleAsset::factory()->create([ + 'asset_type' => 'ProcessMaker\Missing\Asset', + 'asset_id' => 1234, + ]); + + $serialized = $bundleAsset->toArray(); + + $this->assertSame(BundleAsset::INTEGRITY_TYPE_UNAVAILABLE, $serialized['integrity_status']); + $this->assertSame('Missing Asset #1234', $serialized['name']); + $this->assertNull($serialized['url']); + } + + public function testDeletingAnAssetLeavesItsBundleAssociationForManualRepair() + { + $process = Process::factory()->create(); + $bundleAsset = BundleAsset::factory()->create([ + 'asset_type' => Process::class, + 'asset_id' => $process->id, + ]); + + $process->delete(); + + $this->assertTrue($process->trashed()); + $this->assertDatabaseHas('bundle_assets', ['id' => $bundleAsset->id]); + $this->assertSame( + BundleAsset::INTEGRITY_MISSING, + $bundleAsset->refresh()->integrity_status + ); + } } diff --git a/tests/Model/BundleTest.php b/tests/Model/BundleTest.php index d0bee366ee..aa85265b30 100644 --- a/tests/Model/BundleTest.php +++ b/tests/Model/BundleTest.php @@ -3,6 +3,7 @@ namespace Tests\Model; use Illuminate\Support\Facades\Storage; +use ProcessMaker\Exception\BundleIntegrityException; use ProcessMaker\ImportExport\Exporters\ScreenExporter; use ProcessMaker\ImportExport\Logger; use ProcessMaker\Models\Bundle; @@ -17,6 +18,10 @@ class BundleTest extends TestCase { use HelperTrait; + private const ALPHA_DASHBOARD_NAME = 'Alpha Dashboard'; + + private const ALPHA_MENU_NAME = 'Alpha Menu'; + public function testExport() { $this->addGlobalSignalProcess(); @@ -44,6 +49,52 @@ public function testExport() $this->assertEquals($screen->title, $payload[1]['name']); } + public function testExportRejectsBundleWithUnavailableAssetsBeforeExporting() + { + $bundle = Bundle::factory()->create(['name' => 'Corrupt Bundle']); + $screen = Screen::factory()->create(); + $missingProcessId = Process::max('id') + 1000; + BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Screen::class, + 'asset_id' => $screen->id, + ]); + $missingBundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + ]); + $unavailableBundleAsset = BundleAsset::factory()->create([ + 'bundle_id' => $bundle->id, + 'asset_type' => 'ProcessMaker\Missing\Asset', + 'asset_id' => 1234, + ]); + + try { + $bundle->export(); + $this->fail('Expected bundle integrity validation to fail.'); + } catch (BundleIntegrityException $exception) { + $this->assertSame( + 'The bundle Corrupt Bundle contains unavailable assets and cannot be exported.', + $exception->getMessage() + ); + $this->assertSame([ + [ + 'bundle_asset_id' => $missingBundleAsset->id, + 'asset_type' => Process::class, + 'asset_id' => $missingProcessId, + 'integrity_status' => BundleAsset::INTEGRITY_MISSING, + ], + [ + 'bundle_asset_id' => $unavailableBundleAsset->id, + 'asset_type' => 'ProcessMaker\Missing\Asset', + 'asset_id' => 1234, + 'integrity_status' => BundleAsset::INTEGRITY_TYPE_UNAVAILABLE, + ], + ], $exception->invalidAssets()); + } + } + public function testSyncAssets() { $screen1 = Screen::factory()->create(['title' => 'Screen 1']); @@ -166,8 +217,8 @@ public function testSettingPreviewUsesInstalledPayloadMetadata() $bundle->savePayloadsToFile([], [[ self::settingPayload('dashboard_package', 'dashboard-zulu', 'Zulu Dashboard'), self::settingPayload('menu_package', 'menu-bravo', 'Bravo Menu'), - self::settingPayload('dashboard_package', 'dashboard-alpha', 'Alpha Dashboard'), - self::settingPayload('menu_package', 'menu-alpha', 'Alpha Menu'), + self::settingPayload('dashboard_package', 'dashboard-alpha', self::ALPHA_DASHBOARD_NAME), + self::settingPayload('menu_package', 'menu-alpha', self::ALPHA_MENU_NAME), ]]); $this->assertTrue($bundle->newestVersionFile()->getCustomProperty('settings_payloads_complete')); @@ -177,7 +228,7 @@ public function testSettingPreviewUsesInstalledPayloadMetadata() 'selection' => 'partial', 'available' => true, 'items' => [ - ['key' => 'dashboard-alpha', 'name' => 'Alpha Dashboard'], + ['key' => 'dashboard-alpha', 'name' => self::ALPHA_DASHBOARD_NAME], ['key' => 'dashboard-zulu', 'name' => 'Zulu Dashboard'], ], ], $bundle->settingPreview('ui_dashboards')); @@ -187,7 +238,7 @@ public function testSettingPreviewUsesInstalledPayloadMetadata() 'selection' => 'all', 'available' => true, 'items' => [ - ['key' => 'menu-alpha', 'name' => 'Alpha Menu'], + ['key' => 'menu-alpha', 'name' => self::ALPHA_MENU_NAME], ['key' => 'menu-bravo', 'name' => 'Bravo Menu'], ], ], $bundle->settingPreview('ui_menus')); @@ -213,8 +264,8 @@ public function testSettingPreviewIsUnavailableForUnmarkedLegacySnapshot() $bundle->addSettings('ui_dashboards', null); $bundle->addSettings('ui_menus', json_encode(['id' => [9001]])); $bundle->addMediaFromString(gzencode(json_encode([ - self::settingPayload('dashboard_package', 'dashboard-alpha', 'Alpha Dashboard'), - self::settingPayload('menu_package', 'menu-alpha', 'Alpha Menu'), + self::settingPayload('dashboard_package', 'dashboard-alpha', self::ALPHA_DASHBOARD_NAME), + self::settingPayload('menu_package', 'menu-alpha', self::ALPHA_MENU_NAME), ]))) ->usingFileName('payloads.json.gz') ->withCustomProperties(['version' => $bundle->version]) diff --git a/tests/Model/DevLinkTest.php b/tests/Model/DevLinkTest.php index dc6ca7e332..f0efaf12d3 100644 --- a/tests/Model/DevLinkTest.php +++ b/tests/Model/DevLinkTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; +use ProcessMaker\Exception\DevLinkRemoteBundleException; use ProcessMaker\Models\Bundle; use ProcessMaker\Models\DevLink; use ProcessMaker\Models\Screen; @@ -23,6 +24,10 @@ class DevLinkTest extends TestCase private const SECOND_MENU_DESCRIPTION = 'Second menu description'; + private const LOCAL_BUNDLE_API_PATH = 'local-bundles/123'; + + private const EXPORT_LOCAL_BUNDLE_API_PATH = 'export-local-bundle/123'; + public function testGetClientUrl() { $devLink = DevLink::factory()->create([ @@ -86,8 +91,8 @@ public function testInstallRemoteBundle() $bundle->delete(); Http::fake([ - self::remoteApiUrl('local-bundles/123') => Http::response(self::remoteBundleResponse('5')), - self::remoteApiUrl('export-local-bundle/123') => Http::response([ + self::remoteApiUrl(self::LOCAL_BUNDLE_API_PATH) => Http::response(self::remoteBundleResponse('5')), + self::remoteApiUrl(self::EXPORT_LOCAL_BUNDLE_API_PATH) => Http::response([ 'payloads' => $exports, ]), self::remoteApiUrl('export-local-bundle/123/settings') => Http::response([ @@ -123,6 +128,40 @@ public function testInstallRemoteBundle() $this->assertCount(3, $payloads); } + public function testInstallRemoteBundleReportsUnavailableRemoteAssets() + { + Http::preventStrayRequests(); + Http::fake([ + self::remoteApiUrl(self::LOCAL_BUNDLE_API_PATH) => Http::response(self::remoteBundleResponse('5')), + self::remoteApiUrl(self::EXPORT_LOCAL_BUNDLE_API_PATH) => Http::response([ + 'error' => [ + 'code' => 422, + 'message' => 'The bundle contains unavailable assets.', + ], + 'errors' => [ + 'assets' => [[ + 'bundle_asset_id' => 34, + 'asset_type' => 'ProcessMaker\Plugins\Collections\Models\Collection', + 'asset_id' => 20, + 'integrity_status' => 'missing', + ]], + ], + ], 422), + ]); + + $devLink = DevLink::factory()->create([ + 'url' => self::REMOTE_INSTANCE_URL, + ]); + + $this->expectException(DevLinkRemoteBundleException::class); + $this->expectExceptionMessage( + 'The remote bundle contains unavailable assets: Collection #20 (bundle asset #34). ' . + 'Repair the bundle on the source instance and try again.' + ); + + $devLink->installRemoteBundle(123, 'update'); + } + public function testInstallRemoteBundleImportsAndReinstallsEverySelectedMenu() { if (!hasPackage('package-dynamic-ui')) { @@ -310,13 +349,13 @@ public function testUpdateBundle() ]); Http::fake([ - self::remoteApiUrl('local-bundles/123') => Http::sequence() + self::remoteApiUrl(self::LOCAL_BUNDLE_API_PATH) => Http::sequence() ->push(self::remoteBundleResponse('2'), 200) ->push(self::remoteBundleResponse('3'), 200) ->push(self::remoteBundleResponse('4'), 200) ->push(self::remoteBundleResponse('8'), 200) ->push(self::remoteBundleResponse('9'), 200), - self::remoteApiUrl('export-local-bundle/123') => Http::sequence() + self::remoteApiUrl(self::EXPORT_LOCAL_BUNDLE_API_PATH) => Http::sequence() ->push([ 'payloads' => $exports, ], 200) diff --git a/tests/TestCase.php b/tests/TestCase.php index fce7ee220d..587ba3b53f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -72,7 +72,21 @@ protected function setUp(): void // Clear Redis cache before running tests foreach (['default', 'cache', 'cache_settings'] as $connection) { + if (env('TESTING_SETUP_TRACE')) { + fwrite(STDERR, sprintf( + "\033[1;35m[SETUP]\033[0m [%s] Redis flushDb(%s) start\n", + date('H:i:s'), + $connection + )); + } Redis::connection($connection)->flushDb(); + if (env('TESTING_SETUP_TRACE')) { + fwrite(STDERR, sprintf( + "\033[1;35m[SETUP]\033[0m [%s] Redis flushDb(%s) done\n", + date('H:i:s'), + $connection + )); + } } if (!self::$cacheCleared) { diff --git a/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php b/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php new file mode 100644 index 0000000000..6c10b4d719 --- /dev/null +++ b/tests/unit/ProcessMaker/Http/Controllers/TaskControllerSmartExtractTest.php @@ -0,0 +1,64 @@ +create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'true', + ]); + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::DASHBOARD_URL, + 'value' => 'https://dashboard.example.com/edit.html', + ]); + + $processRequest = new ProcessRequest([ + 'data' => [ + 'documentToken' => 'document-token', + 'fileId' => 'file-123', + ], + ]); + $task = new ProcessRequestToken(); + $task->setRelation('processRequest', $processRequest); + + $controller = new TaskController(app(SmartExtractConfiguration::class)); + $method = new ReflectionMethod(TaskController::class, 'smartExtractHitlConfiguration'); + $method->setAccessible(true); + + [$enabled, $iframeUrl] = $method->invoke($controller, $task, true); + + $this->assertTrue($enabled); + $this->assertSame( + 'https://dashboard.example.com/edit.html?documentToken=document-token&fileId=file-123', + $iframeUrl + ); + } + + public function test_hitl_configuration_fails_closed_when_disabled(): void + { + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'false', + ]); + + $task = new ProcessRequestToken(); + $task->setRelation('processRequest', new ProcessRequest(['data' => []])); + + $controller = new TaskController(app(SmartExtractConfiguration::class)); + $method = new ReflectionMethod(TaskController::class, 'smartExtractHitlConfiguration'); + $method->setAccessible(true); + + $this->assertSame([false, null], $method->invoke($controller, $task, true)); + } +} diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php new file mode 100644 index 0000000000..71d172a2ca --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php @@ -0,0 +1,43 @@ +to('recipient@example.com') + ->subject('With attachment') + ->html('

Hello

') + ->attach('file-contents', 'report.txt', 'text/plain'); + + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + + $this->assertSame('With attachment', $payload['message']['subject']); + $this->assertSame('HTML', $payload['message']['body']['contentType']); + $this->assertCount(1, $payload['message']['attachments']); + $this->assertSame([ + '@odata.type' => '#microsoft.graph.fileAttachment', + 'name' => 'report.txt', + 'contentType' => 'text/plain', + 'contentBytes' => base64_encode('file-contents'), + ], $payload['message']['attachments'][0]); + } + + public function testToSendMailPayloadOmitsAttachmentsKeyWhenNonePresent() + { + $email = (new Email()) + ->to('recipient@example.com') + ->subject('No attachment') + ->text('Hello'); + + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + + $this->assertArrayNotHasKey('attachments', $payload['message']); + } +} diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php new file mode 100644 index 0000000000..be23d292f6 --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -0,0 +1,61 @@ + '', + 'key' => 'client-id', + 'secret' => 'client-secret', + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Microsoft Graph credentials are not configured.'); + + $provider->getAccessToken(); + } + + public function testGetAccessTokenRequestsTokenFromMicrosoft() + { + $tokenResponse = new Response(200, [], json_encode(['access_token' => 'token-from-azure'])); + + $guzzle = Mockery::mock(Client::class); + $guzzle->shouldReceive('post') + ->once() + ->with( + 'https://login.microsoftonline.com/tenant-id-123/oauth2/v2.0/token', + Mockery::on(function ($options) { + return $options['form_params']['client_id'] === 'client-id-123' + && $options['form_params']['client_secret'] === 'client-secret-123' + && $options['form_params']['scope'] === 'https://graph.microsoft.com/.default' + && $options['form_params']['grant_type'] === 'client_credentials' + && $options['http_errors'] === false; + }) + ) + ->andReturn($tokenResponse); + + $provider = new MicrosoftGraphTokenProvider([ + 'tenant_id' => 'tenant-id-123', + 'key' => 'client-id-123', + 'secret' => 'client-secret-123', + ], 1, $guzzle); + + $this->assertSame('token-from-azure', $provider->getAccessToken()); + } +} diff --git a/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php b/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php new file mode 100644 index 0000000000..7895d8519f --- /dev/null +++ b/tests/unit/ProcessMaker/Octane/ResetRequestStateTest.php @@ -0,0 +1,74 @@ +assertGreaterThan(0, ProcessMakerServiceProvider::getQueryTime()); + + $listener = new ResetRequestState(); + $listener->handle(); + + $this->assertSame(0.0, ProcessMakerServiceProvider::getQueryTime()); + } + + public function test_it_prevents_redirect_state_from_leaking_into_the_next_request(): void + { + Event::fake([RedirectToEvent::class]); + + $redirectListener = new RedirectStateProbe(); + $redirectListener->queue(ProcessRequest::factory()->create()); + + $listener = new ResetRequestState(); + $listener->handle(); + + HandleRedirectListener::sendRedirectToEvent(); + + Event::assertNotDispatched(RedirectToEvent::class); + } + + public function test_octane_request_termination_automatically_resets_request_state(): void + { + Event::fake([RedirectToEvent::class]); + + $redirectListener = new RedirectStateProbe(); + $redirectListener->queue(ProcessRequest::factory()->create()); + + event(new RequestTerminated( + $this->app, + $this->app, + Request::create('/first-request'), + new Response() + )); + + HandleRedirectListener::sendRedirectToEvent(); + + Event::assertNotDispatched(RedirectToEvent::class); + } +} + +final class RedirectStateProbe extends HandleRedirectListener +{ + public function queue(ProcessRequest $processRequest): void + { + $this->setRedirectTo($processRequest, 'processUpdated'); + } +} diff --git a/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php b/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php new file mode 100644 index 0000000000..2cb283786e --- /dev/null +++ b/tests/unit/ProcessMaker/ScriptRunners/SmartExtractEnvironmentVariablesTest.php @@ -0,0 +1,139 @@ +createApiHost(); + $executor = ScriptExecutor::factory()->create(['language' => 'php']); + $runner = new class ($executor) extends Base { + public function config($code, array $dockerConfig) + { + return $dockerConfig; + } + }; + + $method = new ReflectionMethod(Base::class, 'getEnvironmentVariables'); + $method->setAccessible(true); + $variables = $method->invoke($runner, false); + + $apiHosts = array_values(array_filter( + $variables, + fn (string $variable) => str_starts_with($variable, SmartExtractConfiguration::API_HOST . '=') + )); + + $this->assertSame([ + SmartExtractConfiguration::API_HOST . '=https://database.example.com', + ], $apiHosts); + } + + public function test_microservice_runner_preserves_database_api_host(): void + { + $this->createApiHost(); + $script = Script::factory()->create(['language' => 'php']); + $user = User::factory()->create(); + Cache::put('script-runner-' . $user->id, 'access-token'); + + $runner = new ScriptMicroserviceRunner($script); + $method = new ReflectionMethod(ScriptMicroserviceRunner::class, 'getEnvironmentVariables'); + $method->setAccessible(true); + $variables = $method->invoke($runner, $user); + + $this->assertSame( + 'https://database.example.com', + $variables[SmartExtractConfiguration::API_HOST] + ); + } + + public function test_both_runners_propagate_the_legacy_api_host_fallback(): void + { + config(['smart-extract.api_host' => 'https://legacy.example.com']); + + $executor = ScriptExecutor::factory()->create(['language' => 'php']); + $localRunner = new class ($executor) extends Base { + public function config($code, array $dockerConfig) + { + return $dockerConfig; + } + }; + $localMethod = new ReflectionMethod(Base::class, 'getEnvironmentVariables'); + $localMethod->setAccessible(true); + $localVariables = $localMethod->invoke($localRunner, false); + + $this->assertSame([ + SmartExtractConfiguration::API_HOST . '=https://legacy.example.com', + ], array_values(array_filter( + $localVariables, + fn (string $variable) => str_starts_with($variable, SmartExtractConfiguration::API_HOST . '=') + ))); + + $script = Script::factory()->create(['language' => 'php']); + $user = User::factory()->create(); + Cache::put('script-runner-' . $user->id, 'access-token'); + $microserviceRunner = new ScriptMicroserviceRunner($script); + $microserviceMethod = new ReflectionMethod(ScriptMicroserviceRunner::class, 'getEnvironmentVariables'); + $microserviceMethod->setAccessible(true); + $microserviceVariables = $microserviceMethod->invoke($microserviceRunner, $user); + + $this->assertSame( + 'https://legacy.example.com', + $microserviceVariables[SmartExtractConfiguration::API_HOST] + ); + } + + public function test_existing_empty_database_api_host_suppresses_fallback_in_both_runners(): void + { + config(['smart-extract.api_host' => 'https://legacy.example.com']); + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::API_HOST, + 'value' => '', + ]); + + $executor = ScriptExecutor::factory()->create(['language' => 'php']); + $localRunner = new class ($executor) extends Base { + public function config($code, array $dockerConfig) + { + return $dockerConfig; + } + }; + $localMethod = new ReflectionMethod(Base::class, 'getEnvironmentVariables'); + $localMethod->setAccessible(true); + $localVariables = $localMethod->invoke($localRunner, false); + + $this->assertSame([], array_values(array_filter( + $localVariables, + fn (string $variable) => str_starts_with($variable, SmartExtractConfiguration::API_HOST . '=') + ))); + + $script = Script::factory()->create(['language' => 'php']); + $user = User::factory()->create(); + Cache::put('script-runner-' . $user->id, 'access-token'); + $microserviceRunner = new ScriptMicroserviceRunner($script); + $microserviceMethod = new ReflectionMethod(ScriptMicroserviceRunner::class, 'getEnvironmentVariables'); + $microserviceMethod->setAccessible(true); + $microserviceVariables = $microserviceMethod->invoke($microserviceRunner, $user); + + $this->assertArrayNotHasKey(SmartExtractConfiguration::API_HOST, $microserviceVariables); + } + + private function createApiHost(): void + { + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::API_HOST, + 'value' => 'https://database.example.com', + ]); + } +} diff --git a/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php b/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php new file mode 100644 index 0000000000..63af1793e6 --- /dev/null +++ b/tests/unit/ProcessMaker/Services/SmartExtractConfigurationTest.php @@ -0,0 +1,149 @@ +createConfigurationVariables(); + + DB::flushQueryLog(); + DB::enableQueryLog(); + + $configuration = new SmartExtractConfiguration(); + + $this->assertSame('https://extract.example.com', $configuration->apiHost()); + $this->assertSame('client-id', $configuration->clientId()); + $this->assertSame('client-secret', $configuration->clientSecret()); + $this->assertSame('https://dashboard.example.com/edit.html', $configuration->dashboardUrl()); + $this->assertTrue($configuration->hitlEnabled()); + + $queries = collect(DB::getQueryLog()) + ->filter(fn (array $query) => str_contains($query['query'], 'environment_variables')); + + $this->assertCount(1, $queries); + } + + public function test_missing_empty_and_invalid_values_fail_closed(): void + { + config([ + 'smart-extract.api_host' => null, + 'smart-extract.client_id' => null, + 'smart-extract.client_secret' => null, + 'smart-extract.dashboard_url' => null, + 'smart-extract.hitl_enabled' => false, + ]); + + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::HITL_ENABLED, + 'value' => 'not-a-boolean', + ]); + EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::DASHBOARD_URL, + 'value' => ' ', + ]); + + $configuration = new SmartExtractConfiguration(); + + $this->assertFalse($configuration->hitlEnabled()); + $this->assertNull($configuration->apiHost()); + $this->assertNull($configuration->clientId()); + $this->assertNull($configuration->clientSecret()); + $this->assertNull($configuration->dashboardUrl()); + } + + public function test_missing_database_values_fall_back_to_legacy_configuration(): void + { + config([ + 'smart-extract.api_host' => 'https://legacy.example.com', + 'smart-extract.client_id' => 'legacy-client-id', + 'smart-extract.client_secret' => 'legacy-client-secret', + 'smart-extract.dashboard_url' => 'https://legacy.example.com/edit.html', + 'smart-extract.hitl_enabled' => true, + ]); + + $configuration = new SmartExtractConfiguration(); + + $this->assertSame('https://legacy.example.com', $configuration->apiHost()); + $this->assertSame('legacy-client-id', $configuration->clientId()); + $this->assertSame('legacy-client-secret', $configuration->clientSecret()); + $this->assertSame('https://legacy.example.com/edit.html', $configuration->dashboardUrl()); + $this->assertTrue($configuration->hitlEnabled()); + } + + public function test_existing_database_values_never_revive_the_legacy_fallback(): void + { + config([ + 'smart-extract.api_host' => 'https://legacy.example.com', + 'smart-extract.client_id' => 'legacy-client-id', + 'smart-extract.client_secret' => 'legacy-client-secret', + 'smart-extract.dashboard_url' => 'https://legacy.example.com/edit.html', + 'smart-extract.hitl_enabled' => true, + ]); + + foreach ([ + SmartExtractConfiguration::API_HOST => '', + SmartExtractConfiguration::CLIENT_ID => null, + SmartExtractConfiguration::CLIENT_SECRET => false, + SmartExtractConfiguration::DASHBOARD_URL => ' ', + SmartExtractConfiguration::HITL_ENABLED => 'false', + ] as $name => $value) { + EnvironmentVariable::factory()->create([ + 'name' => $name, + 'value' => $value, + ]); + } + + $configuration = new SmartExtractConfiguration(); + + $this->assertNull($configuration->apiHost()); + $this->assertNull($configuration->clientId()); + $this->assertNull($configuration->clientSecret()); + $this->assertNull($configuration->dashboardUrl()); + $this->assertFalse($configuration->hitlEnabled()); + } + + public function test_scoped_configuration_refreshes_on_the_next_lifecycle(): void + { + $apiHost = EnvironmentVariable::factory()->create([ + 'name' => SmartExtractConfiguration::API_HOST, + 'value' => 'https://first.example.com', + ]); + + $currentLifecycle = app(SmartExtractConfiguration::class); + $this->assertSame('https://first.example.com', $currentLifecycle->apiHost()); + + $apiHost->value = 'https://second.example.com'; + $apiHost->save(); + + $this->assertSame('https://first.example.com', $currentLifecycle->apiHost()); + + app()->forgetScopedInstances(); + $nextLifecycle = app(SmartExtractConfiguration::class); + + $this->assertNotSame($currentLifecycle, $nextLifecycle); + $this->assertSame('https://second.example.com', $nextLifecycle->apiHost()); + } + + private function createConfigurationVariables(): void + { + foreach ([ + SmartExtractConfiguration::API_HOST => 'https://extract.example.com', + SmartExtractConfiguration::CLIENT_ID => 'client-id', + SmartExtractConfiguration::CLIENT_SECRET => 'client-secret', + SmartExtractConfiguration::DASHBOARD_URL => 'https://dashboard.example.com/edit.html', + SmartExtractConfiguration::HITL_ENABLED => 'true', + ] as $name => $value) { + EnvironmentVariable::factory()->create([ + 'name' => $name, + 'value' => $value, + ]); + } + } +}