Skip to content
112 changes: 91 additions & 21 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"cron-parser": "^5.3.0",
"dayjs": "^1.11.13",
"drawflow": "^0.0.60",
"js-yaml": "^4.1.0",
"vue": "^2.6.14",
"vue-chartjs": "^4.0.0",
"vue-codemirror": "^4.0.6",
Expand Down
106 changes: 89 additions & 17 deletions web/src/components/EnvironmentForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<v-btn-toggle v-model="extraVarsEditMode" tile group>
<v-btn value="table" small class="mr-0" style="border-radius: 4px"> Table </v-btn>
<v-btn value="json" small class="mr-0" style="border-radius: 4px"> JSON </v-btn>
<v-btn value="yaml" small class="mr-0" style="border-radius: 4px"> YAML </v-btn>
</v-btn-toggle>

<v-btn icon @click="addExtraVar()" data-testid="varGroup-addVar">
Expand All @@ -97,6 +98,24 @@
style="position: absolute; right: 0; top: 0; margin: 10px"
/>
</div>
<div v-else-if="extraVarsEditMode === 'yaml'" style="position: relative">
<codemirror
:class="{
EnvironmentEditor: true,
}"
:style="{ border: '1px solid lightgray' }"
v-model="yaml"
:options="cmYamlOptions"
:placeholder="$t('enterExtraVariablesYaml')"
/>

<RichEditor
v-model="yaml"
type="yaml"
v-if="extraVarsEditMode === 'yaml'"
style="position: absolute; right: 0; top: 0; margin: 10px"
/>
</div>
<div v-else-if="extraVarsEditMode === 'table'">
<v-data-table
v-if="extraVars != null"
Expand Down Expand Up @@ -434,8 +453,10 @@
import ItemFormBase from '@/components/ItemFormBase';

import { codemirror } from 'vue-codemirror';
import { load as loadYaml, dump as dumpYaml } from 'js-yaml';
import 'codemirror/lib/codemirror.css';
import 'codemirror/mode/vue/vue.js';
import 'codemirror/mode/yaml/yaml.js';
import 'codemirror/addon/display/placeholder.js';
import { getErrorMessage } from '@/lib/error';
import RichEditor from '@/components/RichEditor.vue';
Expand Down Expand Up @@ -465,45 +486,79 @@ export default {
},

watch: {
extraVarsEditMode(val) {
switch (val) {
// Handles Table/JSON/YAML toggling. The mode being left determines which
// field is authoritative (extraVars for table, json for JSON, yaml for YAML);
// it's parsed into a plain object which is then rendered into the mode being
// entered.
extraVarsEditMode(val, oldVal) {
let source;
switch (oldVal) {
case 'json': {
if (this.extraVars == null) {
try {
source = JSON.parse(this.json);
this.formError = null;
} catch (err) {
this.formError = getErrorMessage(err);
if (val === 'table') {
this.extraVars = null;
}
return;
}

// Serialize leniently: a row whose list/dict value is not valid JSON yet
// keeps its raw text (as a string) instead of throwing. This prevents the
// toggle from blanking the JSON editor or dropping rows while the user is
// still typing. Strict validation happens on save (see beforeSave).
this.json = JSON.stringify(this.extraVarsToObjectLenient(this.extraVars), null, 2);
this.formError = null;
break;
}
case 'table': {
let parsed;
case 'yaml': {
try {
parsed = JSON.parse(this.json);
// Only an empty document (loadYaml returns undefined) defaults to
// {}. Valid falsy YAML values (false, 0, null) must be preserved
// as-is rather than silently coerced.
const loaded = loadYaml(this.yaml);
source = loaded === undefined ? {} : loaded;
this.formError = null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (err) {
this.formError = getErrorMessage(err);
this.extraVars = null;
if (val === 'table') {
this.extraVars = null;
}
return;
}
break;
}
default: {
// Coming from the table (or initial load): extraVars is authoritative.
// Serialize leniently: a row whose list/dict value is not valid JSON yet
// keeps its raw text (as a string) instead of throwing. This prevents the
// toggle from blanking the target editor or dropping rows while the user
// is still typing. Strict validation happens on save (see beforeSave).
if (this.extraVars == null) {
return;
}
source = this.extraVarsToObjectLenient(this.extraVars);
}
}

// If the JSON text still matches what the current table represents, the
switch (val) {
case 'json':
this.json = JSON.stringify(source, null, 2);
break;
case 'yaml': {
const dumped = dumpYaml(source);
this.yaml = dumped === '{}\n' ? '' : dumped;
break;
}
case 'table': {
// If the source still matches what the current table represents, the
// user only switched tabs without editing it — keep the existing rows so
// their chosen types (e.g. Dict) and in-progress values are preserved
// instead of being re-inferred (and possibly downgraded to String).
if (
this.extraVars != null
&& JSON.stringify(parsed)
&& JSON.stringify(source)
=== JSON.stringify(this.extraVarsToObjectLenient(this.extraVars))
) {
return;
}

this.extraVars = this.objectToExtraVars(parsed);
this.extraVars = this.objectToExtraVars(source);
break;
}
default:
Expand Down Expand Up @@ -538,6 +593,7 @@ export default {
],

json: '{}',
yaml: '',
extraVars: [],
env: [],
secrets: [],
Expand All @@ -553,6 +609,14 @@ export default {
indentWithTabs: false,
},

cmYamlOptions: {
tabSize: 2,
mode: 'text/x-yaml',
lineNumbers: true,
line: true,
indentWithTabs: false,
},

extraVarsEditMode: 'json',

extraVarTypes: [
Expand Down Expand Up @@ -753,6 +817,14 @@ export default {
case 'json':
this.item.json = this.json;
break;
case 'yaml':
try {
const loaded = loadYaml(this.yaml);
this.item.json = JSON.stringify(loaded === undefined ? {} : loaded);
} catch (err) {
throw new Error(`Extra variables: ${getErrorMessage(err)}`);
}
break;
case 'table':
if (this.extraVars == null) {
this.item.json = this.json;
Expand Down
Loading
Loading