Tidy automation frontend: extract CSS, use the Label component

- Move the automation-node and json-viewer styles out of app.css into their
  own imported stylesheets.
- Replace raw <label> elements across the node config panels and the test
  panel with the shared ui/label component.
This commit is contained in:
Paulo Castellano 2026-06-13 16:30:39 -03:00
parent 730cb9d156
commit ff8fd9de40
17 changed files with 174 additions and 172 deletions

View file

@ -1,6 +1,8 @@
@import 'tailwindcss';
@import 'tw-animate-css';
@import 'vue-sonner/style.css';
@import './automations.css';
@import './json-viewer.css';
@plugin '@tailwindcss/typography';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@ -165,128 +167,3 @@ .trypost-container {
@apply relative max-w-7xl w-full flex-1 mx-auto flex flex-col justify-center;
}
/* Automations canvas nodes TryPost brutalist identity:
thick ink borders, hard offset shadow, warm card background, colored header
strip that fills the top of the card with the accent tint. */
.automation-node {
position: relative;
min-width: 230px;
max-width: 260px;
background: var(--card);
border: 2px solid var(--foreground);
border-radius: 14px;
box-shadow: 3px 3px 0 var(--foreground);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.automation-node:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--foreground);
}
.automation-node--wide {
min-width: 260px;
max-width: 260px;
}
.automation-node.is-selected {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0 #7c3aed;
}
.automation-node__header {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.875rem;
border-bottom: 2px solid var(--foreground);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.automation-node__icon-tile {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 2px solid var(--foreground);
border-radius: 8px;
flex-shrink: 0;
transform: rotate(-3deg);
}
/* Header accent tint mirrors the icon tile color, applied to the whole header
strip so the node type is identifiable at a glance from the canvas. */
.automation-node__icon-tile--violet { background: #ede9fe; color: #5b21b6; }
.automation-node__icon-tile--blue { background: #dbeafe; color: #1d4ed8; }
.automation-node__icon-tile--amber { background: #fef3c7; color: #92400e; }
.automation-node__icon-tile--rose { background: #ffe4e6; color: #be123c; }
.automation-node__icon-tile--emerald { background: #d1fae5; color: #047857; }
.automation-node__icon-tile--slate { background: #e2e8f0; color: #334155; }
.automation-node__icon-tile--zinc { background: #e4e4e7; color: #27272a; }
.automation-node__icon-tile--cyan { background: #cffafe; color: #155e75; }
.automation-node--accent-violet .automation-node__header { background: #f5f3ff; }
.automation-node--accent-blue .automation-node__header { background: #eff6ff; }
.automation-node--accent-amber .automation-node__header { background: #fffbeb; }
.automation-node--accent-rose .automation-node__header { background: #fff1f2; }
.automation-node--accent-emerald .automation-node__header { background: #ecfdf5; }
.automation-node--accent-slate .automation-node__header { background: #f1f5f9; }
.automation-node--accent-zinc .automation-node__header { background: #f4f4f5; }
.automation-node--accent-cyan .automation-node__header { background: #ecfeff; }
.automation-node__title {
min-width: 0;
font-weight: 700;
font-size: 0.875rem;
color: var(--foreground);
line-height: 1.2;
letter-spacing: -0.005em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.automation-node__summary {
padding: 0.625rem 0.875rem 0.75rem 0.875rem;
font-size: 0.75rem;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 70%, transparent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.automation-node__branches {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.375rem 0.875rem 0.625rem 0.875rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.automation-node__branch--yes { color: #047857; }
.automation-node__branch--no { color: #be123c; }
/* JsonViewer — GitHub Light palette for highlight.js JSON output. */
.json-viewer__body {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
margin: 0;
background: #ffffff;
color: #24292f;
}
.json-viewer .hljs-attr { color: #0550ae; }
.json-viewer .hljs-string { color: #0a3069; }
.json-viewer .hljs-number { color: #0550ae; }
.json-viewer .hljs-literal { color: #cf222e; }
.json-viewer .hljs-punctuation { color: #57606a; }
.json-viewer .hljs-comment { color: #6e7781; font-style: italic; }

View file

@ -0,0 +1,103 @@
.automation-node {
position: relative;
min-width: 230px;
max-width: 260px;
background: var(--card);
border: 2px solid var(--foreground);
border-radius: 14px;
box-shadow: 3px 3px 0 var(--foreground);
transition: transform 120ms ease, box-shadow 120ms ease;
}
.automation-node:hover {
transform: translate(-1px, -1px);
box-shadow: 4px 4px 0 var(--foreground);
}
.automation-node--wide {
min-width: 260px;
max-width: 260px;
}
.automation-node.is-selected {
transform: translate(-2px, -2px);
box-shadow: 5px 5px 0 #7c3aed;
}
.automation-node__header {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.875rem;
border-bottom: 2px solid var(--foreground);
border-top-left-radius: 12px;
border-top-right-radius: 12px;
}
.automation-node__icon-tile {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 2px solid var(--foreground);
border-radius: 8px;
flex-shrink: 0;
transform: rotate(-3deg);
}
.automation-node__icon-tile--violet { background: #ede9fe; color: #5b21b6; }
.automation-node__icon-tile--blue { background: #dbeafe; color: #1d4ed8; }
.automation-node__icon-tile--amber { background: #fef3c7; color: #92400e; }
.automation-node__icon-tile--rose { background: #ffe4e6; color: #be123c; }
.automation-node__icon-tile--emerald { background: #d1fae5; color: #047857; }
.automation-node__icon-tile--slate { background: #e2e8f0; color: #334155; }
.automation-node__icon-tile--zinc { background: #e4e4e7; color: #27272a; }
.automation-node__icon-tile--cyan { background: #cffafe; color: #155e75; }
.automation-node--accent-violet .automation-node__header { background: #f5f3ff; }
.automation-node--accent-blue .automation-node__header { background: #eff6ff; }
.automation-node--accent-amber .automation-node__header { background: #fffbeb; }
.automation-node--accent-rose .automation-node__header { background: #fff1f2; }
.automation-node--accent-emerald .automation-node__header { background: #ecfdf5; }
.automation-node--accent-slate .automation-node__header { background: #f1f5f9; }
.automation-node--accent-zinc .automation-node__header { background: #f4f4f5; }
.automation-node--accent-cyan .automation-node__header { background: #ecfeff; }
.automation-node__title {
min-width: 0;
font-weight: 700;
font-size: 0.875rem;
color: var(--foreground);
line-height: 1.2;
letter-spacing: -0.005em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.automation-node__summary {
padding: 0.625rem 0.875rem 0.75rem 0.875rem;
font-size: 0.75rem;
font-weight: 500;
color: color-mix(in srgb, var(--foreground) 70%, transparent);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
.automation-node__branches {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.375rem 0.875rem 0.625rem 0.875rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.automation-node__branch--yes { color: #047857; }
.automation-node__branch--no { color: #be123c; }

View file

@ -0,0 +1,13 @@
.json-viewer__body {
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
margin: 0;
background: #ffffff;
color: #24292f;
}
.json-viewer .hljs-attr { color: #0550ae; }
.json-viewer .hljs-string { color: #0a3069; }
.json-viewer .hljs-number { color: #0550ae; }
.json-viewer .hljs-literal { color: #cf222e; }
.json-viewer .hljs-punctuation { color: #57606a; }
.json-viewer .hljs-comment { color: #6e7781; font-style: italic; }

View file

@ -8,6 +8,7 @@ import { showRun as showRunRoute } from '@/actions/App/Http/Controllers/App/Auto
import JsonViewer from '@/components/JsonViewer.vue';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import { useAutomationEcho } from '@/composables/echo/useAutomationEcho';
import { test as testAutomation } from '@/routes/app/automations';
@ -152,10 +153,10 @@ const isZeroFetchResult = (nodeRun: NodeRun): boolean => {
</div>
<div class="flex items-center justify-between gap-3 rounded-xl border-2 border-foreground bg-card p-3 shadow-[3px_3px_0_var(--foreground)]">
<label class="flex cursor-pointer items-center gap-2 text-sm font-semibold text-foreground/70">
<Label class="cursor-pointer font-semibold text-foreground/70">
<Checkbox v-model="realData" :disabled="isStarting" />
{{ $t('automations.test.with_real_data') }}
</label>
</Label>
<Button size="sm" :disabled="isStarting || !!configIssue" @click="runTest">
<IconLoader2 v-if="isStarting" class="size-4 animate-spin" />
<IconPlayerPlay v-else class="size-4" />

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -36,13 +37,13 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.condition.field') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.condition.field') }}</Label>
<Input v-model="local.field" placeholder="{{ trigger.title }}" />
<InputError :message="errors?.field" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.condition.operator') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.condition.operator') }}</Label>
<Select v-model="local.operator">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
@ -61,7 +62,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.condition.value') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.condition.value') }}</Label>
<Input v-model="local.value" placeholder="keyword" />
<InputError :message="errors?.value" class="mt-1" />
</div>

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -34,13 +35,13 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.delay.duration') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.delay.duration') }}</Label>
<Input type="number" v-model.number="local.duration" placeholder="1" />
<InputError :message="errors?.duration" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.delay.unit') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.delay.unit') }}</Label>
<Select v-model="local.unit">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />

View file

@ -2,6 +2,7 @@
import { ref, watch } from 'vue';
import InputError from '@/components/InputError.vue';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
interface EndConfig {
@ -24,7 +25,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.end.reason') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.end.reason') }}</Label>
<Textarea v-model="local.reason" :placeholder="$t('automations.config.end.reason_placeholder')" :rows="3" />
<InputError :message="errors?.reason" class="mt-1" />
</div>

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
interface FetchRssConfig {
feed_url: string;
@ -24,7 +25,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.fetch_rss.feed_url') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.fetch_rss.feed_url') }}</Label>
<Input v-model="local.feed_url" placeholder="https://example.com/feed.xml" />
<InputError :message="errors?.feed_url" class="mt-1" />
<p class="mt-1 text-xs text-foreground/50">{{ $t('automations.config.fetch_rss.feed_url_hint') }}</p>

View file

@ -5,10 +5,10 @@ import { computed, ref, watch } from 'vue';
import ChannelConfigurator from '@/components/ChannelConfigurator.vue';
import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
import { useExpandedEditor } from '@/composables/useExpandedEditor';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { useExpandedEditor } from '@/composables/useExpandedEditor';
import { getMediaRulesForContentType } from '@/composables/useMediaRules';
import { getMediaIncompatibilityReason, getPlatformMetaIssue } from '@/composables/usePostCompliance';
import type { PinterestBoard } from '@/types';
@ -316,7 +316,7 @@ const channels = computed<Channel[]>(() =>
</div>
<div v-show="!editorExpanded">
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.generate.prompt_template') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.generate.prompt_template') }}</Label>
<div class="h-40">
<CodeEditor
v-model="local.prompt_template"

View file

@ -7,6 +7,7 @@ import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -115,7 +116,7 @@ const isBodyJsonInvalid = computed(() => {
<div class="space-y-4">
<div class="grid grid-cols-[110px_1fr] gap-2">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.method') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.method') }}</Label>
<Select v-model="local.method">
<SelectTrigger class="w-full">
<SelectValue />
@ -127,14 +128,14 @@ const isBodyJsonInvalid = computed(() => {
<InputError :message="errors?.method" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.url') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.url') }}</Label>
<Input v-model="local.url" placeholder="https://api.example.com/items" />
<InputError :message="errors?.url" class="mt-1" />
</div>
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.auth_type') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.auth_type') }}</Label>
<Select v-model="local.auth_type">
<SelectTrigger class="w-full">
<SelectValue />
@ -149,19 +150,19 @@ const isBodyJsonInvalid = computed(() => {
</div>
<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>
<Label class="mb-1 block">{{ $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 === AuthType.Basic">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.basic_username') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.basic_username') }}</Label>
<Input v-model="local.auth_username" autocomplete="off" />
<InputError :message="errors?.auth_username" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.basic_password') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.basic_password') }}</Label>
<Input v-model="local.auth_password" type="password" autocomplete="off" />
<InputError :message="errors?.auth_password" class="mt-1" />
</div>
@ -169,18 +170,18 @@ const isBodyJsonInvalid = computed(() => {
<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>
<Label class="mb-1 block">{{ $t('automations.config.http_request.api_key_header') }}</Label>
<Input v-model="local.auth_header_name" placeholder="X-API-Key" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.api_key_value') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.api_key_value') }}</Label>
<Input v-model="local.auth_token" type="password" autocomplete="off" />
<InputError :message="errors?.auth_token" class="mt-1" />
</div>
</template>
<div v-if="supportsBody" v-show="!editorExpanded">
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.body_template') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.body_template') }}</Label>
<div class="h-36">
<CodeEditor
v-model="local.body_template"
@ -197,7 +198,7 @@ const isBodyJsonInvalid = computed(() => {
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.headers') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.headers') }}</Label>
<div class="space-y-2">
<div v-for="(row, index) in headerRows" :key="index" class="flex items-center gap-2">
<Input
@ -237,19 +238,19 @@ const isBodyJsonInvalid = computed(() => {
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.items_path') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.items_path') }}</Label>
<Input v-model="local.items_path" placeholder="data.items" />
<p class="mt-1 text-xs text-foreground/50">{{ $t('automations.config.http_request.items_path_hint') }}</p>
<InputError :message="errors?.items_path" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.item_date_path') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.item_date_path') }}</Label>
<Input v-model="local.item_date_path" placeholder="published_at" />
<p class="mt-1 text-xs text-foreground/50">{{ $t('automations.config.http_request.item_date_path_hint') }}</p>
<InputError :message="errors?.item_date_path" class="mt-1" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.http_request.item_key_path') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.http_request.item_key_path') }}</Label>
<Input v-model="local.item_key_path" placeholder="id" />
<p class="mt-1 text-xs text-foreground/50">{{ $t('automations.config.http_request.item_key_path_hint') }}</p>
<InputError :message="errors?.item_key_path" class="mt-1" />

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -34,7 +35,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.publish.mode') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.publish.mode') }}</Label>
<Select v-model="local.mode">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
@ -49,7 +50,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</div>
<div v-if="local.mode === PublishMode.Scheduled">
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.publish.scheduled_offset') }}</label>
<Label class="mb-1 block">{{ $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" />
</div>

View file

@ -10,6 +10,7 @@ import {
} from '@/components/automations/schedule-summary';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -113,7 +114,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.type') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.type') }}</Label>
<Select v-model="local.trigger_type">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
@ -141,7 +142,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template v-if="local.trigger_type === TriggerType.Schedule">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.field') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.field') }}</Label>
<Select v-model="local.schedule_field">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
@ -157,17 +158,17 @@ watch(local, (val) => emit('update', val), { deep: true });
</div>
<div v-if="local.schedule_field === ScheduleField.Minutes">
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.minutes_interval') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.minutes_interval') }}</Label>
<Input type="number" v-model.number="local.schedule_minutes_interval" min="1" max="59" />
</div>
<template v-if="local.schedule_field === ScheduleField.Hours">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.hours_interval') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.hours_interval') }}</Label>
<Input type="number" v-model.number="local.schedule_hours_interval" min="1" max="23" />
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.minute') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.minute') }}</Label>
<Select v-model="scheduleMinuteStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -181,12 +182,12 @@ watch(local, (val) => emit('update', val), { deep: true });
<template v-if="local.schedule_field === ScheduleField.Days">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.days_interval') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.days_interval') }}</Label>
<Input type="number" v-model.number="local.schedule_days_interval" min="1" max="31" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.hour') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.hour') }}</Label>
<Select v-model="scheduleHourStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -197,7 +198,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</Select>
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.minute') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.minute') }}</Label>
<Select v-model="scheduleMinuteStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -212,7 +213,7 @@ watch(local, (val) => emit('update', val), { deep: true });
<template v-if="local.schedule_field === ScheduleField.Weeks">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.weekdays') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.weekdays') }}</Label>
<div class="flex flex-wrap gap-1.5">
<button
v-for="day in weekdays"
@ -230,7 +231,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.hour') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.hour') }}</Label>
<Select v-model="scheduleHourStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -241,7 +242,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</Select>
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.minute') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.minute') }}</Label>
<Select v-model="scheduleMinuteStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -256,12 +257,12 @@ watch(local, (val) => emit('update', val), { deep: true });
<template v-if="local.schedule_field === ScheduleField.Months">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.day_of_month') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.day_of_month') }}</Label>
<Input type="number" v-model.number="local.schedule_day_of_month" min="1" max="31" />
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.hour') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.hour') }}</Label>
<Select v-model="scheduleHourStr">
<SelectTrigger class="w-full">
<SelectValue />
@ -272,7 +273,7 @@ watch(local, (val) => emit('update', val), { deep: true });
</Select>
</div>
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.trigger.schedule.minute') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.trigger.schedule.minute') }}</Label>
<Select v-model="scheduleMinuteStr">
<SelectTrigger class="w-full">
<SelectValue />

