From 8acbc8ba03c6b54149e02a16d4d678ab8cf37f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Tue, 2 Sep 2025 14:19:55 +0200 Subject: [PATCH] fix(nocode): KeyValue Pairs have a bug (#10998) --- ui/eslint.config.js | 1 + ui/src/components/code/NoCode.vue | 12 +- .../code/components/inputs/InputPair.vue | 9 +- .../components}/tasks/ClearButton.vue | 0 .../components/tasks/MixinTask.ts} | 5 +- .../components}/tasks/TaskAnyOf.vue | 2 +- .../components}/tasks/TaskArray.vue | 14 +- .../components}/tasks/TaskBasic.vue | 6 +- .../components}/tasks/TaskBoolean.vue | 0 .../components}/tasks/TaskComplex.vue | 2 +- .../components}/tasks/TaskConstant.vue | 0 .../components}/tasks/TaskDict.vue | 8 +- .../components}/tasks/TaskEnum.vue | 2 +- .../components}/tasks/TaskExpression.vue | 4 +- .../components}/tasks/TaskKvPairs.vue | 22 +- .../tasks/TaskLabelWithBoolean.vue | 0 .../components}/tasks/TaskList.vue | 8 +- .../components}/tasks/TaskNamespace.vue | 6 +- .../components}/tasks/TaskNumber.vue | 2 +- .../code/components/tasks/TaskObject.vue | 255 ++++++++++++++++++ .../components}/tasks/TaskObjectField.vue | 7 +- .../components}/tasks/TaskString.vue | 6 +- .../components}/tasks/TaskSubflowId.vue | 4 +- .../components}/tasks/TaskSubflowInputs.vue | 4 +- .../components}/tasks/TaskTask.vue | 4 +- .../components}/tasks/TaskTaskRunner.vue | 0 .../components}/tasks/TaskTasks.vue | 8 +- .../components}/tasks/TaskVersion.vue | 2 +- .../components/tasks/Wrapper.vue} | 2 +- .../components}/tasks/getTaskComponent.ts | 4 - ui/src/components/flows/MetadataEditor.vue | 8 - ui/src/components/flows/TaskEditor.vue | 3 +- .../components/flows/tasks/TaskCondition.vue | 66 ----- ui/src/components/flows/tasks/TaskObject.vue | 255 ------------------ ui/src/components/inputs/EditorWrapper.vue | 9 +- ui/src/stores/plugins.ts | 2 +- .../components}/tasks/TaskDict.stories.tsx | 4 +- .../components/tasks/TaskObject.stories.tsx | 97 +++++++ ui/tsconfig.json | 2 +- 39 files changed, 427 insertions(+), 418 deletions(-) rename ui/src/components/{flows => code/components}/tasks/ClearButton.vue (100%) rename ui/src/components/{flows/tasks/Task.ts => code/components/tasks/MixinTask.ts} (94%) rename ui/src/components/{flows => code/components}/tasks/TaskAnyOf.vue (99%) rename ui/src/components/{flows => code/components}/tasks/TaskArray.vue (92%) rename ui/src/components/{flows => code/components}/tasks/TaskBasic.vue (97%) rename ui/src/components/{flows => code/components}/tasks/TaskBoolean.vue (100%) rename ui/src/components/{flows => code/components}/tasks/TaskComplex.vue (97%) rename ui/src/components/{flows => code/components}/tasks/TaskConstant.vue (100%) rename ui/src/components/{flows => code/components}/tasks/TaskDict.vue (95%) rename ui/src/components/{flows => code/components}/tasks/TaskEnum.vue (95%) rename ui/src/components/{flows => code/components}/tasks/TaskExpression.vue (92%) rename ui/src/components/{flows => code/components}/tasks/TaskKvPairs.vue (55%) rename ui/src/components/{flows => code/components}/tasks/TaskLabelWithBoolean.vue (100%) rename ui/src/components/{flows => code/components}/tasks/TaskList.vue (85%) rename ui/src/components/{flows => code/components}/tasks/TaskNamespace.vue (79%) rename ui/src/components/{flows => code/components}/tasks/TaskNumber.vue (95%) create mode 100644 ui/src/components/code/components/tasks/TaskObject.vue rename ui/src/components/{flows => code/components}/tasks/TaskObjectField.vue (96%) rename ui/src/components/{flows => code/components}/tasks/TaskString.vue (96%) rename ui/src/components/{flows => code/components}/tasks/TaskSubflowId.vue (93%) rename ui/src/components/{flows => code/components}/tasks/TaskSubflowInputs.vue (98%) rename ui/src/components/{flows => code/components}/tasks/TaskTask.vue (94%) rename ui/src/components/{flows => code/components}/tasks/TaskTaskRunner.vue (100%) rename ui/src/components/{flows => code/components}/tasks/TaskTasks.vue (83%) rename ui/src/components/{flows => code/components}/tasks/TaskVersion.vue (89%) rename ui/src/components/{flows/tasks/TaskWrapper.vue => code/components/tasks/Wrapper.vue} (93%) rename ui/src/components/{flows => code/components}/tasks/getTaskComponent.ts (96%) delete mode 100644 ui/src/components/flows/tasks/TaskCondition.vue delete mode 100644 ui/src/components/flows/tasks/TaskObject.vue rename ui/tests/storybook/components/{flows => code/components}/tasks/TaskDict.stories.tsx (95%) create mode 100644 ui/tests/storybook/components/code/components/tasks/TaskObject.stories.tsx diff --git a/ui/eslint.config.js b/ui/eslint.config.js index 92c1db11b8..e7b3df3d90 100644 --- a/ui/eslint.config.js +++ b/ui/eslint.config.js @@ -89,6 +89,7 @@ export default [ { // Enforce the use of the diff --git a/ui/src/components/flows/tasks/TaskEnum.vue b/ui/src/components/code/components/tasks/TaskEnum.vue similarity index 95% rename from ui/src/components/flows/tasks/TaskEnum.vue rename to ui/src/components/code/components/tasks/TaskEnum.vue index a9a090e036..0e11163231 100644 --- a/ui/src/components/flows/tasks/TaskEnum.vue +++ b/ui/src/components/code/components/tasks/TaskEnum.vue @@ -16,7 +16,7 @@ \ No newline at end of file diff --git a/ui/src/components/flows/tasks/TaskLabelWithBoolean.vue b/ui/src/components/code/components/tasks/TaskLabelWithBoolean.vue similarity index 100% rename from ui/src/components/flows/tasks/TaskLabelWithBoolean.vue rename to ui/src/components/code/components/tasks/TaskLabelWithBoolean.vue diff --git a/ui/src/components/flows/tasks/TaskList.vue b/ui/src/components/code/components/tasks/TaskList.vue similarity index 85% rename from ui/src/components/flows/tasks/TaskList.vue rename to ui/src/components/code/components/tasks/TaskList.vue index 7c39367a4e..6ce96bd773 100644 --- a/ui/src/components/flows/tasks/TaskList.vue +++ b/ui/src/components/code/components/tasks/TaskList.vue @@ -13,9 +13,9 @@ + + diff --git a/ui/src/components/flows/tasks/TaskObjectField.vue b/ui/src/components/code/components/tasks/TaskObjectField.vue similarity index 96% rename from ui/src/components/flows/tasks/TaskObjectField.vue rename to ui/src/components/code/components/tasks/TaskObjectField.vue index d61cc6f71d..a3783d346a 100644 --- a/ui/src/components/flows/tasks/TaskObjectField.vue +++ b/ui/src/components/code/components/tasks/TaskObjectField.vue @@ -64,10 +64,9 @@ - - diff --git a/ui/src/components/inputs/EditorWrapper.vue b/ui/src/components/inputs/EditorWrapper.vue index 925bdbc531..7aa5d8f791 100644 --- a/ui/src/components/inputs/EditorWrapper.vue +++ b/ui/src/components/inputs/EditorWrapper.vue @@ -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, diff --git a/ui/src/stores/plugins.ts b/ui/src/stores/plugins.ts index 084db3ad49..e53c302ea9 100644 --- a/ui/src/stores/plugins.ts +++ b/ui/src/stores/plugins.ts @@ -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; diff --git a/ui/tests/storybook/components/flows/tasks/TaskDict.stories.tsx b/ui/tests/storybook/components/code/components/tasks/TaskDict.stories.tsx similarity index 95% rename from ui/tests/storybook/components/flows/tasks/TaskDict.stories.tsx rename to ui/tests/storybook/components/code/components/tasks/TaskDict.stories.tsx index 883d8db010..686557c166 100644 --- a/ui/tests/storybook/components/flows/tasks/TaskDict.stories.tsx +++ b/ui/tests/storybook/components/code/components/tasks/TaskDict.stories.tsx @@ -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 = { - title: "components/flows/tasks/TaskDict", + title: "components/nocode/TaskDict", component: TaskDict, decorators: [ vueRouter([ diff --git a/ui/tests/storybook/components/code/components/tasks/TaskObject.stories.tsx b/ui/tests/storybook/components/code/components/tasks/TaskObject.stories.tsx new file mode 100644 index 0000000000..ed895d5cb5 --- /dev/null +++ b/ui/tests/storybook/components/code/components/tasks/TaskObject.stories.tsx @@ -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: "
home
"} + }]) + ], + title: "Components/NoCode/TaskObject", + component: TaskObject, +} + +type Story = StoryObj; + +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 | undefined>({}) + return () =>
+
+ model.value = value} + /> +
+
+

Resulting object

+
{JSON.stringify(model.value, null, 2)}
+
+
+ } +}); + +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)); + }); + + } +} diff --git a/ui/tsconfig.json b/ui/tsconfig.json index cd44e081ba..c80f5ba4d4 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -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"],