Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3b01a08
Improve workflow dependency resolution error surfacing
Toocky Apr 11, 2026
19c7a63
update gitignore
Toocky Apr 11, 2026
c12e6cf
Extend workflow error step with structured title/body/severity
Toocky Apr 11, 2026
371528b
Add title/body/severity fields to workflow error step configurator
Toocky Apr 11, 2026
449ae12
Surface structured workflow error step output in shared toast mixin
Toocky Apr 11, 2026
0e23d13
Add parameter context to workflow step verification errors
Toocky Apr 11, 2026
ae446b1
Handle workflow save failures gracefully
Toocky Apr 11, 2026
f65bab1
Address branch review feedback
Toocky Apr 11, 2026
0d7f12f
Fix error handler vars disappearing from scope after first normal step
Toocky Apr 11, 2026
d6f214b
Collapse error step body argument into message
Toocky Apr 11, 2026
14e3e59
Fix iterator inside parallel gateway circular dependency
Toocky Apr 11, 2026
a95202e
Merge all parallel branch scopes at join gateway
Toocky Apr 11, 2026
cd8e323
Surface join gateway variable conflicts as runtime warnings
Toocky Apr 11, 2026
8e592d3
Add static analyzer for parallel-join variable conflicts
Toocky Apr 11, 2026
ef8a991
Render workflow warnings in editor alongside issues
Toocky Apr 11, 2026
14090a7
Suppress misleading no-dependency errors from stall reporter
Toocky Apr 11, 2026
0bb6964
Address review: correctness fixes for warnings + verifier
Toocky Apr 12, 2026
1694079
Address review: latent bug hardening
Toocky Apr 12, 2026
1e36f68
Address review: lock masking matrix + document intent
Toocky Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
*.sw*
.run
node_modules
ignore
ignore
.kilo*
.claude*
4 changes: 4 additions & 0 deletions client/web/workflow/public/icons/warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
211 changes: 185 additions & 26 deletions client/web/workflow/src/components/Configurator/Error.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,69 @@
<b-card-body
class="p-0"
>
<!-- Title -->
<b-form-group
:label="$t('general:error-expression')"
:label="$t('general:error-step.title.label')"
:description="$t('general:error-step.title.description')"
label-class="text-primary"
>
<expression-editor
v-model="titleArg.expr"
font-size="16px"
show-line-numbers
@open="openInEditor('title')"
@input="onFieldInput"
/>
</b-form-group>

<!-- Severity -->
<b-form-group
:label="$t('general:error-step.severity.label')"
:description="$t('general:error-step.severity.description')"
label-class="text-primary"
>
<b-form-select
v-if="severityIsLiteral"
v-model="severityValue"
:options="severityOptions"
@change="onSeverityChange"
/>
<div
v-else
class="d-flex align-items-center"
>
<b-form-input
v-model="severityArg.expr"
class="flex-grow-1"
:placeholder="$t('general:error-step.severity.label')"
@input="onFieldInput"
/>
<b-button
v-b-tooltip.hover
variant="link"
size="sm"
:title="$t('general:error-step.severity.reset-to-literal')"
class="ml-2 p-0"
@click="resetSeverityToLiteral"
>
<font-awesome-icon :icon="['fas', 'undo']" />
</b-button>
</div>
</b-form-group>

<!-- Message -->
<b-form-group
:label="$t('general:error-step.message.label')"
:description="$t('general:error-step.message.description')"
label-class="text-primary"
class="mb-0"
>
<expression-editor
v-model="item.config.arguments[0].expr"
font-size="18px"
v-model="messageArg.expr"
font-size="16px"
show-line-numbers
@open="openInEditor"
@input="valueChanged"
@open="openInEditor('message')"
@input="onFieldInput"
/>
</b-form-group>
</b-card-body>
Expand Down Expand Up @@ -60,6 +112,13 @@
import base from './base'
import ExpressionEditor from '../ExpressionEditor.vue'

const ALLOWED_TARGETS = ['message', 'title', 'severity']
const SEVERITY_VALUES = ['error', 'warning', 'info']

function makeArg (target, expr = '') {
return { target, type: 'String', expr }
}

