diff --git a/.github/workflows/deploy-pm4.yml b/.github/workflows/deploy-pm4.yml index d99fc6baa8..55bc017ae3 100644 --- a/.github/workflows/deploy-pm4.yml +++ b/.github/workflows/deploy-pm4.yml @@ -27,7 +27,7 @@ env: GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }} BUILD_BASE: ${{ (contains(github.event.pull_request.body, 'ci:build-base') || github.event_name == 'schedule') && '1' || '0' }} BASE_IMAGE: ${{ secrets.REGISTRY_HOST }}/processmaker/processmaker:base - K8S_BRANCH: ${{ contains(github.event.pull_request.body, 'ci:next') && 'next' || 'develop' }} + K8S_BRANCH: ${{ contains(github.event.pull_request.body, 'ci:next') && 'next' || 'develop' }} concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -145,6 +145,76 @@ jobs: run: | echo "Instance URL: '${INSTANCE_URL}'" bash argocd/gh_comment.sh "$CI_PROJECT" "$pull_req_id" + runAPITest: + name: Run API Tests + needs: [deployEKS] + if: contains(github.event.pull_request.body, 'ci:api-test') + runs-on: self-hosted + steps: + - name: Clone private repository + run: | + git clone --depth 1 -b eng "https://$GITHUB_TOKEN@github.com/ProcessMaker/argocd.git" argocd + - name: Install pm4-tools + run: | + git clone --depth 1 -b "$K8S_BRANCH" "https://$GITHUB_TOKEN@github.com/ProcessMaker/pm4-k8s-distribution.git" pm4-k8s-distribution + echo "versionHelm=$(grep "version:" "pm4-k8s-distribution/charts/enterprise/Chart.yaml" | awk '{print $2}' | sed 's/\"//g')" >> $GITHUB_ENV + cd pm4-k8s-distribution/images/pm4-tools + composer install --no-interaction + cd .. + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID1 }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY1 }} + aws-region: ${{ secrets.AWS_REGION }} + - name: Set up kubectl + run: | + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + - name: Authenticate with Amazon EKS + run: aws eks update-kubeconfig --region us-east-1 --name pm4-eng + - name: Run the API tests + run: | + cd argocd + deploy=$(echo -n ${{env.IMAGE_TAG}} | md5sum | head -c 10) + namespace="ci-$deploy-ns-pm4" + pr_body=$(jq -r .pull_request.body < "$GITHUB_EVENT_PATH" | base64) + kubectl get pods --namespace=$namespace + pod_names=$(kubectl get pods --namespace=$namespace --field-selector=status.phase=Running -o jsonpath="{.items[*].metadata.name}" | tr ' ' '\n' | grep -E '(-processmaker-scheduler-)') + for pod in $pod_names; do + code=' + has_processmaker=$(ls /opt | grep processmaker) + has_sudo=$(ls /usr/bin | grep sudo) + has_php=$(ls /usr/bin | grep php) + if [ ! -z "$has_processmaker" ] && [ ! -z "$has_sudo" ] && [ ! -z "$has_php" ]; then + echo $pr_body | base64 -d > /tmp/pr_body + cd /opt/processmaker + sudo -u nginx php artisan pm4-api-testing:run --body="$pr_body" + else + exit 1 + fi' + kubectl exec -n $namespace $pod -- /bin/sh -c "pr_body='${pr_body}';${code}" | tee /tmp/comment.md && break || true + done + # Send the content of /tmp/comment.md as a PR comment + MESSAGE=$(cat /tmp/comment.md) + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY=${{ github.repository }} + PR_NUMBER=$(jq -r .number < "$GITHUB_EVENT_PATH") + + if [ -z "$PR_NUMBER" ]; then + echo "The PR number is not available. Make sure this script is executed in a context of Pull Request." + exit 1 + fi + + URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" + json_payload=$(jq -n --arg message "$MESSAGE" '{"body": $message}') + + curl -s \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github.v3+json" \ + -d "$json_payload" \ + "${URL}" deleteEKS: name: Delete Instance if: github.event.action == 'closed' diff --git a/ProcessMaker/Console/Commands/BuildScriptExecutors.php b/ProcessMaker/Console/Commands/BuildScriptExecutors.php index 07bb9b40eb..fa624ce614 100644 --- a/ProcessMaker/Console/Commands/BuildScriptExecutors.php +++ b/ProcessMaker/Console/Commands/BuildScriptExecutors.php @@ -8,6 +8,7 @@ use ProcessMaker\Exception\InvalidDockerImageException; use ProcessMaker\Facades\Docker; use ProcessMaker\Models\ScriptExecutor; +use ProcessMaker\ScriptRunners\Base; class BuildScriptExecutors extends Command { @@ -157,6 +158,16 @@ public function buildExecutor() $command = Docker::command() . " build --build-arg SDK_DIR=./sdk -t {$image} -f {$packagePath}/Dockerfile.custom {$packagePath}"; + $this->execCommand($command); + + $isNayra = $scriptExecutor->language === Base::NAYRA_LANG; + if ($isNayra) { + Base::bringUpNayraExecutor($this, $image); + } + } + + public function execCommand(string $command) + { if ($this->userId) { $this->runProc( $command, @@ -185,7 +196,7 @@ public function info($text, $verbosity = null) parent::info($text, $verbosity); } - private function sendEvent($output, $status) + public function sendEvent($output, $status) { if ($this->userId) { event(new BuildScriptExecutor($output, $this->userId, $status)); diff --git a/ProcessMaker/Console/Commands/DockerExecutorPhpNayra.php b/ProcessMaker/Console/Commands/DockerExecutorPhpNayra.php new file mode 100644 index 0000000000..08586bc99f --- /dev/null +++ b/ProcessMaker/Console/Commands/DockerExecutorPhpNayra.php @@ -0,0 +1,46 @@ +exists(); + if (!$exists) { + ScriptExecutor::install([ + 'language' => Base::NAYRA_LANG, + 'title' => 'Nayra (µService)', + 'description' => 'Nayra (µService) Executor', + 'config' => '', + ]); + } + + // Build the instance image. This is the same as if you were to build it from the admin UI + Artisan::call('processmaker:build-script-executor ' . Base::NAYRA_LANG . ' --rebuild'); + } +} diff --git a/ProcessMaker/Events/ActivityAssigned.php b/ProcessMaker/Events/ActivityAssigned.php index 3eeb13190f..75c51c5fde 100644 --- a/ProcessMaker/Events/ActivityAssigned.php +++ b/ProcessMaker/Events/ActivityAssigned.php @@ -60,4 +60,15 @@ public function getProcessRequestToken() { return $this->processRequestToken; } + + /** + * Get data + */ + public function getData(): array + { + return [ + 'payloadUrl' => $this->payloadUrl, + 'element_type' => $this->processRequestToken->element_type, + ]; + } } diff --git a/ProcessMaker/Events/ProcessUpdated.php b/ProcessMaker/Events/ProcessUpdated.php index 7c96abc964..e1bf14f692 100644 --- a/ProcessMaker/Events/ProcessUpdated.php +++ b/ProcessMaker/Events/ProcessUpdated.php @@ -8,6 +8,7 @@ use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; use ProcessMaker\Models\ProcessRequest; +use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface; class ProcessUpdated implements ShouldBroadcastNow { @@ -17,6 +18,10 @@ class ProcessUpdated implements ShouldBroadcastNow public $event; + public $tokenId; + + public $elementType; + private $processRequest; /** @@ -24,12 +29,17 @@ class ProcessUpdated implements ShouldBroadcastNow * * @return void */ - public function __construct(ProcessRequest $processRequest, $event) + public function __construct(ProcessRequest $processRequest, $event, TokenInterface $token = null) { $this->payloadUrl = route('api.requests.show', ['request' => $processRequest->getKey()]); $this->event = $event; $this->processRequest = $processRequest; + + if ($token) { + $this->tokenId = $token->getId(); + $this->elementType = $token->element_type; + } } /** diff --git a/ProcessMaker/Http/Controllers/Api/V1_1/TaskController.php b/ProcessMaker/Http/Controllers/Api/V1_1/TaskController.php new file mode 100644 index 0000000000..4a7dd3bfc5 --- /dev/null +++ b/ProcessMaker/Http/Controllers/Api/V1_1/TaskController.php @@ -0,0 +1,102 @@ +defaultFields) + ->where('element_type', 'task'); + + $this->processFilters(request(), $query); + $pagination = $query->paginate(); + $perPage = $pagination->perPage(); + $page = $pagination->currentPage(); + $lastPage = $pagination->lastPage(); + return [ + 'data' => $pagination->items(), + 'meta' => [ + 'total' => $pagination->total(), + 'perPage' => $pagination->perPage(), + 'currentPage' => $pagination->currentPage(), + 'lastPage' => $pagination->lastPage(), + 'count' => $pagination->count(), + 'from' => $perPage * ($page - 1) + 1, + 'last_page' => $lastPage, + 'path' => '/', + 'per_page' => $perPage, + 'to' => $perPage * ($page - 1) + $perPage, + 'total_pages' => ceil($pagination->count() / $perPage), + ], + ]; + } + + private function processFilters(Request $request, Builder $query) + { + if ($request->has('process_request_id')) { + $query->where('process_request_id', $request->input('process_request_id')); + } + if ($request->has('status')) { + $query->where('status', $request->input('status')); + } + } + + public function show(ProcessRequestToken $task) + { + $resource = TaskResource::preprocessInclude(request(), ProcessRequestToken::where('id', $task->id)); + return $resource->toArray(request()); + } + + public function showScreen($taskId) + { + $task = ProcessRequestToken::select( + array_merge($this->defaultFields, ['process_request_id', 'process_id']) + )->findOrFail($taskId); + $response = new TaskScreen($task); + $response = response($response->toArray(request())['screen'], 200); + $now = time(); + // screen cache time + $cacheTime = config('screen_task_cache_time', 86400); + $response->headers->set('Cache-Control', 'max-age=' . $cacheTime . ', must-revalidate, public'); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', $now + $cacheTime) . ' GMT'); + return $response; + } + + public function showInterstitial($taskId) + { + $task = ProcessRequestToken::select( + array_merge($this->defaultFields, ['process_request_id', 'process_id']) + )->findOrFail($taskId); + $response = new TaskInterstitialResource($task); + $response = response($response->toArray(request())['screen'], 200); + $now = time(); + // screen cache time + $cacheTime = config('screen_task_cache_time', 86400); + $response->headers->set('Cache-Control', 'max-age=' . $cacheTime . ', must-revalidate, public'); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', $now + $cacheTime) . ' GMT'); + return $response; + } +} diff --git a/ProcessMaker/Http/Resources/Task.php b/ProcessMaker/Http/Resources/Task.php index 2e4581c3f0..00dc8e04c6 100644 --- a/ProcessMaker/Http/Resources/Task.php +++ b/ProcessMaker/Http/Resources/Task.php @@ -113,20 +113,6 @@ private function addAssignableUsers(&$array, $include) } } - private function loadUserRequestPermission(ProcessRequest $request, User $user, array $permissions) - { - $permissions[] = [ - 'process_request_id' => $request->id, - 'allowed' => $user ? $user->can('view', $request) : false, - ]; - - if ($request->parentRequest && $user) { - $permissions = $this->loadUserRequestPermission($request->parentRequest, $user, $permissions); - } - - return $permissions; - } - /** * Add the active users to the list of assigned users * @@ -161,16 +147,4 @@ private function addActiveAssignedGroupMembers(array $groups, array $assignedUse { return (new Process)->getConsolidatedUsers($groups, $assignedUsers); } - - private function getData() - { - if ($this->loadedData) { - return $this->loadedData; - } - $dataManager = new DataManager(); - $task = $this->resource->loadTokenInstance(); - $this->loadedData = $dataManager->getData($task); - - return $this->loadedData; - } } diff --git a/ProcessMaker/Http/Resources/V1_1/TaskInterstitialResource.php b/ProcessMaker/Http/Resources/V1_1/TaskInterstitialResource.php new file mode 100644 index 0000000000..cedfa8a5f4 --- /dev/null +++ b/ProcessMaker/Http/Resources/V1_1/TaskInterstitialResource.php @@ -0,0 +1,19 @@ +includeInterstitial(); + } +} diff --git a/ProcessMaker/Http/Resources/V1_1/TaskResource.php b/ProcessMaker/Http/Resources/V1_1/TaskResource.php new file mode 100644 index 0000000000..dd9b948f23 --- /dev/null +++ b/ProcessMaker/Http/Resources/V1_1/TaskResource.php @@ -0,0 +1,176 @@ + ['id', 'firstname', 'lastname', 'email', 'username', 'avatar'], + 'requestor' => ['id', 'first_name', 'last_name', 'email'], + 'processRequest' => ['id', 'process_id', 'process_version_id', 'callable_id', 'status'], + 'draft' => ['id', 'task_id', 'data'], + 'screen' => ['id', 'config'], + 'process' => ['id', 'name'], + ]; + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request + * @return array + */ + public function toArray($request) + { + $array = [ + 'advancedStatus' => $this->advanceStatus, + ]; + foreach (self::$defaultFields as $field) { + $array[$field] = $this->$field; + } + + return $this->processInclude($request, $array); + } + + private static function addRelationshipKeyColumn(Builder $query, string $relationship): bool + { + $model = $query->getModel(); + if (!method_exists($model, $relationship)) { + return false; + } + $relationshipObject = $model->$relationship(); + if (!($relationshipObject instanceof Relation)) { + return false; + } + + if ($relationshipObject instanceof BelongsTo) { + $relationshipKey = $relationshipObject->getForeignKeyName(); + $query->addSelect($relationshipKey); + } + + return true; + } + + private static function addRelationship(ProcessRequestToken $model, string $relationship): bool + { + if (!method_exists($model, $relationship)) { + return false; + } + $relationshipObject = $model->$relationship(); + if (!($relationshipObject instanceof Relation)) { + return false; + } + + $relationshipColumns = self::$defaultFieldsFor[$relationship] ?? ['id']; + $model->$relationship = $relationshipObject->select($relationshipColumns)->getResults(); + + return true; + } + + public static function preprocessInclude(Request $request, Builder $query): self + { + foreach (self::$defaultFields as $field) { + $query->addSelect($field); + } + + $include = $request->query('include', []); + if ($include) { + $include = explode(',', $include); + } + $include = array_merge($include, self::$defaultIncludes); + + if (in_array('data', $include)) { + $query->addSelect('process_request_id'); + self::$defaultFieldsFor['processRequest'][] = 'data'; + } + + foreach (self::$includeMethods as $key) { + if (!in_array($key, $include)) { + continue; + } + + self::addRelationshipKeyColumn($query, $key); + } + + $model = $query->first(); + foreach (self::$includeMethods as $key) { + if (!in_array($key, $include)) { + continue; + } + + self::addRelationship($model, $key); + } + + return new static($model); + } + + private function processInclude(Request $request, array $array) + { + $include = $request->query('include', []); + if ($include) { + $include = explode(',', $include); + } + + foreach (self::$includeMethods as $key) { + if (!in_array($key, $include)) { + continue; + } + + $method = self::INCLUDE_PREFIX . ucfirst($key); + if (method_exists($this, $method)) { + $attributes = $this->$method(); + $array = array_merge($array, $attributes); + } else { + $array[$key] = $this->$key; + } + } + return $array; + } +} diff --git a/ProcessMaker/Http/Resources/V1_1/TaskScreen.php b/ProcessMaker/Http/Resources/V1_1/TaskScreen.php new file mode 100644 index 0000000000..51d4eb775f --- /dev/null +++ b/ProcessMaker/Http/Resources/V1_1/TaskScreen.php @@ -0,0 +1,59 @@ +includeScreen($request); + } + + private function includeScreen($request) + { + $array = ['screen' => null]; + + $screen = $this->getScreenVersion(); + + if ($screen) { + if ($screen->type === 'ADVANCED') { + $array['screen'] = $screen; + } else { + $resource = new ScreenVersionResource($screen); + $array['screen'] = $resource->toArray($request); + } + } else { + $array['screen'] = null; + } + + if ($array['screen']) { + // Apply translations to screen + $processTranslation = new ProcessTranslation($this->process); + $array['screen']['config'] = $processTranslation->applyTranslations($array['screen']); + $array['screen']['config'] = $this->removeInspectorMetadata($array['screen']['config']); + + // Apply translations to nested screens + if (array_key_exists('nested', $array['screen'])) { + foreach ($array['screen']['nested'] as &$nestedScreen) { + $nestedScreen['config'] = $processTranslation->applyTranslations($nestedScreen); + $nestedScreen['config'] = $this->removeInspectorMetadata($nestedScreen['config']); + } + } + } + + return $array; + } +} diff --git a/ProcessMaker/Jobs/ExecuteScript.php b/ProcessMaker/Jobs/ExecuteScript.php index a9c8ad88f2..f82147a07c 100644 --- a/ProcessMaker/Jobs/ExecuteScript.php +++ b/ProcessMaker/Jobs/ExecuteScript.php @@ -11,6 +11,7 @@ use ProcessMaker\Events\ScriptResponseEvent; use ProcessMaker\Models\Script; use ProcessMaker\Models\User; +use ProcessMaker\Nayra\Managers\WorkflowManagerRabbitMq; use Throwable; class ExecuteScript implements ShouldQueue @@ -62,7 +63,12 @@ public function __construct(ScriptInterface $script, User $current_user, $code, */ public function handle() { - //throw new \Exception('This method must be overridden.'); + $useNayraDocker = !empty(config('app.nayra_rest_api_host')); + $isPhp = strtolower($this->script->language) === 'php'; + if ($isPhp && $useNayraDocker && $this->sync) { + return $this->handleNayraDocker(); + } + try { // Just set the code but do not save the object (preview only) $this->script->code = $this->code; @@ -82,6 +88,36 @@ public function handle() } } + /** + * Execute the script task using Nayra Docker. + * + * @return string|bool + */ + public function handleNayraDocker() + { + $engine = new WorkflowManagerRabbitMq(); + $params = [ + 'name' => uniqid('script_', true), + 'script' => $this->code, + 'data' => $this->data, + 'config' => $this->configuration, + 'envVariables' => $engine->getEnvironmentVariables(), + ]; + $body = json_encode($params); + $url = config('app.nayra_rest_api_host') . '/run_script'; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($body), + ]); + $result = curl_exec($ch); + curl_close($ch); + return $result; + } + /** * Send a response to the user interface * diff --git a/ProcessMaker/LicensedPackageManifest.php b/ProcessMaker/LicensedPackageManifest.php index a4ef08cc6e..520784ecf3 100644 --- a/ProcessMaker/LicensedPackageManifest.php +++ b/ProcessMaker/LicensedPackageManifest.php @@ -59,7 +59,7 @@ private function parseLicense() private function licensedPackages() { - $default = collect(['packages']); + $default = collect(['packages','pm4-api-testing']); $data = $this->parseLicense(); $expires = Carbon::parse($data['expires_at']); if ($expires->isPast()) { diff --git a/ProcessMaker/Listeners/BpmnSubscriber.php b/ProcessMaker/Listeners/BpmnSubscriber.php index 7d8a36ebca..bbf6586179 100644 --- a/ProcessMaker/Listeners/BpmnSubscriber.php +++ b/ProcessMaker/Listeners/BpmnSubscriber.php @@ -126,8 +126,9 @@ public function onActivityActivated(ActivityActivatedEvent $event) if ($token->user_id) { $token->sendActivityActivatedNotifications(); } - - event(new ActivityAssigned($event->token)); + if ($event->token->element_type === 'task') { + event(new ActivityAssigned($event->token)); + } } /** diff --git a/ProcessMaker/Managers/TaskSchedulerManager.php b/ProcessMaker/Managers/TaskSchedulerManager.php index 9c516d5319..c7a5ad41e7 100644 --- a/ProcessMaker/Managers/TaskSchedulerManager.php +++ b/ProcessMaker/Managers/TaskSchedulerManager.php @@ -144,7 +144,7 @@ public function scheduleTasks() $this->removeExpiredLocks(); - $tasks = ScheduledTask::all(); + $tasks = ScheduledTask::cursor(); foreach ($tasks as $task) { try { diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index ac282c4deb..6c77a9368d 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -355,8 +355,20 @@ public function getScreensRequested() ->orderBy('completed_at') ->get(); $screens = []; + $definitions = []; foreach ($tokens as $token) { - $definition = $token->getDefinition(); + // Get process related to token + $request = $token->processRequest ?: $token->getInstance(); + $process = $request->processVersion ?: $request->process; + + // Get the definition + if (!empty($definitions[$process->id])) { + $definition = $definitions[$process->id]; + } else { + $definition = $definitions[$process->id] = $token->getDefinition(); + } + + // Get the screen realated to token if (array_key_exists('screenRef', $definition)) { $screen = $token->getScreenVersion(); if ($screen) { @@ -891,11 +903,11 @@ public function getVersionDefinitions($forceParse = false, $engine = null) * * @param string $eventName */ - public function notifyProcessUpdated($eventName) + public function notifyProcessUpdated($eventName, TokenInterface $token = null) { - $event = new ProcessUpdated($this, $eventName); + $event = new ProcessUpdated($this, $eventName, $token); if ($this->parentRequest) { - $this->parentRequest->notifyProcessUpdated($eventName); + $this->parentRequest->notifyProcessUpdated($eventName, $token); } event($event); } diff --git a/ProcessMaker/Models/ProcessRequestToken.php b/ProcessMaker/Models/ProcessRequestToken.php index 85ce41d136..e58a2302b1 100644 --- a/ProcessMaker/Models/ProcessRequestToken.php +++ b/ProcessMaker/Models/ProcessRequestToken.php @@ -1150,6 +1150,8 @@ public function reassign($toUserId, User $requestingUser) // Send a notification to the user $notification = new TaskReassignmentNotification($this); $this->user->notify($notification); - event(new ActivityAssigned($this)); + if ($this->element_type === 'task') { + event(new ActivityAssigned($this)); + } } } diff --git a/ProcessMaker/Models/ScriptDockerNayraTrait.php b/ProcessMaker/Models/ScriptDockerNayraTrait.php new file mode 100644 index 0000000000..399da50e9e --- /dev/null +++ b/ProcessMaker/Models/ScriptDockerNayraTrait.php @@ -0,0 +1,328 @@ + uniqid('script_', true), + 'script' => $code, + 'data' => $data, + 'config' => $config, + 'envVariables' => $envVariables, + 'timeout' => $timeout, + ]; + $body = json_encode($params); + $servers = self::getNayraAddresses(); + if (!$servers) { + $this->bringUpNayra(); + } + $baseUrl = $this->getNayraInstanceUrl(); + $url = $baseUrl . '/run_script'; + $this->ensureNayraServerIsRunning($baseUrl); + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Content-Length: ' . strlen($body), + ]); + $result = curl_exec($ch); + curl_close($ch); + $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE); + if ($httpStatus !== 200) { + $result .= ' HTTP Status: ' . $httpStatus; + $result .= ' URL: ' . $url; + $result .= ' BODY: ' . $body; + Log::error('Error executing script with Nayra Docker', [ + 'url' => $url, + 'httpStatus' => $httpStatus, + 'result' => $result, + ]); + throw new ScriptException($result); + } + return $result; + } + + private function getNayraInstanceUrl() + { + $servers = self::getNayraAddresses(); + return $this->schema . '://' . $servers[0] . ':' . static::$nayraPort; + } + + private function getDockerLogs($instanceName) + { + $docker = Docker::command(); + $logs = []; + exec($docker . " logs {$instanceName}_nayra 2>&1", $logs, $status); + if ($status) { + return 'Error getting logs from Nayra Docker: ' . implode("\n", $logs); + } + return implode("\n", $logs); + } + + /** + * Ensure that the Nayra server is running. + * + * @param string $url URL of the Nayra server + * @return void + * @throws ScriptException If cannot connect to Nayra Service + */ + private function ensureNayraServerIsRunning(string $url) + { + $header = @get_headers($url); + if (!$header) { + $this->bringUpNayra(true); + } + } + + /** + * Bring up Nayra and check the provided URL. + * + * @return void + */ + private function bringUpNayra($restart = false) + { + $docker = Docker::command(); + $instanceName = config('app.instance'); + if (!$restart && $this->findNayraAddresses($docker, $instanceName, 3)) { + // The container is already running + return; + } + + $image = $this->scriptExecutor->dockerImageName(); + //check if image exists + exec($docker . " inspect {$image} 2>&1", $output, $status); + if ($status) { + $this->bringUpNayraContainer(); + } else { + + exec($docker . " stop {$instanceName}_nayra 2>&1 || true"); + exec($docker . " rm {$instanceName}_nayra 2>&1 || true"); + exec( + $docker . ' run -d --name ' . $instanceName . '_nayra ' + . (config('app.nayra_docker_network') + ? '--network=' . config('app.nayra_docker_network') . ' ' + : '') + . $image, + $output, + $status + ); + if ($status) { + Log::error('Error starting Nayra Docker', [ + 'output' => $output, + 'status' => $status, + ]); + throw new ScriptException('Error starting Nayra Docker'); + } + } + $this->waitContainerNetwork($docker, $instanceName); + $url = $this->getNayraInstanceUrl(); + $this->nayraServiceIsRunning($url); + } + + private function bringUpNayraContainer() + { + $lang = Base::NAYRA_LANG; + Artisan::call("processmaker:build-script-executor {$lang} --rebuild"); + } + + /** + * Waits for the container network to be ready. + * + * @param Docker $docker The Docker instance. + * @param string $instanceName The name of the container instance. + */ + private function waitContainerNetwork($docker, $instanceName) + { + if (!$this->findNayraAddresses($docker, $instanceName, 30)) { + throw new ScriptException('Could not get address of the nayra container'); + } + } + + /** + * Find the Nayra addresses. + * + * @param Docker $docker The Docker instance. + * @param string $instanceName The name of the container instance. + * @return bool Returns true if the Nayra addresses were found, false otherwise. + */ + private function findNayraAddresses($docker, $instanceName, $times): bool + { + $ip = ''; + for ($i = 0; $i < $times; $i++) { + if ($i > 0) { + sleep(1); + } + $ip = exec( + $docker . ' inspect --format ' + . (config('app.nayra_docker_network') + ? "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'" + : "'{{ .NetworkSettings.IPAddress }}'" + ) + . " {$instanceName}_nayra 2>/dev/null", + $output, + $status + ); + if ($status) { + $ip = ''; + } + if ($ip) { + self::setNayraAddresses([$ip]); + return true; + } + } + + return false; + } + + /** + * Checks if the Nayra service is running. + * + * @param string $url The URL of the Nayra service. + * @return bool Returns true if the Nayra service is running, false otherwise. + */ + private function nayraServiceIsRunning($url): bool + { + for ($i = 0; $i < 30; $i++) { + if ($i > 0) { + sleep(1); + } + $status = @get_headers($url); + if ($status) { + return true; + } + } + throw new ScriptException('Could not connect to the nayra container'); + } + + public static function getNayraAddresses() + { + // Check if it is running in unit test mode with Cache ArrayStore + $isArrayDriver = self::isCacheArrayStore(); + if ($isArrayDriver) { + return Cache::store('file')->get('nayra_ips'); + } + + return Cache::get('nayra_ips'); + } + + public static function setNayraAddresses(array $addresses) + { + // Check if it is running in unit test mode with Cache ArrayStore + $isArrayDriver = self::isCacheArrayStore(); + if ($isArrayDriver) { + return Cache::store('file')->forever('nayra_ips', $addresses); + } + + Cache::forever('nayra_ips', $addresses); + } + + public static function clearNayraAddresses() + { + // Check if it is running in unit test mode with Cache ArrayStore + $isArrayDriver = self::isCacheArrayStore(); + if ($isArrayDriver) { + return Cache::store('file')->forget('nayra_ips'); + } + + Cache::forget('nayra_ips'); + } + + private static function isCacheArrayStore(): bool + { + $cacheDriver = Cache::getFacadeRoot()->getStore(); + return $cacheDriver instanceof ArrayStore; + } + + public static function bringUpNayraExecutor(BuildScriptExecutors $builder, string $image) + { + $instanceName = config('app.instance'); + $builder->info('Stop existing nayra container'); + $builder->execCommand(Docker::command() . " stop {$instanceName}_nayra 2>&1 || true"); + $builder->execCommand(Docker::command() . " rm {$instanceName}_nayra 2>&1 || true"); + $builder->info('Bring up the nayra container'); + $builder->execCommand( + Docker::command() . ' run -d --name ' . $instanceName . '_nayra ' + . (config('app.nayra_docker_network') + ? '--network=' . config('app.nayra_docker_network') . ' ' + : '') + . $image + ); + $builder->info('Get IP address of the nayra container'); + $ip = ''; + for ($i = 0; $i < 10; $i++) { + $ip = exec( + Docker::command() + . ' inspect --format ' + . (config('app.nayra_docker_network') + ? "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'" + : "'{{ .NetworkSettings.IPAddress }}'" + ) + . " {$instanceName}_nayra 2>/dev/null" + ); + if ($ip) { + $builder->info('Nayra container IP: ' . $ip); + static::setNayraAddresses([$ip]); + $builder->sendEvent(0, 'done'); + break; + } + sleep(1); + } + if (!$ip) { + throw new UnexpectedValueException('Could not get IP address of the nayra container'); + } + } + + /** + * Initialize the phpunit test network for Nayra. + */ + public static function initNayraPhpUnitTest() + { + Base::clearNayraAddresses(); + $network = config('app.nayra_docker_network'); + // Check if docker network exists, if not create it + exec(Docker::command() . " network inspect {$network} 2>&1", $output, $status); + if ($status) { + exec(Docker::command() . " network create {$network} 2>&1", $output, $status); + if ($status) { + throw new UnexpectedValueException('Could not create docker network'); + } + } + } +} diff --git a/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php b/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php index 5dd2fd75ec..bed0984384 100644 --- a/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php +++ b/ProcessMaker/Nayra/Managers/WorkflowManagerRabbitMq.php @@ -682,7 +682,7 @@ private function dispatchAction(array $action, $subject = null): void * * @return array */ - private function getEnvironmentVariables() + public function getEnvironmentVariables() { $environmentVariables = []; EnvironmentVariable::chunk(50, function ($variables) use (&$environmentVariables) { diff --git a/ProcessMaker/Providers/RouteServiceProvider.php b/ProcessMaker/Providers/RouteServiceProvider.php index c0761334e6..1315214a1d 100644 --- a/ProcessMaker/Providers/RouteServiceProvider.php +++ b/ProcessMaker/Providers/RouteServiceProvider.php @@ -89,6 +89,8 @@ protected function mapApiRoutes() { Route::middleware('api') ->group(base_path('routes/api.php')); + Route::middleware('auth:api') + ->group(base_path('routes/v1_1/api.php')); } /** diff --git a/ProcessMaker/Repositories/TokenRepository.php b/ProcessMaker/Repositories/TokenRepository.php index 471e42d34b..af5e72d07e 100644 --- a/ProcessMaker/Repositories/TokenRepository.php +++ b/ProcessMaker/Repositories/TokenRepository.php @@ -155,7 +155,7 @@ public function persistActivityActivated(ActivityInterface $activity, TokenInter $token->saveOrFail(); $token->setId($token->getKey()); $request = $token->getInstance(); - $request->notifyProcessUpdated('ACTIVITY_ACTIVATED'); + $request->notifyProcessUpdated('ACTIVITY_ACTIVATED', $token); $this->instanceRepository->persistInstanceUpdated($token->getInstance()); } @@ -214,7 +214,7 @@ public function persistStartEventTriggered(StartEventInterface $startEvent, Coll $token->saveOrFail(); $token->setId($token->getKey()); $request = $token->getInstance(); - $request->notifyProcessUpdated('START_EVENT_TRIGGERED'); + $request->notifyProcessUpdated('START_EVENT_TRIGGERED', $token); } private function assignTaskUser(ActivityInterface $activity, TokenInterface $token, Instance $instance) @@ -246,7 +246,7 @@ public function persistActivityException(ActivityInterface $activity, TokenInter $token->save(); $token->setId($token->getKey()); $request = $token->getInstance(); - $request->notifyProcessUpdated('ACTIVITY_EXCEPTION'); + $request->notifyProcessUpdated('ACTIVITY_EXCEPTION', $token); } /** @@ -280,7 +280,7 @@ public function persistActivityCompleted(ActivityInterface $activity, TokenInter $token->save(); $token->setId($token->getKey()); $request = $token->getInstance(); - $request->notifyProcessUpdated('ACTIVITY_COMPLETED'); + $request->notifyProcessUpdated('ACTIVITY_COMPLETED', $token); } /** diff --git a/ProcessMaker/RollbackProcessRequest.php b/ProcessMaker/RollbackProcessRequest.php index fdf3c52323..e7245074ce 100644 --- a/ProcessMaker/RollbackProcessRequest.php +++ b/ProcessMaker/RollbackProcessRequest.php @@ -148,7 +148,9 @@ private function rollbackToTask() if ($this->newTask->user_id) { $this->newTask->sendActivityActivatedNotifications(); } - event(new ActivityAssigned($this->newTask)); + if ($this->newTask->element_type === 'task') { + event(new ActivityAssigned($this->newTask)); + } $this->addComment(); } diff --git a/ProcessMaker/ScriptRunners/Base.php b/ProcessMaker/ScriptRunners/Base.php index cf1232d8e9..c503be1ee6 100644 --- a/ProcessMaker/ScriptRunners/Base.php +++ b/ProcessMaker/ScriptRunners/Base.php @@ -2,11 +2,14 @@ namespace ProcessMaker\ScriptRunners; +use Carbon\Carbon; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use ProcessMaker\GenerateAccessToken; use ProcessMaker\Models\EnvironmentVariable; use ProcessMaker\Models\ScriptDockerBindingFilesTrait; use ProcessMaker\Models\ScriptDockerCopyingFilesTrait; +use ProcessMaker\Models\ScriptDockerNayraTrait; use ProcessMaker\Models\ScriptExecutor; use ProcessMaker\Models\User; use RuntimeException; @@ -15,6 +18,9 @@ abstract class Base { use ScriptDockerCopyingFilesTrait; use ScriptDockerBindingFilesTrait; + use ScriptDockerNayraTrait; + + const NAYRA_LANG = 'php-nayra'; private $tokenId = ''; @@ -38,7 +44,7 @@ abstract public function config($code, array $dockerConfig); /** * Set the script executor * - * @var \ProcessMaker\Models\User + * @var \ProcessMaker\Models\ScriptExecutor */ private $scriptExecutor; @@ -61,8 +67,10 @@ public function __construct(ScriptExecutor $scriptExecutor) */ public function run($code, array $data, array $config, $timeout, ?User $user) { + $isNayra = $this->scriptExecutor->language === self::NAYRA_LANG; + // Prepare the docker parameters - $environmentVariables = $this->getEnvironmentVariables(); + $environmentVariables = $this->getEnvironmentVariables(!$isNayra); if (!getenv('HOME')) { putenv('HOME=' . base_path()); } @@ -70,13 +78,24 @@ public function run($code, array $data, array $config, $timeout, ?User $user) // Create tokens for the SDK if a user is set $token = null; if ($user) { - $token = new GenerateAccessToken($user); - $environmentVariables[] = 'API_TOKEN=' . $token->getToken(); + $expires = Carbon::now()->addWeek(); + $accessToken = Cache::remember('script-runner-' . $user->id, $expires, function () use ($user) { + $user->removeOldRunScriptTokens(); + $token = new GenerateAccessToken($user); + return $token->getToken(); + }); + $environmentVariables[] = 'API_TOKEN=' . (!$isNayra ? escapeshellarg($accessToken) : $accessToken); $environmentVariables[] = 'API_HOST=' . config('app.docker_host_url') . '/api/1.0'; $environmentVariables[] = 'APP_URL=' . config('app.docker_host_url'); $environmentVariables[] = 'API_SSL_VERIFY=' . (config('app.api_ssl_verify') ? '1' : '0'); } + // Nayra Executor + if ($isNayra) { + $response = $this->handleNayraDocker($code, $data, $config, $timeout, $environmentVariables); + return json_decode($response, true); + } + if ($environmentVariables) { $parameters = '-e ' . implode(' -e ', $environmentVariables); } else { @@ -146,14 +165,19 @@ public function run($code, array $data, array $config, $timeout, ?User $user) /** * Get the environment variables. * + * @param bool $useEscape * @return array */ - private function getEnvironmentVariables() + private function getEnvironmentVariables($useEscape = true) { $variablesParameter = []; - EnvironmentVariable::chunk(50, function ($variables) use (&$variablesParameter) { + EnvironmentVariable::chunk(50, function ($variables) use (&$variablesParameter, $useEscape) { foreach ($variables as $variable) { - $variablesParameter[] = escapeshellarg($variable['name']) . '=' . escapeshellarg($variable['value']); + if ($useEscape) { + $variablesParameter[] = escapeshellarg($variable['name']) . '=' . escapeshellarg($variable['value']); + } else { + $variablesParameter[] = $variable['name'] . '=' . $variable['value']; + } } }); diff --git a/ProcessMaker/Traits/TaskResourceIncludes.php b/ProcessMaker/Traits/TaskResourceIncludes.php index 8b8db4a0f7..6c046cd905 100644 --- a/ProcessMaker/Traits/TaskResourceIncludes.php +++ b/ProcessMaker/Traits/TaskResourceIncludes.php @@ -6,12 +6,29 @@ use ProcessMaker\Http\Resources\ScreenVersion as ScreenVersionResource; use ProcessMaker\Http\Resources\Users; use ProcessMaker\Managers\DataManager; +use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\TaskDraft; +use ProcessMaker\Models\User; use ProcessMaker\ProcessTranslations\ProcessTranslation; use StdClass; trait TaskResourceIncludes { + use TaskScreenResourceTrait; + + + private function getData() + { + if ($this->loadedData) { + return $this->loadedData; + } + $dataManager = new DataManager(); + $task = $this->resource->loadTokenInstance(); + $this->loadedData = $dataManager->getData($task); + + return $this->loadedData; + } + private function includeData() { return ['data' => $this->getData()]; @@ -29,7 +46,7 @@ private function includeRequestor() private function includeProcessRequest() { - return ['process_request' => new Users($this->processRequest)]; + return ['process_request' => $this->processRequest->attributesToArray()]; } private function includeDraft() @@ -72,11 +89,13 @@ private function includeScreen($request) // Apply translations to screen $processTranslation = new ProcessTranslation($this->process); $array['screen']['config'] = $processTranslation->applyTranslations($array['screen']); + $array['screen']['config'] = $this->removeInspectorMetadata($array['screen']['config']); // Apply translations to nested screens if (array_key_exists('nested', $array['screen'])) { foreach ($array['screen']['nested'] as &$nestedScreen) { $nestedScreen['config'] = $processTranslation->applyTranslations($nestedScreen); + $nestedScreen['config'] = $this->removeInspectorMetadata($nestedScreen['config']); } } } @@ -116,7 +135,7 @@ private function includeProcess() return ['process' => $this->process]; } - private function includeInterstitial() + public function includeInterstitial() { $interstitial = $this->getInterstitial(); @@ -125,6 +144,11 @@ private function includeInterstitial() $translatedConf = $processTranslation->applyTranslations($interstitial['interstitial_screen']); $interstitial['interstitial_screen']['config'] = $translatedConf; + // Remove inspector metadata + $interstitial['interstitial_screen']['config'] = $this->removeInspectorMetadata( + $interstitial['interstitial_screen']['config'] ?: [] + ); + return [ 'allow_interstitial' => $interstitial['allow_interstitial'], 'interstitial_screen' => $interstitial['interstitial_screen'], @@ -137,4 +161,18 @@ private function includeUserRequestPermission() return ['user_request_permission' => $userRequestPermission]; } + + private function loadUserRequestPermission(ProcessRequest $request, User $user, array $permissions) + { + $permissions[] = [ + 'process_request_id' => $request->id, + 'allowed' => $user ? $user->can('view', $request) : false, + ]; + + if ($request->parentRequest && $user) { + $permissions = $this->loadUserRequestPermission($request->parentRequest, $user, $permissions); + } + + return $permissions; + } } diff --git a/ProcessMaker/Traits/TaskScreenResourceTrait.php b/ProcessMaker/Traits/TaskScreenResourceTrait.php new file mode 100644 index 0000000000..bf0d97774e --- /dev/null +++ b/ProcessMaker/Traits/TaskScreenResourceTrait.php @@ -0,0 +1,45 @@ + $page) { + $config[$i]['items'] = $this->removeInspectorMetadataItems($page['items']); + } + return $config; + } + + /** + * Removes the inspector metadata from the screen configuration items + * + * @param array $items + * @return array + */ + private function removeInspectorMetadataItems(array $items) + { + foreach ($items as $i => $item) { + if (isset($item['inspector'])) { + unset($item['inspector']); + } + if (isset($item['component']) && $item['component'] === 'FormMultiColumn') { + foreach ($item['items'] as $c => $col) { + $item['items'][$c] = $this->removeInspectorMetadataItems($col); + } + } elseif (isset($item['items']) && is_array($item['items'])) { + $item['items'] = $this->removeInspectorMetadataItems($item['items']); + } + $items[$i] = $item; + } + return $items; + } +} diff --git a/composer.json b/composer.json index d21d7fc25d..e6d3f1d361 100644 --- a/composer.json +++ b/composer.json @@ -173,6 +173,7 @@ "package-versions": "dev-next", "package-vocabularies": "dev-next", "package-webentry": "dev-next", + "pm4-api-testing": "*", "packages": "^0" }, "docker-executors": { diff --git a/config/app.php b/config/app.php index 9bc7694e76..c0de4355eb 100644 --- a/config/app.php +++ b/config/app.php @@ -99,6 +99,8 @@ env('APP_URL', 'http://localhost') ) ), + 'nayra_rest_api_host' => env('NAYRA_REST_API_HOST', ''), + 'screen_task_cache_time' => env('SCREEN_TASK_CACHE_TIME', 86400), // Allows our script executors to ignore invalid SSL. This should only be set to false for development. 'api_ssl_verify' => env('API_SSL_VERIFY', 'true'), @@ -238,4 +240,6 @@ 'task_drafts_enabled' => env('TASK_DRAFTS_ENABLED', true), 'force_https' => env('FORCE_HTTPS', true), + + 'nayra_docker_network' => env('NAYRA_DOCKER_NETWORK', ''), ]; diff --git a/config/script-runners.php b/config/script-runners.php index 694ce78356..8451df1da8 100644 --- a/config/script-runners.php +++ b/config/script-runners.php @@ -12,4 +12,15 @@ | https://github.com/ProcessMaker/docker-executor-node | */ + 'php-nayra' => [ + 'name' => 'PHP (µService)', + 'runner' => 'PhpRunner', + 'mime_type' => 'application/x-php', + 'options' => ['invokerPackage' => 'ProcessMaker\\Client'], + 'init_dockerfile' => [ + ], + 'package_path' => base_path('/docker-services/nayra'), + 'package_version' => '1.0.0', + 'sdk' => '', + ], ]; diff --git a/docker-services/nayra/Dockerfile b/docker-services/nayra/Dockerfile new file mode 100644 index 0000000000..e949c2f5dc --- /dev/null +++ b/docker-services/nayra/Dockerfile @@ -0,0 +1,2 @@ +FROM processmaker4dev/nayra-engine:next +# The tag :next is temporal until the official release of the engine diff --git a/phpunit.xml b/phpunit.xml index b13e0d9f4d..45ef0c0c9a 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -52,6 +52,7 @@ + diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 36adc17e16..157076dd7c 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -232,7 +232,30 @@ if (token) { console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token"); } -window.ProcessMaker.apiClient.defaults.baseURL = "/api/1.0/"; +// Setup api versions +const apiVersionConfig = [ + { version: "1.0", baseURL: "/api/1.0/" }, + { version: "1.1", baseURL: "/api/1.1/" }, +]; + +window.ProcessMaker.apiClient.defaults.baseURL = apiVersionConfig[0].baseURL; +window.ProcessMaker.apiClient.interceptors.request.use((config) => { + if (typeof config.url !== "string" || !config.url) { + throw new Error("Invalid URL in the request configuration"); + } + + apiVersionConfig.forEach(({ version, baseURL }) => { + const versionPrefix = `/api/${version}/`; + if (config.url.startsWith(versionPrefix)) { + // eslint-disable-next-line no-param-reassign + config.baseURL = baseURL; + // eslint-disable-next-line no-param-reassign + config.url = config.url.replace(versionPrefix, ""); + } + }); + + return config; +}); // Set the default API timeout let apiTimeout = 5000; diff --git a/resources/js/processes/scripts/components/ScriptEditor.vue b/resources/js/processes/scripts/components/ScriptEditor.vue index f599b65302..08060d18f8 100644 --- a/resources/js/processes/scripts/components/ScriptEditor.vue +++ b/resources/js/processes/scripts/components/ScriptEditor.vue @@ -298,9 +298,9 @@
{{ preview.error.exception }}
-
- {{ preview.error.message }} -
+
{{
+                            preview.error.message
+                          }}
@@ -552,7 +552,7 @@ export default { return this.packageAi && this.newCode !== "" && !this.changesApplied && !this.isDiffEditor; }, language() { - return this.scriptExecutor.language; + return this.scriptExecutor.language === 'php-nayra' ? 'php' : this.scriptExecutor.language; }, autosaveApiCall() { return () => { @@ -887,6 +887,7 @@ export default { if (this.script.code === "[]") { switch (this.script.language) { case "php": + case "php-nayra": this.code = Vue.filter("php")(this.boilerPlateTemplate); break; case "lua": diff --git a/resources/js/requests/components/RequestsListing.vue b/resources/js/requests/components/RequestsListing.vue index 269f6bdd24..106da4a47e 100644 --- a/resources/js/requests/components/RequestsListing.vue +++ b/resources/js/requests/components/RequestsListing.vue @@ -470,7 +470,8 @@ export default { "&order_direction=" + this.orderDirection + this.additionalParams + - advancedFilter, + advancedFilter + + "&row_format=", { cancelToken: new CancelToken((c) => { this.cancelToken = c; diff --git a/resources/views/tasks/edit.blade.php b/resources/views/tasks/edit.blade.php index 60a255f362..fab412b14a 100644 --- a/resources/views/tasks/edit.blade.php +++ b/resources/views/tasks/edit.blade.php @@ -517,7 +517,7 @@ class="multiselect__tag-icon"> "COMPLETED": "open-style", "TRIGGERED": "open-style", }; - const status = this.task.advanceStatus.toUpperCase(); + const status = (this.task.advanceStatus || '').toUpperCase(); return "card-header text-status " + header[status]; }, isAllowReassignment() { @@ -655,9 +655,14 @@ class="multiselect__tag-icon"> // to view error details. This is done in loadTask in Task.vue if (error.response?.status && error.response?.status === 422) { // Validation error - Object.entries(error.response.data.errors).forEach(([key, value]) => { - window.ProcessMaker.alert(`${key}: ${value[0]}`, 'danger', 0); - }); + if (error.response.data.errors) { + Object.entries(error.response.data.errors).forEach(([key, value]) => { + window.ProcessMaker.alert(`${key}: ${value[0]}`, 'danger', 0); + }); + } else if (error.response.data.message) { + window.ProcessMaker.alert(error.response.data.message, 'danger', 0); + } + this.$refs.task.loadNextAssignedTask(); } }).finally(() => { this.submitting = false; diff --git a/routes/v1_1/api.php b/routes/v1_1/api.php new file mode 100644 index 0000000000..fe1b6a01c7 --- /dev/null +++ b/routes/v1_1/api.php @@ -0,0 +1,29 @@ +name('api.1.1.') + ->group(function () { + // Tasks Endpoints + Route::name('tasks.')->prefix('tasks')->group(function () { + // Route to list tasks + Route::get('/', [TaskController::class, 'index']) + ->name('index'); + + // Route to show a task + Route::get('/{task}', [TaskController::class, 'show']) + ->name('show') + ->middleware(['bindings','can:view,task']); + + // Route to show the screen of a task + Route::get('/{taskId}/screen', [TaskController::class, 'showScreen']) + ->name('show.screen'); + + // Route to show the interstitial screen of a task + Route::get('/{taskId}/interstitial', [TaskController::class, 'showInterstitial']) + ->name('show.interstitial'); + }); + }); diff --git a/tests/Feature/Api/V1_1/Fixtures/nested_screen_process.json b/tests/Feature/Api/V1_1/Fixtures/nested_screen_process.json new file mode 100644 index 0000000000..7c8928ff60 --- /dev/null +++ b/tests/Feature/Api/V1_1/Fixtures/nested_screen_process.json @@ -0,0 +1,2431 @@ +{ + "type": "process_package", + "version": "1", + "process": { + "id": 3, + "process_category_id": "2", + "user_id": 2, + "description": "asdf", + "name": "nested screen test", + "cancel_screen_id": null, + "request_detail_screen_id": null, + "status": "ACTIVE", + "is_valid": 1, + "package_key": null, + "pause_timer_start": 0, + "deleted_at": null, + "created_at": "2020-09-10T17:55:30+00:00", + "updated_at": "2021-01-04T18:48:52+00:00", + "start_events": [{ + "id": "node_1", + "name": "Start Event", + "config": "{\"web_entry\":{\"require_valid_session\":false,\"mode\":\"DISABLED\",\"screen_id\":null,\"completed_action\":\"SCREEN\",\"completed_screen_id\":null,\"completed_url\":null,\"authenticatable_type\":\"ProcessMaker\\\\Models\\\\User\",\"authenticatable_id\":1,\"enable_query_params\":false,\"password\":null,\"enable_password_protect\":false,\"exclude_data\":[]}}", + "ownerProcessId": "ProcessId", + "eventDefinitions": [], + "ownerProcessName": "ProcessName", + "allowInterstitial": "false" + }], + "warnings": null, + "self_service_tasks": [], + "signal_events": [], + "conditional_events": [], + "properties": null, + "has_timer_start_events": false, + "notifications": { + "requester": { + "started": false, + "canceled": false, + "completed": false + }, + "assignee": { + "started": false, + "canceled": false, + "completed": false + }, + "participants": { + "started": false, + "canceled": false, + "completed": false + } + }, + "task_notifications": {}, + "bpmn": "\n\n \n \n node_4<\/bpmn:outgoing>\n <\/bpmn:startEvent>\n \n node_4<\/bpmn:incoming>\n node_5<\/bpmn:outgoing>\n <\/bpmn:task>\n \n node_5<\/bpmn:incoming>\n <\/bpmn:endEvent>\n \n \n <\/bpmn:process>\n \n \n \n \n <\/bpmndi:BPMNShape>\n \n \n <\/bpmndi:BPMNShape>\n \n \n <\/bpmndi:BPMNShape>\n \n \n \n <\/bpmndi:BPMNEdge>\n \n \n \n <\/bpmndi:BPMNEdge>\n <\/bpmndi:BPMNPlane>\n <\/bpmndi:BPMNDiagram>\n<\/bpmn:definitions>\n" + }, + "process_categories": [{ + "id": 2, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 3, + "category_id": 2, + "category_type": "ProcessMaker\\Models\\ProcessCategory" + } + }], + "screens": [{ + "id": 2, + "screen_category_id": "1", + "title": "child", + "description": "asdf", + "type": "FORM", + "config": [{ + "name": "child", + "items": [{ + "label": "Rich Text", + "config": { + "icon": "fas fa-pencil-ruler", + "label": null, + "content": "

Child<\/p>", + "interactive": true, + "renderVarHtml": null + }, + "component": "FormHtmlViewer", + "inspector": [{ + "type": "FormTextArea", + "field": "content", + "config": { + "rows": 5, + "label": "Content", + "value": null, + "helper": "The HTML text to display" + } + }, { + "type": "FormCheckbox", + "field": "renderVarHtml", + "config": { + "label": "Render HTML from a Variable", + "value": null, + "helper": null + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormHtmlEditor", + "editor-component": "FormHtmlEditor" + }, { + "label": "Line Input", + "config": { + "icon": "far fa-square", + "name": "child_input_1", + "type": "text", + "label": "child_input_1", + "helper": null, + "readonly": false, + "dataFormat": "string", + "validation": [], + "placeholder": null + }, + "component": "FormInput", + "inspector": [{ + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^(?:[A-Z_.a-z])(?:[0-9A-Z_.a-z])*$\/|required" + } + }, { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "name": "Data Type", + "label": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "options": [{ + "value": "string", + "content": "Text" + }, { + "value": "int", + "content": "Integer" + }, { + "value": "currency", + "content": "Currency" + }, { + "value": "percentage", + "content": "Percentage" + }, { + "value": "float", + "content": "Decimal" + }, { + "value": "datetime", + "content": "Datetime" + }, { + "value": "date", + "content": "Date" + }, { + "value": "password", + "content": "Password" + }], + "validation": "required" + } + }, { + "type": { + "extends": { + "props": ["label", "error", "options", "helper", "name", "value", "selectedControl"], + "mixins": [{ + "props": ["validation", "validationData", "validationField", "validationMessages"], + "watch": { + "validationData": { + "deep": true + } + }, + "methods": [] + }], + "methods": [], + "computed": [], + "_compiled": true, + "components": { + "Multiselect": { + "name": "vue-multiselect", + "props": { + "name": { + "default": null + }, + "limit": { + "default": 99999 + }, + "loading": { + "default": false + }, + "disabled": { + "default": false + }, + "tabindex": { + "default": 0 + }, + "limitText": [], + "maxHeight": { + "default": 300 + }, + "showLabels": { + "default": true + }, + "selectLabel": { + "default": "Press enter to select" + }, + "deselectLabel": { + "default": "Press enter to remove" + }, + "openDirection": { + "default": null + }, + "selectedLabel": { + "default": "Selected" + }, + "showNoOptions": { + "default": true + }, + "showNoResults": { + "default": true + }, + "selectGroupLabel": { + "default": "Press enter to select group" + }, + "deselectGroupLabel": { + "default": "Press enter to deselect group" + } + }, + "mixins": [{ + "props": { + "id": { + "default": null + }, + "max": { + "type": [null, null], + "default": false + }, + "label": [], + "value": { + "type": null + }, + "options": { + "required": true + }, + "trackBy": [], + "multiple": { + "default": false + }, + "taggable": { + "default": false + }, + "blockKeys": [], + "allowEmpty": { + "default": true + }, + "groupLabel": [], + "resetAfter": { + "default": false + }, + "searchable": { + "default": true + }, + "customLabel": [], + "groupSelect": { + "default": false + }, + "groupValues": [], + "placeholder": { + "default": "Select option" + }, + "tagPosition": { + "default": "top" + }, + "hideSelected": { + "default": false + }, + "optionsLimit": { + "default": 1000 + }, + "clearOnSelect": { + "default": true + }, + "closeOnSelect": { + "default": true + }, + "internalSearch": { + "default": true + }, + "preselectFirst": { + "default": false + }, + "preserveSearch": { + "default": false + }, + "tagPlaceholder": { + "default": "Press enter to create a tag" + } + }, + "watch": [], + "methods": [], + "computed": [] + }, { + "props": { + "showPointer": { + "default": true + }, + "optionHeight": { + "default": 40 + } + }, + "watch": [], + "methods": [], + "computed": [] + }], + "computed": [], + "_compiled": true, + "beforeCreate": [null], + "staticRenderFns": [] + } + }, + "inheritAttrs": false, + "staticRenderFns": [] + }, + "computed": [], + "_compiled": true, + "staticRenderFns": [] + }, + "field": "dataMask", + "config": { + "name": "Data Format", + "label": "Data Format", + "helper": "The data format for the selected type." + } + }, { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only", + "helper": null + } + }, { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [{ + "value": "text-primary", + "content": "primary" + }, { + "value": "text-secondary", + "content": "secondary" + }, { + "value": "text-success", + "content": "success" + }, { + "value": "text-danger", + "content": "danger" + }, { + "value": "text-warning", + "content": "warning" + }, { + "value": "text-info", + "content": "info" + }, { + "value": "text-light", + "content": "light" + }, { + "value": "text-dark", + "content": "dark" + }] + } + }, { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [{ + "value": "alert alert-primary", + "content": "primary" + }, { + "value": "alert alert-secondary", + "content": "secondary" + }, { + "value": "alert alert-success", + "content": "success" + }, { + "value": "alert alert-danger", + "content": "danger" + }, { + "value": "alert alert-warning", + "content": "warning" + }, { + "value": "alert alert-info", + "content": "info" + }, { + "value": "alert alert-light", + "content": "light" + }, { + "value": "alert alert-dark", + "content": "dark" + }] + } + }, { + "type": { + "props": ["value"], + "watch": { + "value": { + "immediate": true + } + }, + "methods": [], + "_scopeId": "data-v-67152bf8", + "computed": { + "effectiveValue": [] + }, + "_compiled": true, + "components": { + "MonacoEditor": { + "name": "MonacoEditor", + "model": { + "event": "change" + }, + "props": { + "theme": { + "default": "vs" + }, + "amdRequire": [] + }, + "watch": { + "options": { + "deep": true + } + }, + "methods": [] + } + }, + "staticRenderFns": [] + }, + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "Takes precedence over value set in data." + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormInput", + "editor-component": "FormInput" + }] + }, { + "name": "p2", + "items": [{ + "label": "Line Input", + "config": { + "icon": "far fa-square", + "name": "child_input_2", + "type": "text", + "label": "child_input_2", + "helper": null, + "readonly": false, + "dataFormat": "string", + "validation": [], + "placeholder": null + }, + "component": "FormInput", + "inspector": [{ + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^(?:[A-Z_.a-z])(?:[0-9A-Z_.a-z])*$\/|required" + } + }, { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "name": "Data Type", + "label": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "options": [{ + "value": "string", + "content": "Text" + }, { + "value": "int", + "content": "Integer" + }, { + "value": "currency", + "content": "Currency" + }, { + "value": "percentage", + "content": "Percentage" + }, { + "value": "float", + "content": "Decimal" + }, { + "value": "datetime", + "content": "Datetime" + }, { + "value": "date", + "content": "Date" + }, { + "value": "password", + "content": "Password" + }], + "validation": "required" + } + }, { + "type": { + "_Ctor": [], + "extends": { + "_Ctor": [], + "props": { + "name": { + "type": null + }, + "error": { + "type": null + }, + "label": { + "type": null + }, + "value": { + "type": null + }, + "helper": { + "type": null + }, + "options": { + "type": null + }, + "selectedControl": { + "type": null + } + }, + "mixins": [{ + "props": { + "validation": { + "type": null + }, + "validationData": { + "type": null + }, + "validationField": { + "type": null + }, + "validationMessages": { + "type": null + } + }, + "watch": { + "validationData": { + "deep": true, + "user": true + } + }, + "methods": [] + }], + "methods": [], + "computed": [], + "_compiled": true, + "components": { + "Multiselect": { + "name": "vue-multiselect", + "_Ctor": [], + "props": { + "name": { + "default": null + }, + "limit": { + "default": 99999 + }, + "loading": { + "default": false + }, + "disabled": { + "default": false + }, + "tabindex": { + "default": 0 + }, + "limitText": [], + "maxHeight": { + "default": 300 + }, + "showLabels": { + "default": true + }, + "selectLabel": { + "default": "Press enter to select" + }, + "deselectLabel": { + "default": "Press enter to remove" + }, + "openDirection": { + "default": null + }, + "selectedLabel": { + "default": "Selected" + }, + "showNoOptions": { + "default": true + }, + "showNoResults": { + "default": true + }, + "selectGroupLabel": { + "default": "Press enter to select group" + }, + "deselectGroupLabel": { + "default": "Press enter to deselect group" + } + }, + "mixins": [{ + "props": { + "id": { + "default": null + }, + "max": { + "type": [null, null], + "default": false + }, + "label": [], + "value": { + "type": null + }, + "options": { + "required": true + }, + "trackBy": [], + "multiple": { + "default": false + }, + "taggable": { + "default": false + }, + "blockKeys": [], + "allowEmpty": { + "default": true + }, + "groupLabel": [], + "resetAfter": { + "default": false + }, + "searchable": { + "default": true + }, + "customLabel": [], + "groupSelect": { + "default": false + }, + "groupValues": [], + "placeholder": { + "default": "Select option" + }, + "tagPosition": { + "default": "top" + }, + "hideSelected": { + "default": false + }, + "optionsLimit": { + "default": 1000 + }, + "clearOnSelect": { + "default": true + }, + "closeOnSelect": { + "default": true + }, + "internalSearch": { + "default": true + }, + "preselectFirst": { + "default": false + }, + "preserveSearch": { + "default": false + }, + "tagPlaceholder": { + "default": "Press enter to create a tag" + } + }, + "watch": [], + "methods": [], + "computed": [] + }, { + "props": { + "showPointer": { + "default": true + }, + "optionHeight": { + "default": 40 + } + }, + "watch": [], + "methods": [], + "computed": [] + }], + "computed": [], + "_compiled": true, + "beforeCreate": [null], + "staticRenderFns": [] + } + }, + "inheritAttrs": false, + "staticRenderFns": [] + }, + "computed": [], + "_compiled": true, + "staticRenderFns": [] + }, + "field": "dataMask", + "config": { + "name": "Data Format", + "label": "Data Format", + "helper": "The data format for the selected type." + } + }, { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only", + "helper": null + } + }, { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [{ + "value": "text-primary", + "content": "primary" + }, { + "value": "text-secondary", + "content": "secondary" + }, { + "value": "text-success", + "content": "success" + }, { + "value": "text-danger", + "content": "danger" + }, { + "value": "text-warning", + "content": "warning" + }, { + "value": "text-info", + "content": "info" + }, { + "value": "text-light", + "content": "light" + }, { + "value": "text-dark", + "content": "dark" + }] + } + }, { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [{ + "value": "alert alert-primary", + "content": "primary" + }, { + "value": "alert alert-secondary", + "content": "secondary" + }, { + "value": "alert alert-success", + "content": "success" + }, { + "value": "alert alert-danger", + "content": "danger" + }, { + "value": "alert alert-warning", + "content": "warning" + }, { + "value": "alert alert-info", + "content": "info" + }, { + "value": "alert alert-light", + "content": "light" + }, { + "value": "alert alert-dark", + "content": "dark" + }] + } + }, { + "type": { + "_Ctor": [], + "props": { + "value": { + "type": null + } + }, + "watch": { + "value": { + "user": true, + "immediate": true + } + }, + "methods": [], + "_scopeId": "data-v-67152bf8", + "computed": { + "effectiveValue": [] + }, + "_compiled": true, + "components": { + "MonacoEditor": { + "name": "MonacoEditor", + "model": { + "event": "change" + }, + "props": { + "theme": { + "default": "vs" + }, + "amdRequire": [] + }, + "watch": { + "options": { + "deep": true + } + }, + "methods": [] + } + }, + "staticRenderFns": [] + }, + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "Takes precedence over value set in data." + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormInput", + "editor-component": "FormInput" + }] + }], + "computed": [{ + "id": 1, + "name": "test", + "type": "javascript", + "formula": "return 'test calc!';", + "property": "child_input_1" + }], + "custom_css": "* { color: red }", + "created_at": "2020-09-10T17:34:07+00:00", + "updated_at": "2021-01-04T18:48:48+00:00", + "status": "ACTIVE", + "key": null, + "watchers": [{ + "input_data": "{}", + "script_configuration": "{}", + "synchronous": false, + "name": "child watcher", + "watching": "child_input_1", + "script": { + "id": "script-3", + "key": null, + "title": "child watcher script", + "description": "test", + "language": "php", + "code": " 'ok'];", + "timeout": 60, + "run_as_user_id": 1, + "created_at": "2020-09-11T19:06:28+00:00", + "updated_at": "2020-09-11T19:06:37+00:00", + "status": "ACTIVE", + "script_category_id": "1", + "script_executor_id": 3 + }, + "script_id": 3, + "script_key": null, + "output_variable": "watcher_result", + "uid": "15998512345162" + }], + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 2, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + }] + }, { + "id": 3, + "screen_category_id": "1", + "title": "parent", + "description": "asdf", + "type": "FORM", + "config": [{ + "name": "parent", + "items": [{ + "label": "Rich Text", + "config": { + "icon": "fas fa-pencil-ruler", + "label": null, + "content": "

Parent<\/p>", + "interactive": true, + "renderVarHtml": null + }, + "component": "FormHtmlViewer", + "inspector": [{ + "type": "FormTextArea", + "field": "content", + "config": { + "rows": 5, + "label": "Content", + "value": null, + "helper": "The HTML text to display" + } + }, { + "type": "FormCheckbox", + "field": "renderVarHtml", + "config": { + "label": "Render HTML from a Variable", + "value": null, + "helper": null + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormHtmlEditor", + "editor-component": "FormHtmlEditor" + }, { + "items": [ + [{ + "label": "Line Input", + "config": { + "icon": "far fa-square", + "name": "parent_input_1", + "type": "text", + "label": "parent_input_1", + "helper": null, + "readonly": false, + "dataFormat": "string", + "validation": [], + "placeholder": null + }, + "component": "FormInput", + "inspector": [{ + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information.", + "validation": "regex:\/^(?:[A-Z_.a-z])(?:[0-9A-Z_.a-z])*$\/|required" + } + }, { + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the field's name" + } + }, { + "type": "FormMultiselect", + "field": "dataFormat", + "config": { + "name": "Data Type", + "label": "Data Type", + "helper": "The data type specifies what kind of data is stored in the variable.", + "options": [{ + "value": "string", + "content": "Text" + }, { + "value": "int", + "content": "Integer" + }, { + "value": "currency", + "content": "Currency" + }, { + "value": "percentage", + "content": "Percentage" + }, { + "value": "float", + "content": "Decimal" + }, { + "value": "datetime", + "content": "Datetime" + }, { + "value": "date", + "content": "Date" + }, { + "value": "password", + "content": "Password" + }], + "validation": "required" + } + }, { + "type": { + "extends": { + "props": ["label", "error", "options", "helper", "name", "value", "selectedControl"], + "mixins": [{ + "props": ["validation", "validationData", "validationField", "validationMessages"], + "watch": { + "validationData": { + "deep": true + } + }, + "methods": [] + }], + "methods": [], + "computed": [], + "_compiled": true, + "components": { + "Multiselect": { + "name": "vue-multiselect", + "_Ctor": [], + "props": { + "name": { + "default": null + }, + "limit": { + "default": 99999 + }, + "loading": { + "default": false + }, + "disabled": { + "default": false + }, + "tabindex": { + "default": 0 + }, + "limitText": [], + "maxHeight": { + "default": 300 + }, + "showLabels": { + "default": true + }, + "selectLabel": { + "default": "Press enter to select" + }, + "deselectLabel": { + "default": "Press enter to remove" + }, + "openDirection": { + "default": null + }, + "selectedLabel": { + "default": "Selected" + }, + "showNoOptions": { + "default": true + }, + "showNoResults": { + "default": true + }, + "selectGroupLabel": { + "default": "Press enter to select group" + }, + "deselectGroupLabel": { + "default": "Press enter to deselect group" + } + }, + "mixins": [{ + "props": { + "id": { + "default": null + }, + "max": { + "type": [null, null], + "default": false + }, + "label": [], + "value": { + "type": null + }, + "options": { + "required": true + }, + "trackBy": [], + "multiple": { + "default": false + }, + "taggable": { + "default": false + }, + "blockKeys": [], + "allowEmpty": { + "default": true + }, + "groupLabel": [], + "resetAfter": { + "default": false + }, + "searchable": { + "default": true + }, + "customLabel": [], + "groupSelect": { + "default": false + }, + "groupValues": [], + "placeholder": { + "default": "Select option" + }, + "tagPosition": { + "default": "top" + }, + "hideSelected": { + "default": false + }, + "optionsLimit": { + "default": 1000 + }, + "clearOnSelect": { + "default": true + }, + "closeOnSelect": { + "default": true + }, + "internalSearch": { + "default": true + }, + "preselectFirst": { + "default": false + }, + "preserveSearch": { + "default": false + }, + "tagPlaceholder": { + "default": "Press enter to create a tag" + } + }, + "watch": [], + "methods": [], + "computed": [] + }, { + "props": { + "showPointer": { + "default": true + }, + "optionHeight": { + "default": 40 + } + }, + "watch": [], + "methods": [], + "computed": [] + }], + "computed": [], + "_compiled": true, + "beforeCreate": [null], + "staticRenderFns": [] + } + }, + "inheritAttrs": false, + "staticRenderFns": [] + }, + "computed": [], + "_compiled": true, + "staticRenderFns": [] + }, + "field": "dataMask", + "config": { + "name": "Data Format", + "label": "Data Format", + "helper": "The data format for the selected type." + } + }, { + "type": "ValidationSelect", + "field": "validation", + "config": { + "label": "Validation Rules", + "helper": "The validation rules needed for this field" + } + }, { + "type": "FormInput", + "field": "placeholder", + "config": { + "label": "Placeholder Text", + "helper": "The placeholder is what is shown in the field when no value is provided yet" + } + }, { + "type": "FormInput", + "field": "helper", + "config": { + "label": "Helper Text", + "helper": "Help text is meant to provide additional guidance on the field's value" + } + }, { + "type": "FormCheckbox", + "field": "readonly", + "config": { + "label": "Read Only", + "helper": null + } + }, { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [{ + "value": "text-primary", + "content": "primary" + }, { + "value": "text-secondary", + "content": "secondary" + }, { + "value": "text-success", + "content": "success" + }, { + "value": "text-danger", + "content": "danger" + }, { + "value": "text-warning", + "content": "warning" + }, { + "value": "text-info", + "content": "info" + }, { + "value": "text-light", + "content": "light" + }, { + "value": "text-dark", + "content": "dark" + }] + } + }, { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [{ + "value": "alert alert-primary", + "content": "primary" + }, { + "value": "alert alert-secondary", + "content": "secondary" + }, { + "value": "alert alert-success", + "content": "success" + }, { + "value": "alert alert-danger", + "content": "danger" + }, { + "value": "alert alert-warning", + "content": "warning" + }, { + "value": "alert alert-info", + "content": "info" + }, { + "value": "alert alert-light", + "content": "light" + }, { + "value": "alert alert-dark", + "content": "dark" + }] + } + }, { + "type": { + "props": ["value"], + "watch": { + "value": { + "immediate": true + } + }, + "methods": [], + "_scopeId": "data-v-67152bf8", + "computed": { + "effectiveValue": [] + }, + "_compiled": true, + "components": { + "MonacoEditor": { + "name": "MonacoEditor", + "model": { + "event": "change" + }, + "props": { + "theme": { + "default": "vs" + }, + "amdRequire": [] + }, + "watch": { + "options": { + "deep": true + } + }, + "methods": [] + } + }, + "staticRenderFns": [] + }, + "field": "defaultValue", + "config": { + "label": "Default Value", + "helper": "Takes precedence over value set in data." + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormInput", + "editor-component": "FormInput" + }], + [{ + "label": "Nested Screen", + "config": { + "icon": "fas fa-file-invoice", + "name": null, + "event": "submit", + "label": "Nested Screen", + "value": null, + "screen": 2, + "variant": "primary" + }, + "component": "FormNestedScreen", + "inspector": [{ + "type": "ScreenSelector", + "field": "screen", + "config": { + "name": "SelectScreen", + "label": "Screen", + "helper": "Select a screen" + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormNestedScreen" + }] + ], + "label": "Multicolumn \/ Table", + "config": { + "icon": "fas fa-table", + "label": null, + "options": [{ + "value": "1", + "content": "6" + }, { + "value": "2", + "content": "6" + }] + }, + "component": "FormMultiColumn", + "container": true, + "inspector": [{ + "type": "ContainerColumns", + "field": "options", + "config": { + "label": "Column Width", + "validation": "columns-adds-to-12" + } + }, { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [{ + "value": "text-primary", + "content": "primary" + }, { + "value": "text-secondary", + "content": "secondary" + }, { + "value": "text-success", + "content": "success" + }, { + "value": "text-danger", + "content": "danger" + }, { + "value": "text-warning", + "content": "warning" + }, { + "value": "text-info", + "content": "info" + }, { + "value": "text-light", + "content": "light" + }, { + "value": "text-dark", + "content": "dark" + }] + } + }, { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [{ + "value": "alert alert-primary", + "content": "primary" + }, { + "value": "alert alert-secondary", + "content": "secondary" + }, { + "value": "alert alert-success", + "content": "success" + }, { + "value": "alert alert-danger", + "content": "danger" + }, { + "value": "alert alert-warning", + "content": "warning" + }, { + "value": "alert alert-info", + "content": "info" + }, { + "value": "alert alert-light", + "content": "light" + }, { + "value": "alert alert-dark", + "content": "dark" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "MultiColumn", + "editor-component": "MultiColumn" + }, { + "label": "Nested Screen", + "config": { + "icon": "fas fa-file-invoice", + "name": null, + "event": "submit", + "label": "Nested Screen", + "value": null, + "screen": 5, + "variant": "primary" + }, + "component": "FormNestedScreen", + "inspector": [{ + "type": "ScreenSelector", + "field": "screen", + "config": { + "name": "SelectScreen", + "label": "Screen", + "helper": "Select a screen" + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormNestedScreen" + }, { + "label": "Submit Button", + "config": { + "icon": "fas fa-share-square", + "name": null, + "event": "submit", + "label": "New Submit", + "variant": "primary", + "fieldValue": null, + "defaultSubmit": true + }, + "component": "FormButton", + "inspector": [{ + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information." + } + }, { + "type": "FormMultiselect", + "field": "event", + "config": { + "label": "Type", + "helper": "Choose whether the button should submit the form", + "options": [{ + "value": "submit", + "content": "Submit Button" + }, { + "value": "script", + "content": "Regular Button" + }] + } + }, { + "type": "FormInput", + "field": "fieldValue", + "config": { + "label": "Value", + "helper": "The value being submitted" + } + }, { + "type": "FormMultiselect", + "field": "variant", + "config": { + "label": "Button Variant Style", + "helper": "The variant determines the appearance of the button", + "options": [{ + "value": "primary", + "content": "Primary" + }, { + "value": "secondary", + "content": "Secondary" + }, { + "value": "success", + "content": "Success" + }, { + "value": "danger", + "content": "Danger" + }, { + "value": "warning", + "content": "Warning" + }, { + "value": "info", + "content": "Info" + }, { + "value": "light", + "content": "Light" + }, { + "value": "dark", + "content": "Dark" + }, { + "value": "link", + "content": "Link" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormButton" + }] + }], + "computed": [{ + "id": 1, + "name": "test", + "type": "javascript", + "formula": "return 'test calc parent';", + "property": "parent_input_1" + }], + "custom_css": "* { color: blue }", + "created_at": "2020-09-10T17:34:26+00:00", + "updated_at": "2021-01-04T18:48:48+00:00", + "status": "ACTIVE", + "key": null, + "watchers": [{ + "input_data": "{}", + "script_configuration": "{}", + "synchronous": false, + "name": "parent watcher test", + "watching": "parent_input_1", + "output_variable": "parent_watcher_result", + "script": { + "id": "script-2", + "key": null, + "title": "parent watcher script", + "description": "test", + "language": "php", + "code": " 'ok'];", + "timeout": 60, + "run_as_user_id": 1, + "created_at": "2020-09-11T19:05:08+00:00", + "updated_at": "2020-09-11T19:05:41+00:00", + "status": "ACTIVE", + "script_category_id": "1", + "script_executor_id": 3 + }, + "script_id": 2, + "script_key": null, + "uid": "15998513965032" + }], + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 3, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + }] + }, { + "id": 4, + "screen_category_id": "1", + "title": "child3", + "description": "test", + "type": "FORM", + "config": [{ + "name": "child3", + "items": [{ + "label": "Rich Text", + "config": { + "icon": "fas fa-pencil-ruler", + "label": null, + "content": "

Child 3<\/p>", + "interactive": true, + "renderVarHtml": null + }, + "component": "FormHtmlViewer", + "inspector": [{ + "type": "FormTextArea", + "field": "content", + "config": { + "rows": 5, + "label": "Content", + "value": null, + "helper": "The HTML text to display" + } + }, { + "type": "FormCheckbox", + "field": "renderVarHtml", + "config": { + "label": "Render HTML from a Variable", + "value": null, + "helper": null + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormHtmlEditor", + "editor-component": "FormHtmlEditor" + }, { + "label": "Submit Button", + "config": { + "icon": "fas fa-share-square", + "name": null, + "event": "submit", + "label": "child 3 submit", + "variant": "primary", + "fieldValue": null, + "defaultSubmit": true + }, + "component": "FormButton", + "inspector": [{ + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information." + } + }, { + "type": "FormMultiselect", + "field": "event", + "config": { + "label": "Type", + "helper": "Choose whether the button should submit the form", + "options": [{ + "value": "submit", + "content": "Submit Button" + }, { + "value": "script", + "content": "Regular Button" + }] + } + }, { + "type": "FormInput", + "field": "fieldValue", + "config": { + "label": "Value", + "helper": "The value being submitted" + } + }, { + "type": "FormMultiselect", + "field": "variant", + "config": { + "label": "Button Variant Style", + "helper": "The variant determines the appearance of the button", + "options": [{ + "value": "primary", + "content": "Primary" + }, { + "value": "secondary", + "content": "Secondary" + }, { + "value": "success", + "content": "Success" + }, { + "value": "danger", + "content": "Danger" + }, { + "value": "warning", + "content": "Warning" + }, { + "value": "info", + "content": "Info" + }, { + "value": "light", + "content": "Light" + }, { + "value": "dark", + "content": "Dark" + }, { + "value": "link", + "content": "Link" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormButton" + }] + }], + "computed": [], + "custom_css": null, + "created_at": "2020-09-11T21:31:37+00:00", + "updated_at": "2021-01-04T18:50:04+00:00", + "status": "ACTIVE", + "key": null, + "watchers": [], + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 4, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + }] + }, { + "id": 5, + "screen_category_id": "1", + "title": "child2", + "description": "test", + "type": "FORM", + "config": [{ + "name": "child2", + "items": [{ + "label": "Rich Text", + "config": { + "icon": "fas fa-pencil-ruler", + "label": null, + "content": "

Child 2<\/p>", + "interactive": true, + "renderVarHtml": null + }, + "component": "FormHtmlViewer", + "inspector": [{ + "type": "FormTextArea", + "field": "content", + "config": { + "rows": 5, + "label": "Content", + "value": null, + "helper": "The HTML text to display" + } + }, { + "type": "FormCheckbox", + "field": "renderVarHtml", + "config": { + "label": "Render HTML from a Variable", + "value": null, + "helper": null + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormHtmlEditor", + "editor-component": "FormHtmlEditor" + }, { + "label": "Submit Button", + "config": { + "icon": "fas fa-share-square", + "name": null, + "event": "submit", + "label": "child 2 submit", + "variant": "primary", + "fieldValue": null, + "defaultSubmit": true + }, + "component": "FormButton", + "inspector": [{ + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information." + } + }, { + "type": "FormMultiselect", + "field": "event", + "config": { + "label": "Type", + "helper": "Choose whether the button should submit the form", + "options": [{ + "value": "submit", + "content": "Submit Button" + }, { + "value": "script", + "content": "Regular Button" + }] + } + }, { + "type": "FormInput", + "field": "fieldValue", + "config": { + "label": "Value", + "helper": "The value being submitted" + } + }, { + "type": "FormMultiselect", + "field": "variant", + "config": { + "label": "Button Variant Style", + "helper": "The variant determines the appearance of the button", + "options": [{ + "value": "primary", + "content": "Primary" + }, { + "value": "secondary", + "content": "Secondary" + }, { + "value": "success", + "content": "Success" + }, { + "value": "danger", + "content": "Danger" + }, { + "value": "warning", + "content": "Warning" + }, { + "value": "info", + "content": "Info" + }, { + "value": "light", + "content": "Light" + }, { + "value": "dark", + "content": "Dark" + }, { + "value": "link", + "content": "Link" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormButton" + }, { + "items": [ + [], + [{ + "label": "Submit Button", + "config": { + "icon": "fas fa-share-square", + "name": null, + "event": "submit", + "label": "child 2 submit in multicolumn", + "variant": "primary", + "fieldValue": null, + "defaultSubmit": true + }, + "component": "FormButton", + "inspector": [{ + "type": "FormInput", + "field": "label", + "config": { + "label": "Label", + "helper": "The label describes the button's text" + } + }, { + "type": "FormInput", + "field": "name", + "config": { + "name": "Variable Name", + "label": "Variable Name", + "helper": "A variable name is a symbolic name to reference information." + } + }, { + "type": "FormMultiselect", + "field": "event", + "config": { + "label": "Type", + "helper": "Choose whether the button should submit the form", + "options": [{ + "value": "submit", + "content": "Submit Button" + }, { + "value": "script", + "content": "Regular Button" + }] + } + }, { + "type": "FormInput", + "field": "fieldValue", + "config": { + "label": "Value", + "helper": "The value being submitted" + } + }, { + "type": "FormMultiselect", + "field": "variant", + "config": { + "label": "Button Variant Style", + "helper": "The variant determines the appearance of the button", + "options": [{ + "value": "primary", + "content": "Primary" + }, { + "value": "secondary", + "content": "Secondary" + }, { + "value": "success", + "content": "Success" + }, { + "value": "danger", + "content": "Danger" + }, { + "value": "warning", + "content": "Warning" + }, { + "value": "info", + "content": "Info" + }, { + "value": "light", + "content": "Light" + }, { + "value": "dark", + "content": "Dark" + }, { + "value": "link", + "content": "Link" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormButton" + }] + ], + "label": "Multicolumn \/ Table", + "config": { + "icon": "fas fa-table", + "label": null, + "options": [{ + "value": "1", + "content": "6" + }, { + "value": "2", + "content": "6" + }] + }, + "component": "FormMultiColumn", + "container": true, + "inspector": [{ + "type": "ContainerColumns", + "field": "options", + "config": { + "label": "Column Width", + "validation": "columns-adds-to-12" + } + }, { + "type": "ColorSelect", + "field": "color", + "config": { + "label": "Text Color", + "helper": "Set the element's text color", + "options": [{ + "value": "text-primary", + "content": "primary" + }, { + "value": "text-secondary", + "content": "secondary" + }, { + "value": "text-success", + "content": "success" + }, { + "value": "text-danger", + "content": "danger" + }, { + "value": "text-warning", + "content": "warning" + }, { + "value": "text-info", + "content": "info" + }, { + "value": "text-light", + "content": "light" + }, { + "value": "text-dark", + "content": "dark" + }] + } + }, { + "type": "ColorSelect", + "field": "bgcolor", + "config": { + "label": "Background Color", + "helper": "Set the element's background color", + "options": [{ + "value": "alert alert-primary", + "content": "primary" + }, { + "value": "alert alert-secondary", + "content": "secondary" + }, { + "value": "alert alert-success", + "content": "success" + }, { + "value": "alert alert-danger", + "content": "danger" + }, { + "value": "alert alert-warning", + "content": "warning" + }, { + "value": "alert alert-info", + "content": "info" + }, { + "value": "alert alert-light", + "content": "light" + }, { + "value": "alert alert-dark", + "content": "dark" + }] + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "MultiColumn", + "editor-component": "MultiColumn" + }, { + "label": "Nested Screen", + "config": { + "icon": "fas fa-file-invoice", + "name": null, + "event": "submit", + "label": "Nested Screen", + "value": null, + "screen": 4, + "variant": "primary" + }, + "component": "FormNestedScreen", + "inspector": [{ + "type": "ScreenSelector", + "field": "screen", + "config": { + "name": "SelectScreen", + "label": "Screen", + "helper": "Select a screen" + } + }, { + "type": "FormInput", + "field": "conditionalHide", + "config": { + "label": "Visibility Rule", + "helper": "This control is hidden until this expression is true" + } + }, { + "type": "FormInput", + "field": "customCssSelector", + "config": { + "label": "CSS Selector Name", + "helper": "Use this in your custom css rules", + "validation": "regex: [-?[_a-zA-Z]+[_-a-zA-Z0-9]*]" + } + }], + "editor-control": "FormSubmit", + "editor-component": "FormNestedScreen" + }] + }], + "computed": [], + "custom_css": null, + "created_at": "2020-09-11T21:32:12+00:00", + "updated_at": "2021-01-04T18:49:50+00:00", + "status": "ACTIVE", + "key": null, + "watchers": [], + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 5, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScreenCategory" + } + }] + }], + "screen_categories": [], + "scripts": [{ + "id": 2, + "key": null, + "title": "parent watcher script", + "description": "test", + "language": "php", + "code": " 'ok'];", + "timeout": 60, + "run_as_user_id": null, + "created_at": "2020-09-11T19:05:08+00:00", + "updated_at": "2021-01-04T18:48:48+00:00", + "status": "ACTIVE", + "script_category_id": "1", + "script_executor_id": 3, + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 2, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScriptCategory" + } + }] + }, { + "id": 3, + "key": null, + "title": "child watcher script", + "description": "test", + "language": "php", + "code": " 'ok'];", + "timeout": 60, + "run_as_user_id": null, + "created_at": "2020-09-11T19:06:28+00:00", + "updated_at": "2021-01-04T18:48:48+00:00", + "status": "ACTIVE", + "script_category_id": "1", + "script_executor_id": 3, + "categories": [{ + "id": 1, + "name": "Uncategorized", + "status": "ACTIVE", + "is_system": 0, + "created_at": "2021-01-04T18:48:24+00:00", + "updated_at": "2021-01-04T18:48:24+00:00", + "pivot": { + "assignable_id": 3, + "category_id": 1, + "category_type": "ProcessMaker\\Models\\ScriptCategory" + } + }] + }], + "environment_variables": [] +} \ No newline at end of file diff --git a/tests/Feature/Api/V1_1/TaskControllerTest.php b/tests/Feature/Api/V1_1/TaskControllerTest.php new file mode 100644 index 0000000000..3248ff4952 --- /dev/null +++ b/tests/Feature/Api/V1_1/TaskControllerTest.php @@ -0,0 +1,48 @@ +create(); + $response = $this->apiCall('GET', route('api.1.1.tasks.show', $task->id)); + $response->assertStatus(200) + ->assertJson(['id' => $task->id]); + } + + public function testShowScreen() + { + $content = file_get_contents( + __DIR__ . '/Fixtures/nested_screen_process.json' + ); + ImportProcess::dispatchSync($content); + $request = ProcessRequest::factory()->create([ + 'process_id' => Process::where('name', 'nested screen test')->first()->id, + ]); + $task = ProcessRequestToken::factory()->create([ + 'element_type' => 'task', + 'element_name' => 'Task 1', + 'element_id' => 'node_2', + 'process_id' => Process::where('name', 'nested screen test')->first()->id, + 'process_request_id' => $request->id, + ]); + $response = $this->apiCall('GET', route('api.1.1.tasks.show.screen', $task->id) . '?include=screen,nested'); + $this->assertNotNull($response->json()); + $this->assertIsArray($response->json()); + $this->assertNotNull($response->headers->get('Cache-Control')); + $this->assertNotNull($response->headers->get('Expires')); + } +} diff --git a/tests/Feature/Shared/RequestHelper.php b/tests/Feature/Shared/RequestHelper.php index 87dade97a3..845434ead8 100644 --- a/tests/Feature/Shared/RequestHelper.php +++ b/tests/Feature/Shared/RequestHelper.php @@ -44,10 +44,13 @@ protected function apiCall($method, $url, $params = []) { // If the url was generated using the route() helper, // strip out the http://.../api/1.0 part of it; - $url = preg_replace('/^.*\/api\/1\.0/i', '', $url); + $url = preg_replace('/^.*\/api\//i', '', $url); + if (substr($url, 0, 1) === '/') { + $url = '1.0' . $url; + } $response = $this->actingAs($this->user, 'api') - ->json($method, '/api/1.0' . $url, $params); + ->json($method, '/api/' . $url, $params); $this->_debug_response = $response; return $response; diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 586fd87c58..d5f5962059 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Support\Facades\Artisan; use ProcessMaker\Models\ScriptExecutor; +use ProcessMaker\ScriptRunners\Base; // Bootstrap laravel app()->make(Kernel::class)->bootstrap(); @@ -102,10 +103,15 @@ ['language' => 'php'], ['title' => 'Test Executor'] ); + ScriptExecutor::firstOrCreate( + ['language' => 'php-nayra'], + ['title' => 'Test Executor Nayra'] + ); ScriptExecutor::firstOrCreate( ['language' => 'lua'], ['title' => 'Test Executor'] ); + Base::initNayraPhpUnitTest(); if (env('PARALLEL_TEST_PROCESSES')) { Artisan::call('processmaker:create-test-dbs'); diff --git a/upgrades/2024_06_12_150527_create_nayra_script_executor.php b/upgrades/2024_06_12_150527_create_nayra_script_executor.php new file mode 100644 index 0000000000..c76bd1faaa --- /dev/null +++ b/upgrades/2024_06_12_150527_create_nayra_script_executor.php @@ -0,0 +1,45 @@ +exists(); + if (!$exists) { + $scriptExecutor = new ScriptExecutor(); + $scriptExecutor->language = Base::NAYRA_LANG; + $scriptExecutor->title = 'Nayra (µService)'; + $scriptExecutor->save(); + } + } + + /** + * Reverse the upgrade migration. + * + * @return void + */ + public function down() + { + try { + $existsScriptsUsingNayra = Script::where('language', Base::NAYRA_LANG)->exists(); + if (!$existsScriptsUsingNayra) { + ScriptExecutor::where('language', Base::NAYRA_LANG)->delete(); + } else { + Log::error('There are scripts using Nayra, so the Nayra script executor cannot be deleted.'); + } + } catch (Exception $e) { + Log::error('Cannot delete Nayra script executor: ' . $e->getMessage()); + } + } +}