From a95957caa50d87db984ff85e0c6e1766a789922d Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Fri, 14 Aug 2026 13:24:20 -0400 Subject: [PATCH 1/2] fix(FOUR-24155): changing the script configuration deletes the code Skip version publishing in HasVersioning when versionable content is unchanged. Define code as the versionable attribute for Script. https://processmaker.atlassian.net/browse/FOUR-24155 --- ProcessMaker/Models/Script.php | 8 +++ ProcessMaker/Traits/HasVersioning.php | 55 +++++++++++++++++ tests/Feature/Api/ScriptsTest.php | 85 +++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) diff --git a/ProcessMaker/Models/Script.php b/ProcessMaker/Models/Script.php index f01c7f4492..2223b21d8f 100644 --- a/ProcessMaker/Models/Script.php +++ b/ProcessMaker/Models/Script.php @@ -292,6 +292,14 @@ public function versions() return $this->hasMany(ScriptVersion::class); } + /** + * @inheritdoc + */ + protected function getVersionableAttributes(): ?array + { + return ['code']; + } + /** * Get the associated run_as_user */ diff --git a/ProcessMaker/Traits/HasVersioning.php b/ProcessMaker/Traits/HasVersioning.php index ef3d73ba89..b2b2fa2beb 100644 --- a/ProcessMaker/Traits/HasVersioning.php +++ b/ProcessMaker/Traits/HasVersioning.php @@ -55,6 +55,14 @@ public static function saveNewVersion(Model $model) */ public function saveVersion() { + $draft = $this->getDraftVersion(); + + if ($draft && !$this->isPublishingVersionableContent()) { + $this->syncDraftMetadata($draft); + + return null; + } + $attributes = $this->getModelAttributes(); $version = $this->versions()->create($attributes); @@ -68,6 +76,53 @@ public function saveVersion() return $version; } + /** + * Attributes that trigger publishing a new version when changed. + * Return null to always publish on save (legacy behavior). + */ + protected function getVersionableAttributes(): ?array + { + return null; + } + + /** + * Determine if the current save publishes versionable content. + */ + protected function isPublishingVersionableContent(): bool + { + $versionableAttributes = $this->getVersionableAttributes(); + + if ($versionableAttributes === null) { + return true; + } + + return !empty(array_intersect( + array_keys($this->getChanges()), + $versionableAttributes + )); + } + + /** + * Sync metadata from the model to an existing draft without overwriting versionable content. + */ + private function syncDraftMetadata(Model $draft): void + { + $attributes = $this->getModelAttributes(); + + foreach ($this->getVersionableAttributes() ?? [] as $attribute) { + unset($attributes[$attribute]); + } + + foreach ($attributes as $key => $value) { + if ($draft->getAttribute($key) != $value) { + $draft->fill($attributes); + $draft->save(); + + return; + } + } + } + /** * Save a draft version of the model * diff --git a/tests/Feature/Api/ScriptsTest.php b/tests/Feature/Api/ScriptsTest.php index bb76dad7db..9371c85939 100644 --- a/tests/Feature/Api/ScriptsTest.php +++ b/tests/Feature/Api/ScriptsTest.php @@ -674,4 +674,89 @@ public function testCloseDraftScript() $response->assertStatus(204); $this->assertTrue($script->versions()->draft()->doesntExist()); } + + public function testUpdateScriptConfigurationPreservesDraft() + { + $user = User::factory()->create(['is_administrator' => true]); + $category = ScriptCategory::factory()->create(); + $newCategory = ScriptCategory::factory()->create(); + $publishedCode = ' true];'; + + $script = Script::factory()->create([ + 'code' => $publishedCode, + 'script_category_id' => $category->id, + ]); + + $response = $this->apiCall('PUT', route('api.scripts.draft', ['script' => $script->id]), [ + 'title' => $script->title, + 'language' => $script->language, + 'description' => $script->description, + 'code' => $draftCode, + 'run_as_user_id' => $user->id, + 'script_category_id' => $script->script_category_id, + ]); + $response->assertStatus(204); + + $publishedVersionCount = $script->versions()->published()->count(); + + $response = $this->apiCall('PUT', route('api.scripts.update', ['script' => $script->id]), [ + 'title' => 'Updated Script Title', + 'language' => $script->language, + 'description' => $script->description, + 'run_as_user_id' => $user->id, + 'script_category_id' => $newCategory->id, + 'script_executor_id' => $script->script_executor_id, + 'timeout' => $script->timeout, + ]); + $response->assertStatus(204); + + $script->refresh(); + $draft = $script->versions()->draft()->first(); + + $this->assertNotNull($draft); + $this->assertEquals($draftCode, $draft->code); + $this->assertEquals('Updated Script Title', $script->title); + $this->assertEquals('Updated Script Title', $draft->title); + $this->assertEquals($newCategory->id, (int) $script->script_category_id); + $this->assertEquals($publishedCode, $script->code); + $this->assertEquals($publishedVersionCount, $script->versions()->published()->count()); + } + + public function testUpdateScriptWithCodeDeletesDraft() + { + $faker = Faker::create(); + $user = User::factory()->create(['is_administrator' => true]); + $publishedCode = ' true];'; + $publishedCodeAfterSave = $faker->sentence(3); + + $script = Script::factory()->create([ + 'code' => $publishedCode, + ]); + + $response = $this->apiCall('PUT', route('api.scripts.draft', ['script' => $script->id]), [ + 'title' => $script->title, + 'language' => $script->language, + 'description' => $script->description, + 'code' => $draftCode, + 'run_as_user_id' => $user->id, + 'script_category_id' => $script->script_category_id, + ]); + $response->assertStatus(204); + + $response = $this->apiCall('PUT', route('api.scripts.update', ['script' => $script->id]), [ + 'title' => $script->title, + 'language' => $script->language, + 'description' => $script->description, + 'code' => $publishedCodeAfterSave, + 'run_as_user_id' => $user->id, + 'script_category_id' => $script->script_category_id, + ]); + $response->assertStatus(204); + + $script->refresh(); + $this->assertTrue($script->versions()->draft()->doesntExist()); + $this->assertEquals($publishedCodeAfterSave, $script->code); + } } From f40eea85d6d681684ec832dba1b429f95ce43255 Mon Sep 17 00:00:00 2001 From: Roly Gutierrez Date: Fri, 14 Aug 2026 15:52:16 -0400 Subject: [PATCH 2/2] test(FOUR-24155): set explicit timeout in draft preservation test Avoid 422 validation errors when updating script configuration in tests. --- tests/Feature/Api/ScriptsTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Feature/Api/ScriptsTest.php b/tests/Feature/Api/ScriptsTest.php index 9371c85939..1ff8e4af2d 100644 --- a/tests/Feature/Api/ScriptsTest.php +++ b/tests/Feature/Api/ScriptsTest.php @@ -686,6 +686,7 @@ public function testUpdateScriptConfigurationPreservesDraft() $script = Script::factory()->create([ 'code' => $publishedCode, 'script_category_id' => $category->id, + 'timeout' => 60, ]); $response = $this->apiCall('PUT', route('api.scripts.draft', ['script' => $script->id]), [ @@ -707,7 +708,7 @@ public function testUpdateScriptConfigurationPreservesDraft() 'run_as_user_id' => $user->id, 'script_category_id' => $newCategory->id, 'script_executor_id' => $script->script_executor_id, - 'timeout' => $script->timeout, + 'timeout' => 60, ]); $response->assertStatus(204);