export default {
components: {
ExpressionEditor,
Expand All @@ -71,53 +130,153 @@ export default {
return {
expressionEditor: {
currentExpression: undefined,
currentField: undefined,
},
}
},

computed: {
// Computed getters are pure reads. All four allowed-target args
// are pre-created in `created()` and also re-created by
// `ensureArgs()` before any method that needs write access. This
// keeps the computeds side-effect-free so Vue's reactivity graph
// can't re-enter the getter while it is mutating arguments.
messageArg () {
return this.findArg('message') || makeArg('message')
},
titleArg () {
return this.findArg('title') || makeArg('title')
},
severityArg () {
return this.findArg('severity') || makeArg('severity')
},

// severityIsLiteral: true when the severity expression is either
// empty or a plain quoted literal we can round-trip through the
// dropdown. When it's a real expression (e.g. `vars.level`), the
// configurator falls back to a raw text input so we never clobber
// the author's work.
severityIsLiteral () {
const raw = (this.severityArg.expr || '').trim()
if (!raw) return true
return /^["'](error|warning|info)["']$/.test(raw)
},

severityValue: {
get () {
const raw = (this.severityArg.expr || '').trim()
if (!raw) return 'error'
const m = raw.match(/^["'](error|warning|info)["']$/)
return m ? m[1] : 'error'
},
set (v) {
if (!SEVERITY_VALUES.includes(v)) v = 'error'
const arg = this.severityArg
this.$set(arg, 'expr', `"${v}"`)
},
},

severityOptions () {
return [
{ value: 'error', text: this.$t('general:error-step.severity.options.error') },
{ value: 'warning', text: this.$t('general:error-step.severity.options.warning') },
{ value: 'info', text: this.$t('general:error-step.severity.options.info') },
]
},
},

created () {
let args = [{
target: 'message',
type: 'String',
expr: '',
}]

if (this.item.config.arguments && this.item.config.arguments.length) {
args = this.item.config.arguments.map(({ target, type, value, expr }) => {
return {
this.ensureArgs()
},

methods: {
// ensureArgs normalises item.config.arguments: keeps only allowed
// targets and guarantees one entry per target, even if empty. Safe
// to call from any write path. Does NOT run from computed getters.
ensureArgs () {
const existing = Array.isArray(this.item.config.arguments) ? this.item.config.arguments : []
const byTarget = {}
existing.forEach(({ target, type, value, expr }) => {
if (!ALLOWED_TARGETS.includes(target)) return
byTarget[target] = {
target,
type,
type: type || 'String',
expr: expr || (value ? `"${value}"` : ''),
}
})
}

this.$set(this.item.config, 'arguments', args)
},
const args = ALLOWED_TARGETS.map(t => byTarget[t] || makeArg(t))
this.$set(this.item.config, 'arguments', args)
},

methods: {
valueChanged (value) {
// findArg is a pure read. Returns the matching arg object or
// undefined when absent. Safe to call from computeds.
findArg (target) {
const args = this.item.config.arguments || []
return args.find(a => a.target === target)
},

// findOrCreateArg is a mutating read used by write paths only.
// It guarantees the arg exists on item.config.arguments before
// returning it. Never call from inside a computed getter — use
// findArg with a fallback instead.
findOrCreateArg (target) {
let arg = this.findArg(target)
if (!arg) {
this.ensureArgs()
arg = this.findArg(target) || makeArg(target)
}
return arg
},

// stripLiteralQuotes returns the unquoted content of a simple
// quoted string expression (e.g. "foo" -> foo) so the canvas node
// label preview reads cleanly. Non-literal expressions are passed
// through unchanged.
stripLiteralQuotes (s) {
const t = (s || '').trim()
const m = t.match(/^(["'])((?:[^\\]|\\.)*)\1$/)
return m ? m[2] : t
},

onFieldInput () {
const pick = this.titleArg.expr || this.messageArg.expr || ''
const preview = this.stripLiteralQuotes(pick)
this.$emit('update-default-value', {
value: `Stop workflow with error: ${value}`,
value: preview ? `Stop workflow with error: ${preview}` : 'Stop workflow with error',
force: !this.item.node.value,
})
this.$root.$emit('change-detected')
},

openInEditor () {
this.expressionEditor.currentExpression = this.item.config.arguments[0].expr
onSeverityChange () {
this.$root.$emit('change-detected')
},

saveExpression () {
const { currentExpression } = this.expressionEditor
this.$set(this.item.config.arguments[0], 'expr', currentExpression)
resetSeverityToLiteral () {
this.$set(this.severityArg, 'expr', '"error"')
this.$root.$emit('change-detected')
},

openInEditor (field) {
const arg = this.findOrCreateArg(field)
this.expressionEditor.currentField = field
this.expressionEditor.currentExpression = arg.expr || ''
},

saveExpression () {
const { currentExpression, currentField } = this.expressionEditor
if (currentField) {
const arg = this.findOrCreateArg(currentField)
this.$set(arg, 'expr', currentExpression)
this.$root.$emit('change-detected')
}
this.resetExpression()
},

resetExpression () {
this.expressionEditor.currentExpression = undefined
this.expressionEditor.currentField = undefined
},
},
}
Expand Down
Loading