Back fixed-set automation strings with enums and consts
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
This commit is contained in:
parent
ed5a757c4f
commit
605261e1b8
33 changed files with 291 additions and 96 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Actions\Automation\Automation;
|
||||
|
||||
use App\Enums\Automation\ScheduleField;
|
||||
use App\Enums\Automation\Status;
|
||||
use App\Enums\Automation\Trigger\Type as TriggerType;
|
||||
use App\Models\Automation;
|
||||
|
|
@ -41,7 +42,7 @@ private function defaultTriggerNode(): array
|
|||
'data' => [
|
||||
'trigger_type' => TriggerType::Schedule->value,
|
||||
'cron' => '0 9 * * *',
|
||||
'schedule_field' => 'days',
|
||||
'schedule_field' => ScheduleField::Days->value,
|
||||
'schedule_days_interval' => 1,
|
||||
'schedule_hour' => 9,
|
||||
'schedule_minute' => 0,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ public function __construct(private ExpressionResolver $resolver) {}
|
|||
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
||||
{
|
||||
$context = $run->resolverContext();
|
||||
$field = $this->resolver->resolve(data_get($config, 'field', ''), $context);
|
||||
$operator = Operator::from(data_get($config, 'operator', 'equals'));
|
||||
$field = $this->resolver->resolve((string) data_get($config, 'field', ''), $context);
|
||||
$operator = Operator::from(data_get($config, 'operator', Operator::Equals->value));
|
||||
$value = $this->resolver->resolve((string) data_get($config, 'value', ''), $context);
|
||||
|
||||
$matched = match ($operator) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Actions\Automation\Node;
|
||||
|
||||
use App\DataTransferObjects\Automation\NodeRunResult;
|
||||
use App\Enums\Automation\DelayUnit;
|
||||
use App\Models\AutomationRun;
|
||||
use InvalidArgumentException;
|
||||
|
||||
|
|
@ -12,13 +13,13 @@ class RunDelayNode
|
|||
{
|
||||
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
||||
{
|
||||
$duration = (int) ($config['duration'] ?? 0);
|
||||
$unit = $config['unit'] ?? 'minutes';
|
||||
$duration = (int) data_get($config, 'duration', 0);
|
||||
$unit = data_get($config, 'unit', DelayUnit::Minutes->value);
|
||||
|
||||
$until = match ($unit) {
|
||||
'minutes' => now()->addMinutes($duration),
|
||||
'hours' => now()->addHours($duration),
|
||||
'days' => now()->addDays($duration),
|
||||
$until = match (DelayUnit::tryFrom((string) $unit)) {
|
||||
DelayUnit::Minutes => now()->addMinutes($duration),
|
||||
DelayUnit::Hours => now()->addHours($duration),
|
||||
DelayUnit::Days => now()->addDays($duration),
|
||||
default => throw new InvalidArgumentException("Unknown delay unit: {$unit}"),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ class RunFetchRssNode
|
|||
{
|
||||
private const ITEM_HANDLE = 'default';
|
||||
|
||||
private const NO_ITEMS_HANDLE = 'no_items';
|
||||
|
||||
public function __construct(
|
||||
private ExpressionResolver $resolver,
|
||||
private SafeHttpFetcher $safeHttp,
|
||||
|
|
@ -90,7 +92,7 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|||
}
|
||||
|
||||
if ($newItems === []) {
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items');
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE);
|
||||
}
|
||||
|
||||
$first = array_shift($newItems);
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ public function __construct(
|
|||
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
||||
{
|
||||
$context = $run->resolverContext();
|
||||
$prompt = $this->resolver->resolve(data_get($config, 'prompt_template', ''), $context);
|
||||
$prompt = $this->resolver->resolve((string) data_get($config, 'prompt_template', ''), $context);
|
||||
|
||||
$accountsConfig = $this->resolveAccountsConfig($config);
|
||||
['format' => $format, 'slide_count' => $slideCount] = $this->deriveFormat($accountsConfig, $config);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
use App\Actions\Automation\Run\AdvanceAutomationRun;
|
||||
use App\DataTransferObjects\Automation\NodeRunResult;
|
||||
use App\Enums\Automation\AuthType;
|
||||
use App\Enums\Automation\HttpMethod;
|
||||
use App\Enums\Automation\Run\Status as RunStatus;
|
||||
use App\Models\AutomationNodeState;
|
||||
use App\Models\AutomationRun;
|
||||
|
|
@ -47,6 +49,8 @@ class RunHttpRequestNode
|
|||
{
|
||||
private const ITEM_HANDLE = 'default';
|
||||
|
||||
private const NO_ITEMS_HANDLE = 'no_items';
|
||||
|
||||
/**
|
||||
* Upper bound on the per-node seen-key history. FIFO-evicts the oldest keys
|
||||
* once exceeded — matching n8n's capped "history size" for its dedup store.
|
||||
|
|
@ -62,7 +66,7 @@ public function __construct(
|
|||
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
||||
{
|
||||
$url = (string) data_get($config, 'url', '');
|
||||
$method = strtoupper((string) data_get($config, 'method', 'GET'));
|
||||
$method = strtoupper((string) data_get($config, 'method', HttpMethod::Get->value));
|
||||
$nodeId = (string) $run->current_node_id;
|
||||
$context = $run->resolverContext();
|
||||
|
||||
|
|
@ -85,12 +89,12 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
|||
$jsonBody = $this->buildJsonBody($method, $config, $context);
|
||||
|
||||
try {
|
||||
$response = match ($method) {
|
||||
'GET' => $request->get($resolvedUrl),
|
||||
'DELETE' => $request->delete($resolvedUrl),
|
||||
'POST' => $request->post($resolvedUrl, $jsonBody),
|
||||
'PUT' => $request->put($resolvedUrl, $jsonBody),
|
||||
'PATCH' => $request->patch($resolvedUrl, $jsonBody),
|
||||
$response = match (HttpMethod::tryFrom($method)) {
|
||||
HttpMethod::Get => $request->get($resolvedUrl),
|
||||
HttpMethod::Delete => $request->delete($resolvedUrl),
|
||||
HttpMethod::Post => $request->post($resolvedUrl, $jsonBody),
|
||||
HttpMethod::Put => $request->put($resolvedUrl, $jsonBody),
|
||||
HttpMethod::Patch => $request->patch($resolvedUrl, $jsonBody),
|
||||
default => null,
|
||||
};
|
||||
} catch (Throwable $e) {
|
||||
|
|
@ -188,7 +192,7 @@ private function processItems(AutomationRun $run, string $nodeId, array $config,
|
|||
// flow, without spawning siblings, advancing watermarks or recording keys.
|
||||
if ($run->is_manual || $run->is_dry_run) {
|
||||
if ($items === []) {
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items');
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE);
|
||||
}
|
||||
|
||||
return NodeRunResult::completed([
|
||||
|
|
@ -206,7 +210,7 @@ private function processItems(AutomationRun $run, string $nodeId, array $config,
|
|||
};
|
||||
|
||||
if ($newItems === []) {
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: 'no_items');
|
||||
return NodeRunResult::completed(['fetch' => ['count' => 0]], nextHandle: self::NO_ITEMS_HANDLE);
|
||||
}
|
||||
|
||||
$first = array_shift($newItems);
|
||||
|
|
@ -333,13 +337,13 @@ private function buildRequest(array $config, array $context): PendingRequest
|
|||
$headers[$k] = $this->resolver->resolve((string) $v, $context);
|
||||
}
|
||||
|
||||
$authType = data_get($config, 'auth_type', 'none');
|
||||
if ($authType === 'bearer') {
|
||||
$authType = AuthType::tryFrom((string) data_get($config, 'auth_type', AuthType::None->value));
|
||||
if ($authType === AuthType::Bearer) {
|
||||
$token = $this->decrypt((string) data_get($config, 'auth_token', ''));
|
||||
if ($token !== '') {
|
||||
$request = $request->withToken($this->resolver->resolve($token, $context));
|
||||
}
|
||||
} elseif ($authType === 'basic') {
|
||||
} elseif ($authType === AuthType::Basic) {
|
||||
$user = (string) data_get($config, 'auth_username', '');
|
||||
$pass = $this->decrypt((string) data_get($config, 'auth_password', ''));
|
||||
if ($user !== '' || $pass !== '') {
|
||||
|
|
@ -348,7 +352,7 @@ private function buildRequest(array $config, array $context): PendingRequest
|
|||
$this->resolver->resolve($pass, $context),
|
||||
);
|
||||
}
|
||||
} elseif ($authType === 'api_key') {
|
||||
} elseif ($authType === AuthType::ApiKey) {
|
||||
$headerName = (string) data_get($config, 'auth_header_name', 'X-API-Key');
|
||||
$token = $this->decrypt((string) data_get($config, 'auth_token', ''));
|
||||
if ($token !== '') {
|
||||
|
|
@ -370,7 +374,7 @@ private function buildRequest(array $config, array $context): PendingRequest
|
|||
*/
|
||||
private function buildJsonBody(string $method, array $config, array $context): array
|
||||
{
|
||||
if (! in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
|
||||
if (! in_array(HttpMethod::tryFrom($method), HttpMethod::withBody(), true)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
namespace App\Actions\Automation\Node;
|
||||
|
||||
use App\DataTransferObjects\Automation\NodeRunResult;
|
||||
use App\Enums\Automation\HttpMethod;
|
||||
use App\Models\AutomationRun;
|
||||
use App\Services\Automation\ExpressionResolver;
|
||||
use App\Services\Brand\SafeHttpFetcher;
|
||||
|
|
@ -22,8 +23,14 @@ public function __construct(
|
|||
public function __invoke(AutomationRun $run, array $config): NodeRunResult
|
||||
{
|
||||
$context = $run->resolverContext();
|
||||
$url = $this->resolver->resolve($config['url'] ?? '', $context);
|
||||
$method = strtoupper($config['method'] ?? 'POST');
|
||||
$url = $this->resolver->resolve((string) data_get($config, 'url', ''), $context);
|
||||
$method = strtoupper((string) data_get($config, 'method', HttpMethod::Post->value));
|
||||
|
||||
if ($url === '') {
|
||||
return NodeRunResult::failed(__('automations.errors.webhook_missing_url'), [
|
||||
'reason' => 'missing_url',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->safeHttp->guardAgainstSsrf($url);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ public function __invoke(AutomationRun $run, string $fromNodeId, string $handle
|
|||
public function targetsFor(Automation $automation, string $fromNodeId, string $handle = 'default'): array
|
||||
{
|
||||
return collect($automation->connections ?? [])
|
||||
->filter(fn ($c) => ($c['source'] ?? null) === $fromNodeId && ($c['source_handle'] ?? 'default') === $handle)
|
||||
->filter(fn ($c) => data_get($c, 'source') === $fromNodeId && data_get($c, 'source_handle', 'default') === $handle)
|
||||
->pluck('target')
|
||||
->filter()
|
||||
->values()
|
||||
|
|
|
|||
17
app/Enums/Automation/AuthType.php
Normal file
17
app/Enums/Automation/AuthType.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Automation;
|
||||
|
||||
/**
|
||||
* Authentication strategies for the HTTP Request node. Mirrors the frontend
|
||||
* AuthType const (resources/js/types/automation/auth-type.ts).
|
||||
*/
|
||||
enum AuthType: string
|
||||
{
|
||||
case None = 'none';
|
||||
case Bearer = 'bearer';
|
||||
case Basic = 'basic';
|
||||
case ApiKey = 'api_key';
|
||||
}
|
||||
16
app/Enums/Automation/DelayUnit.php
Normal file
16
app/Enums/Automation/DelayUnit.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Automation;
|
||||
|
||||
/**
|
||||
* Time units for the Delay node. Mirrors the frontend DelayUnit const
|
||||
* (resources/js/types/automation/delay-unit.ts).
|
||||
*/
|
||||
enum DelayUnit: string
|
||||
{
|
||||
case Minutes = 'minutes';
|
||||
case Hours = 'hours';
|
||||
case Days = 'days';
|
||||
}
|
||||
28
app/Enums/Automation/HttpMethod.php
Normal file
28
app/Enums/Automation/HttpMethod.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Automation;
|
||||
|
||||
/**
|
||||
* HTTP verbs available to the HTTP Request and Webhook nodes. Mirrors the
|
||||
* frontend HttpMethod const (resources/js/types/automation/http-method.ts).
|
||||
*/
|
||||
enum HttpMethod: string
|
||||
{
|
||||
case Get = 'GET';
|
||||
case Post = 'POST';
|
||||
case Put = 'PUT';
|
||||
case Patch = 'PATCH';
|
||||
case Delete = 'DELETE';
|
||||
|
||||
/**
|
||||
* Verbs that carry a request body.
|
||||
*
|
||||
* @return array<int, self>
|
||||
*/
|
||||
public static function withBody(): array
|
||||
{
|
||||
return [self::Post, self::Put, self::Patch];
|
||||
}
|
||||
}
|
||||
18
app/Enums/Automation/ScheduleField.php
Normal file
18
app/Enums/Automation/ScheduleField.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\Automation;
|
||||
|
||||
/**
|
||||
* Schedule interval units for a Schedule trigger. Mirrors the frontend
|
||||
* ScheduleField const (resources/js/types/automation/schedule-field.ts).
|
||||
*/
|
||||
enum ScheduleField: string
|
||||
{
|
||||
case Minutes = 'minutes';
|
||||
case Hours = 'hours';
|
||||
case Days = 'days';
|
||||
case Weeks = 'weeks';
|
||||
case Months = 'months';
|
||||
}
|
||||
|
|
@ -4,9 +4,13 @@
|
|||
|
||||
namespace App\Http\Requests\App\Automations;
|
||||
|
||||
use App\Enums\Automation\AuthType;
|
||||
use App\Enums\Automation\Condition\Operator as ConditionOperator;
|
||||
use App\Enums\Automation\DelayUnit;
|
||||
use App\Enums\Automation\HttpMethod;
|
||||
use App\Enums\Automation\Node\Type as NodeType;
|
||||
use App\Enums\Automation\Publish\Mode as PublishMode;
|
||||
use App\Enums\Automation\ScheduleField;
|
||||
use App\Enums\Automation\Trigger\Type as TriggerType;
|
||||
use App\Services\Automation\GenerateNodeValidator;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
|
|
@ -121,7 +125,7 @@ private function dataRulesForNodeType(?string $type, int $i): array
|
|||
NodeType::Trigger->value => [
|
||||
'trigger_type' => ['required', Rule::in(array_column(TriggerType::cases(), 'value'))],
|
||||
'cron' => ['required_if:nodes.'.$i.'.data.trigger_type,'.TriggerType::Schedule->value, 'string'],
|
||||
'schedule_field' => ['sometimes', Rule::in(['minutes', 'hours', 'days', 'weeks', 'months'])],
|
||||
'schedule_field' => ['sometimes', Rule::in(array_column(ScheduleField::cases(), 'value'))],
|
||||
'schedule_minutes_interval' => ['sometimes', 'integer', 'min:1', 'max:59'],
|
||||
'schedule_hours_interval' => ['sometimes', 'integer', 'min:1', 'max:23'],
|
||||
'schedule_days_interval' => ['sometimes', 'integer', 'min:1', 'max:31'],
|
||||
|
|
@ -137,8 +141,8 @@ private function dataRulesForNodeType(?string $type, int $i): array
|
|||
],
|
||||
NodeType::HttpRequest->value => [
|
||||
'url' => ['required', 'url'],
|
||||
'method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])],
|
||||
'auth_type' => ['required', Rule::in(['none', 'bearer', 'basic', 'api_key'])],
|
||||
'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))],
|
||||
'auth_type' => ['required', Rule::in(array_column(AuthType::cases(), 'value'))],
|
||||
'auth_token' => ['nullable', 'string'],
|
||||
'auth_username' => ['nullable', 'string'],
|
||||
'auth_password' => ['nullable', 'string'],
|
||||
|
|
@ -159,7 +163,7 @@ private function dataRulesForNodeType(?string $type, int $i): array
|
|||
],
|
||||
NodeType::Delay->value => [
|
||||
'duration' => ['required', 'integer', 'min:1'],
|
||||
'unit' => ['required', Rule::in(['minutes', 'hours', 'days'])],
|
||||
'unit' => ['required', Rule::in(array_column(DelayUnit::cases(), 'value'))],
|
||||
],
|
||||
NodeType::Condition->value => [
|
||||
'field' => ['required', 'string'],
|
||||
|
|
@ -172,7 +176,7 @@ private function dataRulesForNodeType(?string $type, int $i): array
|
|||
],
|
||||
NodeType::Webhook->value => [
|
||||
'url' => ['required', 'url'],
|
||||
'method' => ['required', Rule::in(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])],
|
||||
'method' => ['required', Rule::in(array_column(HttpMethod::cases(), 'value'))],
|
||||
'payload_template' => ['nullable', 'string'],
|
||||
'headers' => ['nullable', 'array'],
|
||||
'headers.*' => ['string'],
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
'no_generated_post' => 'No generated post found on run.',
|
||||
'webhook_server_error' => 'Webhook server error.',
|
||||
'webhook_request_failed' => 'The webhook request could not be completed.',
|
||||
'webhook_missing_url' => 'The webhook node is missing a URL.',
|
||||
'webhook_invalid_payload_json' => 'The payload template is not valid JSON.',
|
||||
'url_not_allowed' => 'The request URL points to a private or unreachable address and was blocked.',
|
||||
'node_no_longer_exists' => 'Node :node_id no longer exists in the automation.',
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
'no_generated_post' => 'No se encontró un post generado en la ejecución.',
|
||||
'webhook_server_error' => 'Error del servidor del webhook.',
|
||||
'webhook_request_failed' => 'No se pudo completar la solicitud del webhook.',
|
||||
'webhook_missing_url' => 'Al nodo de webhook le falta la URL.',
|
||||
'webhook_invalid_payload_json' => 'La plantilla de payload no es un JSON válido.',
|
||||
'url_not_allowed' => 'La URL de la petición apunta a una dirección privada o inaccesible y fue bloqueada.',
|
||||
'node_no_longer_exists' => 'El nodo :node_id ya no existe en la automatización.',
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@
|
|||
'no_generated_post' => 'Nenhum post gerado encontrado para esta execução.',
|
||||
'webhook_server_error' => 'Erro no servidor do webhook.',
|
||||
'webhook_request_failed' => 'Não foi possível completar a requisição do webhook.',
|
||||
'webhook_missing_url' => 'O nó de webhook está sem a URL.',
|
||||
'webhook_invalid_payload_json' => 'O template do payload não é um JSON válido.',
|
||||
'url_not_allowed' => 'A URL da requisição aponta para um endereço privado ou inacessível e foi bloqueada.',
|
||||
'node_no_longer_exists' => 'O nó :node_id não existe mais nesta automação.',
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ConditionOperator, type ConditionOperatorValue } from '@/types/automation/condition-operator';
|
||||
|
||||
interface ConditionConfig {
|
||||
field: string;
|
||||
operator: string;
|
||||
operator: ConditionOperatorValue;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ const emit = defineEmits<{ update: [Record<string, unknown>] }>();
|
|||
|
||||
const local = ref<ConditionConfig>({
|
||||
field: (props.data.field as string) ?? '',
|
||||
operator: (props.data.operator as string) ?? 'contains',
|
||||
operator: (props.data.operator as ConditionOperatorValue) ?? ConditionOperator.Contains,
|
||||
value: (props.data.value as string) ?? '',
|
||||
});
|
||||
|
||||
|
|
@ -47,13 +48,13 @@ watch(local, (val) => emit('update', val), { deep: true });
|
|||
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="contains">{{ $t('automations.config.condition.operators.contains') }}</SelectItem>
|
||||
<SelectItem value="not_contains">{{ $t('automations.config.condition.operators.not_contains') }}</SelectItem>
|
||||
<SelectItem value="equals">{{ $t('automations.config.condition.operators.equals') }}</SelectItem>
|
||||
<SelectItem value="not_equals">{{ $t('automations.config.condition.operators.not_equals') }}</SelectItem>
|
||||
<SelectItem value="matches">{{ $t('automations.config.condition.operators.matches') }}</SelectItem>
|
||||
<SelectItem value="greater_than">{{ $t('automations.config.condition.operators.greater_than') }}</SelectItem>
|
||||
<SelectItem value="less_than">{{ $t('automations.config.condition.operators.less_than') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.Contains">{{ $t('automations.config.condition.operators.contains') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.NotContains">{{ $t('automations.config.condition.operators.not_contains') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.Equals">{{ $t('automations.config.condition.operators.equals') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.NotEquals">{{ $t('automations.config.condition.operators.not_equals') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.Matches">{{ $t('automations.config.condition.operators.matches') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.GreaterThan">{{ $t('automations.config.condition.operators.greater_than') }}</SelectItem>
|
||||
<SelectItem :value="ConditionOperator.LessThan">{{ $t('automations.config.condition.operators.less_than') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError :message="errors?.operator" class="mt-1" />
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { DelayUnit, type DelayUnitValue } from '@/types/automation/delay-unit';
|
||||
|
||||
interface DelayConfig {
|
||||
duration: number;
|
||||
unit: 'minutes' | 'hours' | 'days';
|
||||
unit: DelayUnitValue;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -24,7 +25,7 @@ const emit = defineEmits<{ update: [Record<string, unknown>] }>();
|
|||
|
||||
const local = ref<DelayConfig>({
|
||||
duration: (props.data.duration as number) ?? 1,
|
||||
unit: (props.data.unit as DelayConfig['unit']) ?? 'hours',
|
||||
unit: (props.data.unit as DelayConfig['unit']) ?? DelayUnit.Hours,
|
||||
});
|
||||
|
||||
watch(local, (val) => emit('update', val), { deep: true });
|
||||
|
|
@ -45,9 +46,9 @@ watch(local, (val) => emit('update', val), { deep: true });
|
|||
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="minutes">{{ $t('automations.config.delay.units.minutes') }}</SelectItem>
|
||||
<SelectItem value="hours">{{ $t('automations.config.delay.units.hours') }}</SelectItem>
|
||||
<SelectItem value="days">{{ $t('automations.config.delay.units.days') }}</SelectItem>
|
||||
<SelectItem :value="DelayUnit.Minutes">{{ $t('automations.config.delay.units.minutes') }}</SelectItem>
|
||||
<SelectItem :value="DelayUnit.Hours">{{ $t('automations.config.delay.units.hours') }}</SelectItem>
|
||||
<SelectItem :value="DelayUnit.Days">{{ $t('automations.config.delay.units.days') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError :message="errors?.unit" class="mt-1" />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-vue';
|
||||
|
||||
import CodeEditor from '@/components/CodeEditor.vue';
|
||||
import InputError from '@/components/InputError.vue';
|
||||
|
|
@ -15,14 +15,15 @@ import {
|
|||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useExpandedEditor } from '@/composables/useExpandedEditor';
|
||||
import { AuthType, type AuthTypeValue } from '@/types/automation/auth-type';
|
||||
import { HTTP_METHODS, HTTP_METHODS_WITH_BODY, HttpMethod, type HttpMethodValue } from '@/types/automation/http-method';
|
||||
|
||||
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
type AuthType = 'none' | 'bearer' | 'basic' | 'api_key';
|
||||
type Method = HttpMethodValue;
|
||||
|
||||
interface HttpRequestConfig {
|
||||
url: string;
|
||||
method: Method;
|
||||
auth_type: AuthType;
|
||||
auth_type: AuthTypeValue;
|
||||
auth_token: string;
|
||||
auth_username: string;
|
||||
auth_password: string;
|
||||
|
|
@ -42,8 +43,8 @@ const emit = defineEmits<{ update: [Record<string, unknown>] }>();
|
|||
|
||||
const local = ref<HttpRequestConfig>({
|
||||
url: (props.data.url as string) ?? '',
|
||||
method: (props.data.method as Method) ?? 'GET',
|
||||
auth_type: (props.data.auth_type as AuthType) ?? 'none',
|
||||
method: (props.data.method as Method) ?? HttpMethod.Get,
|
||||
auth_type: (props.data.auth_type as AuthTypeValue) ?? AuthType.None,
|
||||
auth_token: (props.data.auth_token as string) ?? '',
|
||||
auth_username: (props.data.auth_username as string) ?? '',
|
||||
auth_password: (props.data.auth_password as string) ?? '',
|
||||
|
|
@ -94,7 +95,7 @@ watch(local, (val) => emit('update', val), { deep: true });
|
|||
|
||||
const editorExpanded = useExpandedEditor();
|
||||
|
||||
const supportsBody = computed(() => ['POST', 'PUT', 'PATCH'].includes(local.value.method));
|
||||
const supportsBody = computed(() => HTTP_METHODS_WITH_BODY.includes(local.value.method));
|
||||
|
||||
const isBodyJsonInvalid = computed(() => {
|
||||
const value = local.value.body_template.trim();
|
||||
|
|
@ -120,11 +121,7 @@ const isBodyJsonInvalid = computed(() => {
|
|||
<SelectValue />
|
||||
</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>
|
||||
<SelectItem v-for="m in HTTP_METHODS" :key="m" :value="m">{{ m }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError :message="errors?.method" class="mt-1" />
|
||||
|
|
@ -143,21 +140,21 @@ const isBodyJsonInvalid = computed(() => {
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">{{ $t('automations.config.http_request.auth.none') }}</SelectItem>
|
||||
<SelectItem value="bearer">{{ $t('automations.config.http_request.auth.bearer') }}</SelectItem>
|
||||
<SelectItem value="basic">{{ $t('automations.config.http_request.auth.basic') }}</SelectItem>
|
||||
<SelectItem value="api_key">{{ $t('automations.config.http_request.auth.api_key') }}</SelectItem>
|
||||
<SelectItem :value="AuthType.None">{{ $t('automations.config.http_request.auth.none') }}</SelectItem>
|
||||
<SelectItem :value="AuthType.Bearer">{{ $t('automations.config.http_request.auth.bearer') }}</SelectItem>
|
||||
<SelectItem :value="AuthType.Basic">{{ $t('automations.config.http_request.auth.basic') }}</SelectItem>
|
||||
<SelectItem :value="AuthType.ApiKey">{{ $t('automations.config.http_request.auth.api_key') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="local.auth_type === 'bearer'">
|
||||
<div v-if="local.auth_type === AuthType.Bearer">
|
||||
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.bearer_token') }}</label>
|
||||
<Input v-model="local.auth_token" type="password" autocomplete="off" placeholder="sk-…" />
|
||||
<InputError :message="errors?.auth_token" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<template v-if="local.auth_type === 'basic'">
|
||||
<template v-if="local.auth_type === AuthType.Basic">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.basic_username') }}</label>
|
||||
<Input v-model="local.auth_username" autocomplete="off" />
|
||||
|
|
@ -170,7 +167,7 @@ const isBodyJsonInvalid = computed(() => {
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="local.auth_type === 'api_key'">
|
||||
<template v-if="local.auth_type === AuthType.ApiKey">
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.api_key_header') }}</label>
|
||||
<Input v-model="local.auth_header_name" placeholder="X-API-Key" />
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { PublishMode, type PublishModeValue } from '@/types/automation/publish-mode';
|
||||
|
||||
interface PublishConfig {
|
||||
mode: 'now' | 'scheduled' | 'draft';
|
||||
mode: PublishModeValue;
|
||||
scheduled_offset?: number;
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ const props = defineProps<{
|
|||
const emit = defineEmits<{ update: [Record<string, unknown>] }>();
|
||||
|
||||
const local = ref<PublishConfig>({
|
||||
mode: (props.data.mode as PublishConfig['mode']) ?? 'now',
|
||||
mode: (props.data.mode as PublishConfig['mode']) ?? PublishMode.Now,
|
||||
scheduled_offset: (props.data.scheduled_offset as number) ?? 60,
|
||||
});
|
||||
|
||||
|
|
@ -39,15 +40,15 @@ watch(local, (val) => emit('update', val), { deep: true });
|
|||
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="now">{{ $t('automations.config.publish.modes.now') }}</SelectItem>
|
||||
<SelectItem value="scheduled">{{ $t('automations.config.publish.modes.scheduled') }}</SelectItem>
|
||||
<SelectItem value="draft">{{ $t('automations.config.publish.modes.draft') }}</SelectItem>
|
||||
<SelectItem :value="PublishMode.Now">{{ $t('automations.config.publish.modes.now') }}</SelectItem>
|
||||
<SelectItem :value="PublishMode.Scheduled">{{ $t('automations.config.publish.modes.scheduled') }}</SelectItem>
|
||||
<SelectItem :value="PublishMode.Draft">{{ $t('automations.config.publish.modes.draft') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError :message="errors?.mode" class="mt-1" />
|
||||
</div>
|
||||
|
||||
<div v-if="local.mode === 'scheduled'">
|
||||
<div v-if="local.mode === PublishMode.Scheduled">
|
||||
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.publish.scheduled_offset') }}</label>
|
||||
<Input type="number" v-model.number="local.scheduled_offset" placeholder="60" />
|
||||
<InputError :message="errors?.scheduled_offset" class="mt-1" />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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 { useExpandedEditor } from '@/composables/useExpandedEditor';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -12,10 +11,12 @@ import {
|
|||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { useExpandedEditor } from '@/composables/useExpandedEditor';
|
||||
import { HTTP_METHODS, HttpMethod, type HttpMethodValue } from '@/types/automation/http-method';
|
||||
|
||||
interface WebhookConfig {
|
||||
url: string;
|
||||
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
||||
method: HttpMethodValue;
|
||||
headers?: Record<string, string>;
|
||||
payload_template: string;
|
||||
}
|
||||
|
|
@ -30,7 +31,7 @@ const editorExpanded = useExpandedEditor();
|
|||
|
||||
const local = ref<WebhookConfig>({
|
||||
url: (props.data.url as string) ?? '',
|
||||
method: (props.data.method as WebhookConfig['method']) ?? 'POST',
|
||||
method: (props.data.method as WebhookConfig['method']) ?? HttpMethod.Post,
|
||||
headers: (props.data.headers as Record<string, string>) ?? {},
|
||||
payload_template: (props.data.payload_template as string) ?? '{}',
|
||||
});
|
||||
|
|
@ -66,11 +67,7 @@ const isPayloadJsonInvalid = computed(() => {
|
|||
<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>
|
||||
<SelectItem v-for="m in HTTP_METHODS" :key="m" :value="m">{{ m }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError :message="errors?.method" class="mt-1" />
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Handle, Position } from '@vue-flow/core';
|
|||
import { computed } from 'vue';
|
||||
|
||||
import { ConditionHandle } from '@/types/automation/condition-handle';
|
||||
import { ConditionOperator } from '@/types/automation/condition-operator';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
|
|
@ -16,7 +17,7 @@ const props = defineProps<{
|
|||
|
||||
const summary = computed(() => {
|
||||
const field = props.data.field || '…';
|
||||
const operator = props.data.operator || 'contains';
|
||||
const operator = props.data.operator || ConditionOperator.Contains;
|
||||
const value = props.data.value || '…';
|
||||
return `${field} ${operator} ${value}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { IconClock } from '@tabler/icons-vue';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { DelayUnit } from '@/types/automation/delay-unit';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
duration?: number;
|
||||
|
|
@ -13,7 +15,7 @@ const props = defineProps<{
|
|||
|
||||
const summary = computed(() => {
|
||||
const duration = props.data.duration ?? 1;
|
||||
const unit = props.data.unit ?? 'hours';
|
||||
const unit = props.data.unit ?? DelayUnit.Hours;
|
||||
return `${duration} ${unit}`;
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { IconWorld } from '@tabler/icons-vue';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { HttpMethod } from '@/types/automation/http-method';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
url?: string;
|
||||
|
|
@ -12,7 +14,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const summary = computed(() => {
|
||||
const method = (props.data.method ?? 'GET').toUpperCase();
|
||||
const method = (props.data.method ?? HttpMethod.Get).toUpperCase();
|
||||
const url = props.data.url;
|
||||
if (!url) return method;
|
||||
let host = url;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { IconSend } from '@tabler/icons-vue';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { PublishMode } from '@/types/automation/publish-mode';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
mode?: string;
|
||||
|
|
@ -12,8 +14,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const summary = computed(() => {
|
||||
const mode = props.data.mode ?? 'now';
|
||||
if (mode === 'scheduled' && props.data.scheduled_offset != null) {
|
||||
const mode = props.data.mode ?? PublishMode.Now;
|
||||
if (mode === PublishMode.Scheduled && props.data.scheduled_offset != null) {
|
||||
return `scheduled · +${props.data.scheduled_offset} min`;
|
||||
}
|
||||
return mode;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
<script setup lang="ts">
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { IconWebhook } from '@tabler/icons-vue';
|
||||
import { Handle, Position } from '@vue-flow/core';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { HttpMethod } from '@/types/automation/http-method';
|
||||
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
url?: string;
|
||||
|
|
@ -12,7 +14,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const summary = computed(() => {
|
||||
const method = (props.data.method ?? 'POST').toUpperCase();
|
||||
const method = (props.data.method ?? HttpMethod.Post).toUpperCase();
|
||||
const url = props.data.url || 'https://…';
|
||||
return `${method} · ${url}`;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,8 +57,13 @@ import { usePageErrors } from '@/composables/usePageErrors';
|
|||
import { useShortcut } from '@/composables/useShortcut';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { update as updateAutomation } from '@/routes/app/automations';
|
||||
import { AuthType } from '@/types/automation/auth-type';
|
||||
import type { Automation, AutomationVariable } from '@/types/automation/automation';
|
||||
import { ConditionOperator } from '@/types/automation/condition-operator';
|
||||
import { DelayUnit } from '@/types/automation/delay-unit';
|
||||
import { HttpMethod } from '@/types/automation/http-method';
|
||||
import { NodeType } from '@/types/automation/node-type';
|
||||
import { PublishMode } from '@/types/automation/publish-mode';
|
||||
import type { RawConnection } from '@/types/automation/raw-connection';
|
||||
import { ScheduleField } from '@/types/automation/schedule-field';
|
||||
import { TriggerType } from '@/types/automation/trigger-type';
|
||||
|
|
@ -262,16 +267,16 @@ const defaultConfigFor = (type: string): Record<string, unknown> => {
|
|||
schedule_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
};
|
||||
case NodeType.Generate: return { accounts: [], prompt_template: '', target_slide_count: 1 };
|
||||
case NodeType.Delay: return { duration: 1, unit: 'hours' };
|
||||
case NodeType.Condition: return { field: '', operator: 'contains', value: '' };
|
||||
case NodeType.Publish: return { mode: 'now', scheduled_offset: 60 };
|
||||
case NodeType.Webhook: return { url: '', method: 'POST', headers: {}, payload_template: '{}' };
|
||||
case NodeType.Delay: return { duration: 1, unit: DelayUnit.Hours };
|
||||
case NodeType.Condition: return { field: '', operator: ConditionOperator.Contains, value: '' };
|
||||
case NodeType.Publish: return { mode: PublishMode.Now, scheduled_offset: 60 };
|
||||
case NodeType.Webhook: return { url: '', method: HttpMethod.Post, headers: {}, payload_template: '{}' };
|
||||
case NodeType.End: return { reason: '' };
|
||||
case NodeType.FetchRss: return { feed_url: '' };
|
||||
case NodeType.HttpRequest: return {
|
||||
url: '',
|
||||
method: 'GET',
|
||||
auth_type: 'none',
|
||||
method: HttpMethod.Get,
|
||||
auth_type: AuthType.None,
|
||||
headers: {},
|
||||
body_template: '',
|
||||
items_path: '',
|
||||
|
|
|
|||
12
resources/js/types/automation/auth-type.ts
Normal file
12
resources/js/types/automation/auth-type.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Auth strategies for the HTTP Request node. Mirrors the backend
|
||||
* App\Enums\Automation\AuthType enum.
|
||||
*/
|
||||
export const AuthType = {
|
||||
None: 'none',
|
||||
Bearer: 'bearer',
|
||||
Basic: 'basic',
|
||||
ApiKey: 'api_key',
|
||||
} as const;
|
||||
|
||||
export type AuthTypeValue = (typeof AuthType)[keyof typeof AuthType];
|
||||
15
resources/js/types/automation/condition-operator.ts
Normal file
15
resources/js/types/automation/condition-operator.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Comparison operators for the Condition node. Mirrors the backend
|
||||
* App\Enums\Automation\Condition\Operator enum.
|
||||
*/
|
||||
export const ConditionOperator = {
|
||||
Contains: 'contains',
|
||||
NotContains: 'not_contains',
|
||||
Equals: 'equals',
|
||||
NotEquals: 'not_equals',
|
||||
Matches: 'matches',
|
||||
GreaterThan: 'greater_than',
|
||||
LessThan: 'less_than',
|
||||
} as const;
|
||||
|
||||
export type ConditionOperatorValue = (typeof ConditionOperator)[keyof typeof ConditionOperator];
|
||||
11
resources/js/types/automation/delay-unit.ts
Normal file
11
resources/js/types/automation/delay-unit.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Time units for the Delay node. Mirrors the backend
|
||||
* App\Enums\Automation\DelayUnit enum.
|
||||
*/
|
||||
export const DelayUnit = {
|
||||
Minutes: 'minutes',
|
||||
Hours: 'hours',
|
||||
Days: 'days',
|
||||
} as const;
|
||||
|
||||
export type DelayUnitValue = (typeof DelayUnit)[keyof typeof DelayUnit];
|
||||
19
resources/js/types/automation/http-method.ts
Normal file
19
resources/js/types/automation/http-method.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* HTTP verbs for the HTTP Request and Webhook nodes. Mirrors the backend
|
||||
* App\Enums\Automation\HttpMethod enum — these values feed the node config
|
||||
* selects and must match the validation rules server-side.
|
||||
*/
|
||||
export const HttpMethod = {
|
||||
Get: 'GET',
|
||||
Post: 'POST',
|
||||
Put: 'PUT',
|
||||
Patch: 'PATCH',
|
||||
Delete: 'DELETE',
|
||||
} as const;
|
||||
|
||||
export type HttpMethodValue = (typeof HttpMethod)[keyof typeof HttpMethod];
|
||||
|
||||
export const HTTP_METHODS: HttpMethodValue[] = Object.values(HttpMethod);
|
||||
|
||||
/** Verbs that carry a request body (mirror HttpMethod::withBody on the backend). */
|
||||
export const HTTP_METHODS_WITH_BODY: HttpMethodValue[] = [HttpMethod.Post, HttpMethod.Put, HttpMethod.Patch];
|
||||
11
resources/js/types/automation/publish-mode.ts
Normal file
11
resources/js/types/automation/publish-mode.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Publish modes for the Publish node. Mirrors the backend
|
||||
* App\Enums\Automation\Publish\Mode enum.
|
||||
*/
|
||||
export const PublishMode = {
|
||||
Now: 'now',
|
||||
Scheduled: 'scheduled',
|
||||
Draft: 'draft',
|
||||
} as const;
|
||||
|
||||
export type PublishModeValue = (typeof PublishMode)[keyof typeof PublishMode];
|
||||
|
|
@ -124,6 +124,21 @@
|
|||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('fails with a clear error when the url is missing', function () {
|
||||
Http::fake();
|
||||
|
||||
$run = AutomationRun::factory()->create();
|
||||
|
||||
$result = app(RunWebhookNode::class)($run, [
|
||||
'method' => 'POST',
|
||||
'payload_template' => '{}',
|
||||
]);
|
||||
|
||||
expect($result->status)->toBe(Status::Failed);
|
||||
expect($result->error['reason'])->toBe('missing_url');
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('blocks a request to a private or reserved address', function () {
|
||||
Http::fake();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue