mirror of
https://github.com/kestra-io/kestra.git
synced 2025-12-19 18:05:41 -05:00
fix(nocode): KeyValue Pairs have a bug (#10998)
This commit is contained in:
committed by
GitHub
parent
d92cc099c7
commit
8acbc8ba03
@@ -89,6 +89,7 @@ export default [
|
||||
{
|
||||
// Enforce the use of the <script setup> block in components within these paths
|
||||
files: [components("filter"), components("code")],
|
||||
ignores: [components("code/components/tasks")],
|
||||
rules: {"vue/component-api-style": ["error", ["script-setup"]]},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,25 +6,25 @@
|
||||
/>
|
||||
|
||||
<el-form v-else label-position="top">
|
||||
<TaskWrapper :key="v.fieldKey" v-for="(v) in fieldsFromSchemaTop" :merge="shouldMerge(v.schema)" :transparent="v.fieldKey === 'inputs'">
|
||||
<Wrapper :key="v.fieldKey" v-for="(v) in fieldsFromSchemaTop" :merge="shouldMerge(v.schema)" :transparent="v.fieldKey === 'inputs'">
|
||||
<template #tasks>
|
||||
<TaskObjectField
|
||||
v-bind="v"
|
||||
@update:model-value="(val) => onTaskUpdateField(v.fieldKey, val)"
|
||||
/>
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</Wrapper>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<TaskWrapper :key="v.fieldKey" v-for="(v) in fieldsFromSchemaRest" :merge="shouldMerge(v.schema)" :transparent="SECTIONS_IDS.includes(v.fieldKey)">
|
||||
<Wrapper :key="v.fieldKey" v-for="(v) in fieldsFromSchemaRest" :merge="shouldMerge(v.schema)" :transparent="SECTIONS_IDS.includes(v.fieldKey)">
|
||||
<template #tasks>
|
||||
<TaskObjectField
|
||||
v-bind="v"
|
||||
@update:model-value="(val) => onTaskUpdateField(v.fieldKey, val)"
|
||||
/>
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</Wrapper>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,8 +37,8 @@
|
||||
import {removeNullAndUndefined} from "./utils/cleanUp";
|
||||
|
||||
import Task from "./segments/Task.vue";
|
||||
import TaskWrapper from "../flows/tasks/TaskWrapper.vue";
|
||||
import TaskObjectField from "../flows/tasks/TaskObjectField.vue";
|
||||
import Wrapper from "./components/tasks/Wrapper.vue";
|
||||
import TaskObjectField from "./components/tasks/TaskObjectField.vue";
|
||||
import {
|
||||
BLOCK_SCHEMA_PATH_INJECTION_KEY,
|
||||
CLOSE_TASK_FUNCTION_INJECTION_KEY,
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="16" class="d-flex">
|
||||
<slot name="value-field" :value="pair[1]" :key="pair[0]">
|
||||
<slot name="value-field" :value="pair[1]" :key="pair[0]" :index="index" :update-value="updateValue">
|
||||
<InputText
|
||||
:model-value="pair[1]"
|
||||
:placeholder="t('value')"
|
||||
@@ -109,15 +109,19 @@
|
||||
|
||||
function updateModel() {
|
||||
localEdit.value = true;
|
||||
emit("update:modelValue", Object.fromEntries(internalPairs.value.filter(pair => pair[0] !== "" && pair[1] !== undefined)));
|
||||
const newVal = Object.fromEntries(internalPairs.value.filter(pair => pair[0] !== "" && pair[1] !== undefined))
|
||||
|
||||
emit("update:modelValue", newVal);
|
||||
}
|
||||
|
||||
function handleKeyInput(index: number, newValue: string) {
|
||||
|
||||
internalPairs.value[index][0] = newValue.toString();
|
||||
updateModel()
|
||||
};
|
||||
|
||||
function addPair() {
|
||||
|
||||
internalPairs.value.push(["", undefined])
|
||||
updateModel()
|
||||
};
|
||||
@@ -128,6 +132,7 @@
|
||||
};
|
||||
|
||||
function updateValue (pairId: number, newValue: string){
|
||||
|
||||
internalPairs.value[pairId][1] = newValue;
|
||||
updateModel()
|
||||
};
|
||||
|
||||
@@ -40,9 +40,6 @@ export default defineComponent({
|
||||
isRequired(key: string) {
|
||||
return this.schema?.required?.includes(key);
|
||||
},
|
||||
onShow() {
|
||||
},
|
||||
|
||||
onInput(value:any) {
|
||||
this.$emit("update:modelValue", collapseEmptyValues(value));
|
||||
}
|
||||
@@ -63,7 +60,7 @@ export default defineComponent({
|
||||
return YAML_UTILS.stringify(this.values);
|
||||
},
|
||||
info() {
|
||||
return `${this.schema?.title || this.schema?.type}`
|
||||
return this.schema?.title ?? this.schema?.type
|
||||
},
|
||||
isValid() {
|
||||
return true;
|
||||
@@ -37,7 +37,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
import {TaskIcon} from "@kestra-io/ui-libs";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
@@ -16,7 +16,7 @@
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="items.length > 1 ? 20 : 22" class="pe-2">
|
||||
<TaskWrapper :merge="!needWrapper">
|
||||
<Wrapper :merge="!needWrapper">
|
||||
<template #tasks>
|
||||
<component
|
||||
:key="'array-' + index"
|
||||
@@ -30,7 +30,7 @@
|
||||
@update:model-value="handleInput($event, index)"
|
||||
/>
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</Wrapper>
|
||||
</el-col>
|
||||
<el-col :span="2" class="d-flex align-items-center justify-content-center delete">
|
||||
<DeleteOutline @click="removeItem(index)" />
|
||||
@@ -42,12 +42,12 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, provide, ref} from "vue";
|
||||
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../code/utils/icons";
|
||||
import {DeleteOutline, ChevronUp, ChevronDown} from "../../utils/icons";
|
||||
|
||||
import Add from "../../code/components/Add.vue";
|
||||
import Add from "../Add.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import TaskWrapper from "./TaskWrapper.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../code/injectionKeys";
|
||||
import Wrapper from "./Wrapper.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
|
||||
defineOptions({inheritAttrs: false});
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../code/styles/code.scss";
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
@@ -53,10 +53,10 @@
|
||||
<script setup>
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import Help from "vue-material-design-icons/HelpBox.vue";
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import Markdown from "../../../layout/Markdown.vue";
|
||||
</script>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
|
||||
export default {
|
||||
name: "TaskBasic",
|
||||
@@ -147,7 +147,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../code/styles/code.scss";
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.type-tag {
|
||||
background-color: var(--ks-tag-background);
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
@@ -39,11 +39,11 @@
|
||||
<script lang="ts" setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import {DeleteOutline} from "../../code/utils/icons";
|
||||
import {DeleteOutline} from "../../utils/icons";
|
||||
|
||||
import InputText from "../../code/components/inputs/InputText.vue";
|
||||
import InputText from "../inputs/InputText.vue";
|
||||
import TaskExpression from "./TaskExpression.vue";
|
||||
import Add from "../../code/components/Add.vue";
|
||||
import Add from "../Add.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
import debounce from "lodash/debounce";
|
||||
|
||||
@@ -153,5 +153,5 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../code/styles/code.scss";
|
||||
@import "../../styles/code.scss";
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
</el-select>
|
||||
</template>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
export default {
|
||||
mixins: [Task],
|
||||
};
|
||||
@@ -11,8 +11,8 @@
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {collapseEmptyValues} from "./Task";
|
||||
import Editor from "../../../components/inputs/Editor.vue";
|
||||
import {collapseEmptyValues} from "./MixinTask";
|
||||
import Editor from "../../../../components/inputs/Editor.vue";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
import {computed, ref} from "vue";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<InputPair v-model="protectedModel">
|
||||
<template #value-field="{value, key}">
|
||||
<template #value-field="{value, updateValue, index}">
|
||||
<TaskString
|
||||
v-bind="$attrs"
|
||||
:model-value="value"
|
||||
@update:model-value="(changed: any) => updateValue(key, changed)"
|
||||
@update:model-value="(changed: any) => updateValue(index, changed)"
|
||||
/>
|
||||
</template>
|
||||
</InputPair>
|
||||
@@ -12,14 +12,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from "vue";
|
||||
import {PairField} from "../../code/utils/types";
|
||||
import InputPair from "../../code/components/inputs/InputPair.vue";
|
||||
import {PairField} from "../../utils/types";
|
||||
import InputPair from "../inputs/InputPair.vue";
|
||||
// @ts-expect-error no typings for taskString yet
|
||||
import TaskString from "./TaskString.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: PairField["value"] | string): void;
|
||||
}>();
|
||||
|
||||
const model = defineModel<PairField["value"] | string>();
|
||||
|
||||
const protectedModel = computed({
|
||||
@@ -30,13 +27,4 @@
|
||||
model.value = value
|
||||
}
|
||||
})
|
||||
|
||||
function updateValue(key: string, changed: string){
|
||||
if(!model.value || typeof model.value === "string"){
|
||||
return
|
||||
}
|
||||
|
||||
model.value[key] = changed
|
||||
emit("update:modelValue", model.value);
|
||||
}
|
||||
</script>
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from "vue";
|
||||
import Collapse from "../../code/components/collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../code/injectionKeys";
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import Collapse from "../collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
|
||||
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref(""))
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../code/styles/code.scss";
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.tasks-wrapper {
|
||||
width: 100%;
|
||||
@@ -9,10 +9,10 @@
|
||||
</template>
|
||||
<script>
|
||||
import {mapStores} from "pinia";
|
||||
import Task from "./Task";
|
||||
import NamespaceSelect from "../../namespaces/components/NamespaceSelect.vue";
|
||||
import Task from "./MixinTask";
|
||||
import NamespaceSelect from "../../../namespaces/components/NamespaceSelect.vue";
|
||||
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
export default {
|
||||
components: {NamespaceSelect},
|
||||
mixins: [Task],
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Task from "./Task"
|
||||
import Task from "./MixinTask"
|
||||
export default {
|
||||
mixins: [Task],
|
||||
computed: {
|
||||
255
ui/src/components/code/components/tasks/TaskObject.vue
Normal file
255
ui/src/components/code/components/tasks/TaskObject.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
|
||||
<template>
|
||||
<el-form label-position="top" class="w-100">
|
||||
<template v-if="sortedProperties">
|
||||
<template v-for="[fieldKey, fieldSchema] in protectedRequiredProperties" :key="fieldKey">
|
||||
<Wrapper :merge>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</Wrapper>
|
||||
</template>
|
||||
|
||||
<el-collapse v-model="activeNames" v-if="requiredProperties.length && (optionalProperties?.length || deprecatedProperties?.length || connectionProperties?.length)" class="collapse">
|
||||
<el-collapse-item name="connection" v-if="connectionProperties?.length" :title="t('no_code.sections.connection')">
|
||||
<template v-for="[fieldKey, fieldSchema] in connectionProperties" :key="fieldKey">
|
||||
<Wrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</Wrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item name="optional" v-if="optionalProperties?.length" :title="t('no_code.sections.optional')">
|
||||
<template v-for="[fieldKey, fieldSchema] in optionalProperties" :key="fieldKey">
|
||||
<Wrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</Wrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item name="deprecated" v-if="deprecatedProperties?.length" :title="t('no_code.sections.deprecated')">
|
||||
<template v-for="[fieldKey, fieldSchema] in deprecatedProperties" :key="fieldKey">
|
||||
<Wrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</Wrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<template v-else-if="typeof modelValue === 'object' && modelValue !== null && !Array.isArray(modelValue)">
|
||||
<task-dict
|
||||
:model-value="modelValue"
|
||||
:task="task"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:modelValue', value)
|
||||
"
|
||||
:root="root"
|
||||
:schema="schema ?? {}"
|
||||
:required="required"
|
||||
:definitions="definitions"
|
||||
/>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import TaskDict from "./TaskDict.vue";
|
||||
import Wrapper from "./Wrapper.vue";
|
||||
import TaskObjectField from "./TaskObjectField.vue";
|
||||
import {collapseEmptyValues} from "./MixinTask";
|
||||
|
||||
defineOptions({
|
||||
name: "TaskObject",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
type Model = Record<string, any> | undefined;
|
||||
type Schema = { required?: string[]; [k: string]: any } | undefined;
|
||||
|
||||
const props = defineProps<{
|
||||
merge?: boolean;
|
||||
properties?: any;
|
||||
metadataInputs?: boolean;
|
||||
modelValue?: Model;
|
||||
required?: boolean;
|
||||
schema?: Schema;
|
||||
definitions?: any;
|
||||
// passed-through by parent in some contexts
|
||||
task?: any;
|
||||
root?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: Model): void;
|
||||
}>();
|
||||
|
||||
const {t} = useI18n();
|
||||
|
||||
const activeNames = ref<string[]>([]);
|
||||
|
||||
const FIRST_FIELDS = ["id", "forced", "on", "type"] as const;
|
||||
|
||||
type Entry = [string, any];
|
||||
|
||||
function sortProperties(properties: Entry[], required?: string[]): Entry[] {
|
||||
if (!properties?.length) return [];
|
||||
return properties.slice().sort((a, b) => {
|
||||
if (FIRST_FIELDS.includes(a[0] as any)) return -1;
|
||||
if (FIRST_FIELDS.includes(b[0] as any)) return 1;
|
||||
|
||||
const aRequired = (required || []).includes(a[0]);
|
||||
const bRequired = (required || []).includes(b[0]);
|
||||
|
||||
if (aRequired && !bRequired) return -1;
|
||||
if (!aRequired && bRequired) return 1;
|
||||
|
||||
const aDefault = "default" in a[1];
|
||||
const bDefault = "default" in b[1];
|
||||
|
||||
if (aDefault && !bDefault) return 1;
|
||||
if (!aDefault && bDefault) return -1;
|
||||
|
||||
return a[0].localeCompare(b[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function isDeprecated(value: any) {
|
||||
if(value?.allOf){
|
||||
return value.allOf.some(isDeprecated);
|
||||
}
|
||||
return value?.$deprecated;
|
||||
}
|
||||
|
||||
const filteredProperties = computed<Entry[]>(() => {
|
||||
const propertiesProc = (props.properties ?? props.schema?.properties);
|
||||
return propertiesProc
|
||||
? (Object.entries(propertiesProc) as Entry[]).filter(([key, value]) => key !== "type" && !Array.isArray(value))
|
||||
: [];
|
||||
});
|
||||
|
||||
const sortedProperties = computed<Entry[]>(() => sortProperties(filteredProperties.value, props.schema?.required));
|
||||
|
||||
const isRequired = (key: string) => Boolean(props.schema?.required?.includes(key));
|
||||
|
||||
const requiredProperties = computed<Entry[]>(() => {
|
||||
return props.merge ? sortedProperties.value : sortedProperties.value.filter(([p, v]) => v && isRequired(p));
|
||||
});
|
||||
|
||||
const protectedRequiredProperties = computed<Entry[]>(() => {
|
||||
return requiredProperties.value.length ? requiredProperties.value : sortedProperties.value;
|
||||
});
|
||||
|
||||
const optionalProperties = computed<Entry[]>(() => {
|
||||
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && !isRequired(p) && !isDeprecated(v) && v.$group !== "connection");
|
||||
});
|
||||
|
||||
const deprecatedProperties = computed<Entry[]>(() => {
|
||||
const obj = (typeof props.modelValue === "object" && props.modelValue !== null) ? (props.modelValue as Record<string, any>) : {};
|
||||
return props.merge ? [] : sortedProperties.value.filter(([k, v]) => v && isDeprecated(v) && obj[k] !== undefined);
|
||||
});
|
||||
|
||||
const connectionProperties = computed<Entry[]>(() => {
|
||||
return props.merge ? [] : sortedProperties.value.filter(([p, v]) => v && v.$group === "connection" && !isRequired(p));
|
||||
});
|
||||
|
||||
function onInput(value: any) {
|
||||
emit("update:modelValue", collapseEmptyValues(value));
|
||||
}
|
||||
|
||||
function onObjectInput(propertyName: string, value: any) {
|
||||
const currentValue = (typeof props.modelValue === "object" && props.modelValue !== null ? {...(props.modelValue as Record<string, any>)} : {});
|
||||
currentValue[propertyName] = value;
|
||||
onInput(currentValue);
|
||||
}
|
||||
|
||||
function fieldProps(key: string, schema: any) {
|
||||
const mv = (typeof props.modelValue === "object" && props.modelValue !== null) ? (props.modelValue as Record<string, any>)[key] : undefined;
|
||||
return {
|
||||
modelValue: mv,
|
||||
"onUpdate:modelValue": (value: any) => onObjectInput(key, value),
|
||||
root: props.root,
|
||||
fieldKey: key,
|
||||
task: props.modelValue,
|
||||
schema: schema,
|
||||
definitions: props.definitions,
|
||||
required: props.schema?.required,
|
||||
} as const;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-form-item__content {
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper.singleton-tooltip {
|
||||
max-width: 300px !important;
|
||||
background: var(--ks-tooltip-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
> :deep(.el-form-item__label) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
|
||||
.inline-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--ks-content-primary);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
background-color: var(--ks-tag-background-active);
|
||||
color: var(--ks-tag-content);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 2px;
|
||||
border-radius: 8px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.information-icon {
|
||||
color: var(--ks-content-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -64,10 +64,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref} from "vue";
|
||||
import {templateRef} from "@vueuse/core";
|
||||
import {computed, ref, useTemplateRef} from "vue";
|
||||
import Help from "vue-material-design-icons/Information.vue";
|
||||
import Markdown from "../../layout/Markdown.vue";
|
||||
import Markdown from "../../../layout/Markdown.vue";
|
||||
import TaskLabelWithBoolean from "./TaskLabelWithBoolean.vue";
|
||||
import ClearButton from "./ClearButton.vue";
|
||||
import getTaskComponent from "./getTaskComponent";
|
||||
@@ -87,7 +86,7 @@
|
||||
(e: "update:modelValue", value?: Record<string, any> | string | number | boolean | Array<any>): void;
|
||||
}>();
|
||||
|
||||
const taskComponent = templateRef<{resetSelectType?: () => void}>("taskComponent");
|
||||
const taskComponent = useTemplateRef<{resetSelectType?: () => void}>("taskComponent");
|
||||
|
||||
const isRequired = computed(() => {
|
||||
return !props.disabled && props.required?.includes(props.fieldKey);// && props.schema.$required;
|
||||
@@ -45,12 +45,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import Editor from "../../../components/inputs/Editor.vue";
|
||||
import InputText from "../../code/components/inputs/InputText.vue";
|
||||
import Editor from "../../../../components/inputs/Editor.vue";
|
||||
import InputText from "../inputs/InputText.vue";
|
||||
import IconCodeBracesBox from "vue-material-design-icons/CodeBracesBox.vue";
|
||||
</script>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
@@ -19,8 +19,8 @@
|
||||
</template>
|
||||
<script>
|
||||
import {mapStores} from "pinia";
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import Task from "./Task";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
import Task from "./MixinTask";
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
@@ -38,12 +38,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
import Task from "./MixinTask";
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
import Minus from "vue-material-design-icons/Minus.vue";
|
||||
import TaskExpression from "./TaskExpression.vue";
|
||||
import {mapStores} from "pinia";
|
||||
import {useCoreStore} from "../../../stores/core";
|
||||
import {useCoreStore} from "../../../../stores/core";
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
@@ -21,8 +21,8 @@
|
||||
REF_PATH_INJECTION_KEY,
|
||||
CREATING_TASK_INJECTION_KEY,
|
||||
BLOCK_SCHEMA_PATH_INJECTION_KEY
|
||||
} from "../../code/injectionKeys";
|
||||
import Element from "../../code/components/collapse/Element.vue";
|
||||
} from "../../injectionKeys";
|
||||
import Element from "../collapse/Element.vue";
|
||||
|
||||
const model = defineModel({
|
||||
type: Object,
|
||||
@@ -13,9 +13,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, inject, ref} from "vue";
|
||||
import Collapse from "../../code/components/collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../code/injectionKeys";
|
||||
import {useFlowStore} from "../../../stores/flow";
|
||||
import Collapse from "../collapse/Collapse.vue";
|
||||
import {BLOCK_SCHEMA_PATH_INJECTION_KEY} from "../../injectionKeys";
|
||||
import {useFlowStore} from "../../../../stores/flow";
|
||||
|
||||
const blockSchemaPath = inject(BLOCK_SCHEMA_PATH_INJECTION_KEY, ref())
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../code/styles/code.scss";
|
||||
@import "../../styles/code.scss";
|
||||
|
||||
.tasks-wrapper {
|
||||
width: 100%;
|
||||
@@ -10,7 +10,7 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, getCurrentInstance} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import InputText from "../../code/components/inputs/InputText.vue";
|
||||
import InputText from "../inputs/InputText.vue";
|
||||
|
||||
const {t} = useI18n()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({name: "TaskWrapper"});
|
||||
defineOptions({name: "Wrapper"});
|
||||
|
||||
defineProps<{merge?: boolean, transparent?: boolean}>();
|
||||
</script>
|
||||
@@ -12,10 +12,6 @@ function getType(property: any, key?: string, schema?: any): string {
|
||||
return "task"
|
||||
}
|
||||
|
||||
if (property.$ref.includes(".conditions.")) {
|
||||
return "condition"
|
||||
}
|
||||
|
||||
if (property.$ref.includes("tasks.runners.TaskRunner")) {
|
||||
return "task-runner"
|
||||
}
|
||||
@@ -113,12 +113,6 @@
|
||||
<template #label>
|
||||
<code>{{ $t("concurrency") }}</code>
|
||||
<br>
|
||||
<task-basic
|
||||
:schema="concurrencySchema"
|
||||
v-model="newMetadata.concurrency"
|
||||
root="concurrency"
|
||||
v-if="showConcurrency"
|
||||
/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
@@ -132,8 +126,6 @@
|
||||
</el-form>
|
||||
</template>
|
||||
<script setup>
|
||||
import TaskBasic from "./tasks/TaskBasic.vue";
|
||||
|
||||
import Pencil from "vue-material-design-icons/Pencil.vue";
|
||||
import Eye from "vue-material-design-icons/Eye.vue";
|
||||
import Plus from "vue-material-design-icons/Plus.vue";
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
import {computed, inject, onActivated, provide, ref, toRaw, watch} from "vue";
|
||||
import {useI18n} from "vue-i18n";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
// @ts-expect-error TaskObject can't be typed for now because of time constraints
|
||||
import TaskObject from "./tasks/TaskObject.vue";
|
||||
import TaskObject from "../code/components/tasks/TaskObject.vue";
|
||||
import PluginSelect from "../../components/plugins/PluginSelect.vue";
|
||||
import {NoCodeElement, Schemas} from "../code/utils/types";
|
||||
import {
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<el-input :model-value="JSON.stringify(values)">
|
||||
<template #append>
|
||||
<el-button :icon="TextSearch" @click="isOpen = true" />
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<drawer
|
||||
v-if="isOpen"
|
||||
v-model="isOpen"
|
||||
:title="root"
|
||||
>
|
||||
<template #header>
|
||||
<code>{{ root }}</code>
|
||||
</template>
|
||||
<el-form label-position="top">
|
||||
<task-editor
|
||||
ref="editor"
|
||||
:section="SECTIONS.TRIGGERS"
|
||||
:model-value="taskYaml"
|
||||
@update:model-value="onInput"
|
||||
/>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button :icon="ContentSave" @click="isOpen = false" type="primary">
|
||||
{{ $t('save') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {SECTIONS} from "@kestra-io/ui-libs";
|
||||
import * as YAML_UTILS from "@kestra-io/ui-libs/flow-yaml-utils";
|
||||
|
||||
import TextSearch from "vue-material-design-icons/TextSearch.vue";
|
||||
import ContentSave from "vue-material-design-icons/ContentSave.vue";
|
||||
import TaskEditor from "../TaskEditor.vue"
|
||||
import Drawer from "../../Drawer.vue"
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Task from "./Task"
|
||||
|
||||
export default {
|
||||
mixins: [Task],
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
taskYaml() {
|
||||
return YAML_UTILS.stringify(this.modelValue);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onInput(value) {
|
||||
this.$emit("update:modelValue", YAML_UTILS.parse(value));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,255 +0,0 @@
|
||||
<template>
|
||||
<el-form label-position="top" class="w-100">
|
||||
<template v-if="sortedProperties">
|
||||
<template v-for="[fieldKey, fieldSchema] in protectedRequiredProperties" :key="fieldKey">
|
||||
<TaskWrapper :merge>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</template>
|
||||
|
||||
<el-collapse v-model="activeNames" v-if="requiredProperties.length && (optionalProperties?.length || deprecatedProperties?.length || connectionProperties?.length)" class="collapse">
|
||||
<el-collapse-item name="connection" v-if="connectionProperties?.length" :title="$t('no_code.sections.connection')">
|
||||
<template v-for="[fieldKey, fieldSchema] in connectionProperties" :key="fieldKey">
|
||||
<TaskWrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item name="optional" v-if="optionalProperties?.length" :title="$t('no_code.sections.optional')">
|
||||
<template v-for="[fieldKey, fieldSchema] in optionalProperties" :key="fieldKey">
|
||||
<TaskWrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item name="deprecated" v-if="deprecatedProperties?.length" :title="$t('no_code.sections.deprecated')">
|
||||
<template v-for="[fieldKey, fieldSchema] in deprecatedProperties" :key="fieldKey">
|
||||
<TaskWrapper>
|
||||
<template #tasks>
|
||||
<TaskObjectField v-bind="fieldProps(fieldKey, fieldSchema)" />
|
||||
</template>
|
||||
</TaskWrapper>
|
||||
</template>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<task-dict
|
||||
:model-value="modelValue"
|
||||
:task="task"
|
||||
@update:model-value="
|
||||
(value) => $emit('update:modelValue', value)
|
||||
"
|
||||
:root="root"
|
||||
:schema="schema"
|
||||
:required="required"
|
||||
:definitions="definitions"
|
||||
/>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TaskDict from "./TaskDict.vue";
|
||||
import TaskWrapper from "./TaskWrapper.vue";
|
||||
import TaskObjectField from "./TaskObjectField.vue";
|
||||
|
||||
defineEmits(["update:modelValue"]);
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import Task from "./Task";
|
||||
|
||||
const FIRST_FIELDS = ["id", "forced", "on", "type"];
|
||||
|
||||
function sortProperties(properties, required) {
|
||||
if(!properties.length) {
|
||||
return [];
|
||||
}
|
||||
return properties.sort((a, b) => {
|
||||
if (FIRST_FIELDS.includes(a[0])) {
|
||||
return -1;
|
||||
} else if (FIRST_FIELDS.includes(b[0])) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const aRequired = (required || []).includes(
|
||||
a[0],
|
||||
);
|
||||
const bRequired = (required || []).includes(
|
||||
b[0],
|
||||
);
|
||||
|
||||
if (aRequired && !bRequired) {
|
||||
return -1;
|
||||
} else if (!aRequired && bRequired) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const aDefault = "default" in a[1];
|
||||
const bDefault = "default" in b[1];
|
||||
|
||||
if (aDefault && !bDefault) {
|
||||
return 1;
|
||||
} else if (!aDefault && bDefault) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return a[0].localeCompare(b[0]);
|
||||
})
|
||||
}
|
||||
|
||||
function isDeprecated(value) {
|
||||
if(value?.allOf){
|
||||
return value.allOf.some(isDeprecated);
|
||||
}
|
||||
return value?.$deprecated;
|
||||
}
|
||||
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
name: "TaskObject",
|
||||
mixins: [Task],
|
||||
props: {
|
||||
properties: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
merge: {type: Boolean, default: false},
|
||||
metadataInputs: {type: Boolean, default: false}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeNames: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredProperties() {
|
||||
return this.properties ? Object.entries(this.properties).filter(([key, value]) => {
|
||||
return !(key === "type") && !Array.isArray(value);
|
||||
}) : [];
|
||||
},
|
||||
sortedProperties() {
|
||||
return sortProperties(this.filteredProperties, this.schema?.required);
|
||||
},
|
||||
requiredProperties() {
|
||||
return this.merge ? this.sortedProperties : this.sortedProperties.filter(([p,v]) => v && this.isRequired(p));
|
||||
},
|
||||
protectedRequiredProperties(){
|
||||
return this.requiredProperties.length ? this.requiredProperties : this.sortedProperties;
|
||||
},
|
||||
optionalProperties() {
|
||||
return this.merge ? [] : this.sortedProperties.filter(([p,v]) => v && !this.isRequired(p) && !isDeprecated(v) && v.$group !== "connection");
|
||||
},
|
||||
deprecatedProperties() {
|
||||
return this.merge ? [] : this.sortedProperties.filter(([k,v]) => v && isDeprecated(v) && this.modelValue[k] !== undefined);
|
||||
},
|
||||
connectionProperties() {
|
||||
return this.merge ? [] : this.sortedProperties.filter(([p,v]) => v && v.$group === "connection" && !this.isRequired(p));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onObjectInput(propertyName, value) {
|
||||
const currentValue = this.modelValue || {};
|
||||
currentValue[propertyName] = value;
|
||||
this.onInput(currentValue);
|
||||
},
|
||||
isNestedProperty(key) {
|
||||
return key.includes(".") ||
|
||||
["interval", "maxInterval", "minInterval", "type"].includes(key);
|
||||
},
|
||||
fieldProps(key, schema) {
|
||||
return {
|
||||
modelValue: this.modelValue?.[key],
|
||||
"onUpdate:modelValue": (value) => {
|
||||
this.onObjectInput(key, value);
|
||||
},
|
||||
root: this.root,
|
||||
fieldKey: key,
|
||||
task: this.modelValue,
|
||||
schema: schema,
|
||||
definitions: this.definitions,
|
||||
required: this.schema.required,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-form-item__content {
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper.singleton-tooltip {
|
||||
max-width: 300px !important;
|
||||
background: var(--ks-tooltip-background);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../code/styles/code.scss";
|
||||
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
> :deep(.el-form-item__label) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inline-wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
|
||||
.inline-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: var(--ks-content-primary);
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.type-tag {
|
||||
background-color: var(--ks-tag-background-active);
|
||||
color: var(--ks-tag-content);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 2px;
|
||||
border-radius: 8px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.information-icon {
|
||||
color: var(--ks-content-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -186,22 +186,22 @@
|
||||
function updatePluginDocumentation(event: any) {
|
||||
const source = event.model.getValue();
|
||||
const cursorOffset = event.model.getOffsetAt(event.position);
|
||||
|
||||
|
||||
const isPlugin = (type: string) => pluginsStore.allTypes.includes(type);
|
||||
const isInRange = (range: [number, number, number]) =>
|
||||
const isInRange = (range: [number, number, number]) =>
|
||||
cursorOffset >= range[0] && cursorOffset <= range[2];
|
||||
const getRangeSize = (range: [number, number, number]) => range[2] - range[0];
|
||||
|
||||
const getElementFromRange = (typeElement: any) => {
|
||||
const wrapper = YAML_UTILS.localizeElementAtIndex(source, typeElement.range[0]);
|
||||
return wrapper?.value?.type && isPlugin(wrapper.value.type)
|
||||
? wrapper.value
|
||||
? wrapper.value
|
||||
: {type: typeElement.type};
|
||||
};
|
||||
|
||||
const selectedElement = YAML_UTILS.extractFieldFromMaps(source, "type", () => true, isPlugin)
|
||||
.filter(el => el.range && isInRange(el.range))
|
||||
.reduce((closest, current) =>
|
||||
.reduce((closest, current) =>
|
||||
!closest || getRangeSize(current.range) < getRangeSize(closest.range)
|
||||
? current
|
||||
: closest
|
||||
@@ -237,6 +237,7 @@
|
||||
|
||||
const saveFileContent = async () => {
|
||||
clearTimeout(timeout.value);
|
||||
if(!namespace.value || !props.path) return
|
||||
await namespacesStore.createFile({
|
||||
namespace: namespace.value,
|
||||
path: props.path,
|
||||
|
||||
@@ -8,7 +8,7 @@ import InitialFlowSchema from "./flow-schema.json"
|
||||
import {toRaw} from "vue";
|
||||
import {isEntryAPluginElementPredicate} from "@kestra-io/ui-libs";
|
||||
|
||||
interface PluginComponent {
|
||||
export interface PluginComponent {
|
||||
icon?: string;
|
||||
cls?: string;
|
||||
deprecated?: boolean;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {ref} from "vue";
|
||||
import TaskDict from "../../../../../src/components/flows/tasks/TaskDict.vue";
|
||||
import TaskDict from "../../../../../../src/components/code/components/tasks/TaskDict.vue";
|
||||
import {userEvent, waitFor, within, expect} from "storybook/internal/test";
|
||||
import {Meta, StoryObj} from "@storybook/vue3-vite";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
|
||||
const meta: Meta<typeof TaskDict> = {
|
||||
title: "components/flows/tasks/TaskDict",
|
||||
title: "components/nocode/TaskDict",
|
||||
component: TaskDict,
|
||||
decorators: [
|
||||
vueRouter([
|
||||
@@ -0,0 +1,97 @@
|
||||
import TaskObject from "../../../../../../src/components/code/components/tasks/TaskObject.vue";
|
||||
import {ref} from "vue"
|
||||
import {StoryObj} from "@storybook/vue3-vite";
|
||||
import {waitFor, within, expect, fireEvent} from "storybook/test";
|
||||
import {vueRouter} from "storybook-vue3-router";
|
||||
|
||||
export default {
|
||||
decorators: [vueRouter([
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: {template: "<div>home</div>"}
|
||||
}])
|
||||
],
|
||||
title: "Components/NoCode/TaskObject",
|
||||
component: TaskObject,
|
||||
}
|
||||
|
||||
type Story = StoryObj<typeof TaskObject>;
|
||||
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
data: {
|
||||
title: "The list of data rows for the table.",
|
||||
type: "array",
|
||||
items: {type: "object"},
|
||||
},
|
||||
type: {const: "io.kestra.plugin.ee.apps.core.blocks.Table"},
|
||||
},
|
||||
title: "A block for displaying a table.",
|
||||
required: ["id", "id"],
|
||||
};
|
||||
|
||||
const AppTableBlockRender = () => ({
|
||||
setup() {
|
||||
const model = ref<Record<string, any> | undefined>({})
|
||||
return () => <div style={{display: "flex", gap: "16px"}}>
|
||||
<div style={{width: "500px"}}>
|
||||
<TaskObject
|
||||
schema={schema}
|
||||
modelValue={model.value}
|
||||
onUpdate:modelValue={(value) => model.value = value}
|
||||
/>
|
||||
</div>
|
||||
<div style={{width: "500px"}}>
|
||||
<h2>Resulting object</h2>
|
||||
<pre style={{
|
||||
border: "1px solid #555",
|
||||
borderRadius: "4px",
|
||||
padding: "2px",
|
||||
background: "#222"
|
||||
}} data-testid="resulting-object">{JSON.stringify(model.value, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
export const AppTableBlock: Story = {
|
||||
render: AppTableBlockRender,
|
||||
async play({canvasElement}) {
|
||||
const canvas = within(canvasElement);
|
||||
canvas.getByText("+ Add a new value").click();
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByText(/null/)).toBeVisible();
|
||||
});
|
||||
canvas.getByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByPlaceholderText("Key")).toBeVisible();
|
||||
});
|
||||
|
||||
fireEvent.input(canvas.getByPlaceholderText("Key"), {target: {value: "key1"}})
|
||||
fireEvent.input(canvas.getByTestId("monaco-editor-hidden-synced-textarea"), {target: {value: "value1"}})
|
||||
|
||||
canvas.getByText("+ Add a new value", {selector: ".schema-wrapper .schema-wrapper button"}).click();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getAllByPlaceholderText("Key")[1]).toBeVisible();
|
||||
});
|
||||
|
||||
fireEvent.input(canvas.getAllByPlaceholderText("Key")[1], {target: {value: "key2"}})
|
||||
fireEvent.input(canvas.getAllByTestId("monaco-editor-hidden-synced-textarea")[1], {target: {value: "value2"}})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(canvas.getByTestId("resulting-object").innerHTML).toBe(JSON.stringify({
|
||||
data: [
|
||||
{
|
||||
key1: "value1",
|
||||
key2: "value2"
|
||||
}
|
||||
]
|
||||
}, null, 2));
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2023.Array", "ES2020", "DOM", "DOM.Iterable", "ES2021.String"],
|
||||
"lib": ["ES2023.Array", "ES2020", "DOM", "DOM.Iterable", "ES2021.String", "ES2022.Array"],
|
||||
"skipLibCheck": true,
|
||||
"incremental": true,
|
||||
"types": ["vitest/globals"],
|
||||
|
||||
Reference in New Issue
Block a user