From 6bfb88dedeb4aed148051076e523d2819f4a9afc Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Fri, 24 Jul 2026 23:26:32 -0300 Subject: [PATCH] Fix Generate node clamping image count to 0 for Pinterest. Empty-account mount was clamping target_slide_count to 0 and never raising it when a media-required account was selected, so Pinterest showed a false requires-media error after picking a board. Co-authored-by: Cursor --- .../automations/config/GenerateNodeConfig.vue | 36 ++++++++++++---- .../Automation/GenerateNodeValidationTest.php | 41 +++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/resources/js/components/automations/config/GenerateNodeConfig.vue b/resources/js/components/automations/config/GenerateNodeConfig.vue index 17a35e87..983015c7 100644 --- a/resources/js/components/automations/config/GenerateNodeConfig.vue +++ b/resources/js/components/automations/config/GenerateNodeConfig.vue @@ -232,11 +232,24 @@ const imageCountCap = computed(() => ), ); -// Single picker: 0 = no image (text-only), 1 = single image, 2+ = carousel. -const imageCountOptions = computed(() => - Array.from({ length: imageCountCap.value + 1 }, (_, i) => i), +// Floor at 1 when any selected account requires media (Pinterest pin, IG feed, +// etc.) — otherwise the empty-accounts clamp to 0 sticks after selecting them +// and surfaces a false "add an image" compliance error. +const minImageCount = computed(() => + local.value.accounts.some((a) => getMediaRulesForContentType(a.content_type).requiresMedia) + ? 1 + : 0, ); +// Single picker: 0 = no image (text-only), 1 = single image, 2+ = carousel. +// Option 0 is omitted when a media-required account is selected. +const imageCountOptions = computed(() => { + const min = Math.min(minImageCount.value, imageCountCap.value); + const max = imageCountCap.value; + + return Array.from({ length: Math.max(0, max - min + 1) }, (_, i) => i + min); +}); + const intendedImageCount = computed(() => local.value.target_slide_count); const syntheticImages = (count: number): MediaItem[] => @@ -254,14 +267,23 @@ const accountIssue = (accountId: string): string | null => { return account ? getPlatformMetaIssue(account.platform, entry.meta) : null; }; -// Clamp the chosen count to what the selected accounts actually allow — runs on -// mount too so legacy/over-cap values (or text-only accounts → 0) self-correct. +// Clamp the chosen count into [min, cap] for the current selection. Skip while +// no accounts are selected so the default of 1 is preserved until the user +// picks a destination (avoids clamp-to-0 → select Pinterest → stuck at 0). watch( - imageCountCap, - (cap) => { + [imageCountCap, minImageCount, () => local.value.accounts.length], + ([cap, min, accountCount]) => { + if (accountCount === 0) { + return; + } + if (local.value.target_slide_count > cap) { local.value.target_slide_count = cap; } + + if (local.value.target_slide_count < min) { + local.value.target_slide_count = Math.min(min, cap); + } }, { immediate: true }, ); diff --git a/tests/Feature/Automation/GenerateNodeValidationTest.php b/tests/Feature/Automation/GenerateNodeValidationTest.php index 097662c6..1baf5f85 100644 --- a/tests/Feature/Automation/GenerateNodeValidationTest.php +++ b/tests/Feature/Automation/GenerateNodeValidationTest.php @@ -58,6 +58,47 @@ expect($automation->fresh()->nodes)->toHaveCount(2); }); +it('rejects a generate node that targets Pinterest with zero images', function () { + $automation = Automation::factory()->for($this->workspace)->create(); + + $this->actingAs($this->user) + ->putJson(route('app.automations.update', $automation->id), [ + 'nodes' => [ + ['id' => 'n1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'schedule', 'cron' => '0 9 * * *']], + ['id' => 'n2', 'type' => 'generate', 'position' => ['x' => 1, 'y' => 0], 'data' => [ + 'accounts' => [['social_account_id' => 'acc-1', 'content_type' => 'pinterest_pin', 'meta' => ['board_id' => 'board-1']]], + 'prompt_template' => 'hi', + 'target_slide_count' => 0, + ]], + ], + 'connections' => [['id' => 'e1', 'source' => 'n1', 'target' => 'n2']], + ]) + ->assertStatus(422) + ->assertJsonValidationErrors(['nodes.1.data.accounts']); +}); + +it('allows saving a Pinterest pin generate node with one image', function () { + $automation = Automation::factory()->for($this->workspace)->create(); + + $this->actingAs($this->user) + ->put(route('app.automations.update', $automation->id), [ + 'nodes' => [ + ['id' => 'n1', 'type' => 'trigger', 'position' => ['x' => 0, 'y' => 0], 'data' => ['trigger_type' => 'schedule', 'cron' => '0 9 * * *']], + ['id' => 'n2', 'type' => 'generate', 'position' => ['x' => 1, 'y' => 0], 'data' => [ + 'accounts' => [['social_account_id' => 'acc-1', 'content_type' => 'pinterest_pin', 'meta' => ['board_id' => 'board-1']]], + 'prompt_template' => 'hi', + 'target_slide_count' => 1, + 'style' => 'tweet_card', + ]], + ], + 'connections' => [['id' => 'e1', 'source' => 'n1', 'target' => 'n2']], + ]) + ->assertRedirect() + ->assertSessionHasNoErrors(); + + expect($automation->fresh()->nodes)->toHaveCount(2); +}); + it('persists the brand toggles on the generate node', function () { $automation = Automation::factory()->for($this->workspace)->create();