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..1ff8e4af2d 100644 --- a/tests/Feature/Api/ScriptsTest.php +++ b/tests/Feature/Api/ScriptsTest.php @@ -674,4 +674,90 @@ 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, + 'timeout' => 60, + ]); + + $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' => 60, + ]); + $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); + } }