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 @@
{{ + 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
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()); + } + } +}