View file

@ -5,6 +5,7 @@ import { isPayloadTemplateValid } from '@/components/automations/config-validati
import CodeEditor from '@/components/CodeEditor.vue';
import InputError from '@/components/InputError.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Select,
SelectContent,
@ -45,13 +46,13 @@ const isPayloadJsonInvalid = computed(() => !isPayloadTemplateValid(local.value.
<template>
<div class="space-y-3">
<div>
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.webhook.url') }}</label>
<Label class="mb-1 block">{{ $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>
<Label class="mb-1 block">{{ $t('automations.config.webhook.method') }}</Label>
<Select v-model="local.method">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('automations.config.select_placeholder')" />
@ -64,7 +65,7 @@ const isPayloadJsonInvalid = computed(() => !isPayloadTemplateValid(local.value.
</div>
<div v-show="!editorExpanded">
<label class="mb-1 block text-sm font-medium">{{ $t('automations.config.webhook.payload_template') }}</label>
<Label class="mb-1 block">{{ $t('automations.config.webhook.payload_template') }}</Label>
<div class="h-40">
<CodeEditor
v-model="local.payload_template"

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core';
import { IconCircleX } from '@tabler/icons-vue';
import { Handle, Position } from '@vue-flow/core';
import { computed } from 'vue';
const props = defineProps<{

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core';
import { IconRss } from '@tabler/icons-vue';
import { Handle, Position } from '@vue-flow/core';
import { computed } from 'vue';
const props = defineProps<{

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core';
import { IconSparkles } from '@tabler/icons-vue';
import { Handle, Position } from '@vue-flow/core';
import { computed } from 'vue';
const props = defineProps<{

View file

@ -1,6 +1,6 @@
<script setup lang="ts">
import { Handle, Position } from '@vue-flow/core';
import { IconBolt } from '@tabler/icons-vue';
import { Handle, Position } from '@vue-flow/core';
import { computed } from 'vue';
import { triggerSummary } from '@/components/automations/schedule-summary';