trypost/resources/js/components/automations/config/WebhookNodeConfig.vue
Paulo Castellano 4efaa0bf99 Harden automations module: full-post generation, reliable runs, editor UX
Generation
- Generate node now produces the full post (text + AI image + carousel)
  via a shared PostImagePipeline extracted from StreamPostCreation
- Generate config UI mirrors the /posts/create wizard (carousel slide
  count, include-image toggle); drop the decorative format/unsplash keys

Flow correctness
- RSS/HTTP nodes expose named has-items (default) and no-items output
  handles, labeled and colored like the Condition node
- AdvanceAutomationRun records a no_matching_edge terminal instead of
  completing silently; "0 new items" feedback in the test panel
- Manual/test runs no longer persist the production dedup watermark

Run reliability
- Pause truly halts in-flight runs (production only; manual test runs
  always run regardless of automation status)
- ProcessAutomationNode::failed() marks the run failed
- automation:recover-stuck-runs and automation:prune-dry-runs commands

Webhook / HTTP
- Branded User-Agent (config-driven) on outbound webhook + http_request
- Webhook fails on invalid JSON instead of silently sending {}
- HTTP custom headers editor; CodeMirror-based CodeEditor for JSON

Editor UX
- Header Test button only opens the panel; the panel has a Run button
  (saves first) and owns the with-real-data toggle
- Clicking a node closes the test panel and opens its config
- Node cards: max-width + truncate so long URLs don't grow the node
2026-06-10 20:45:01 -03:00

87 lines
3 KiB
Vue

<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
interface WebhookConfig {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
headers?: Record<string, string>;
payload_template: string;
}
const props = defineProps<{
data: Record<string, unknown>;
errors?: Record<string, string>;
}>();
const emit = defineEmits<{ update: [Record<string, unknown>] }>();
const local = ref<WebhookConfig>({
url: (props.data.url as string) ?? '',
method: (props.data.method as WebhookConfig['method']) ?? 'POST',
headers: (props.data.headers as Record<string, string>) ?? {},
payload_template: (props.data.payload_template as string) ?? '{}',
});
watch(local, (val) => emit('update', val), { deep: true });
const isPayloadJsonInvalid = computed(() => {
const value = local.value.payload_template.trim();
if (value === '') {
return false;
}
try {
JSON.parse(value);
return false;
} catch {
return true;
}
});
</script>
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.webhook.url') }}</label>
<Input v-model="local.url" placeholder="https://hooks.example.com/…" />
<InputError :message="errors?.url" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.webhook.method') }}</label>
<Select v-model="local.method">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="PATCH">PATCH</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
<InputError :message="errors?.method" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.webhook.payload_template') }}</label>
<div class="h-40">
<CodeEditor v-model="local.payload_template" language="json" placeholder='{"content": "{{ post.content }}"}' />
</div>
<p v-if="isPayloadJsonInvalid" class="mt-1 text-xs text-amber-600 dark:text-amber-500">
{{ $t('automations.config.invalid_json') }}
</p>
<InputError :message="errors?.payload_template" class="mt-1" />
</div>
</div>
</template>