Repository: kestra-io/kestra
Issue: Retry not handle as an object and can't be edited in task editor
Labels: bug

### Expected Behavior

Should be able to edit retry

### Actual Behaviour

If a retry exists, it will not be displayed as an object. Also, it cannot be edited.
![image](https://github.com/kestra-io/kestra/assets/37600690/8d525b06-21dc-4914-9751-9aa556e9f9f0)


### Steps To Reproduce

_No response_

### Environment Information

- Kestra Version: 0.9.0

### Example flow

_No response_

Issue comments:

Pull request review comments:
Required for class like Retry where they have a "type" property that we want to keep
It is a bit tricky here, we must force default value if the property is required because, however, even if the property is visually filled in the input, it will not be in the javascript object that represents the task 
Cast is necessary because we have now two `flatten` method with 3 params that could works with a null as a 3rd parameter
There may be multiple `type` that we will need to keep, better to extract that in a dedicated method like `isTypeToKeep(entry.getKey)` I also think that if there is a class related exception we should just goes with the default (so returning null)
The `onInput` method has been removed, are you sure it's correct here?
This class is for the documentation but the validation should occurs on the JsonSchema so I have mixed feelings here ...
Yes because we use the mixins Task that already have the `onInput` method which is the good one for this component, the  `onInput` on this component was overriding the other one.
Well, I just ask to be extracted to a method, not need to create a list with one element this seems overcomplicated ;)
@Skraye can you explain why it is in the documentation part and not on the jsonschema?
Well, if we face another issue similar with a type property, we just need to add `Class.class` and its done
If we face another issue we just nee to switch to a list and iterate over it.
KISS, don't optimize for a potential future that will never happen

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java b/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
index 6fc592c42b..541cc2a006 100644
--- a/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
+++ b/core/src/main/java/io/kestra/core/docs/AbstractClassDocumentation.java
@@ -1,7 +1,12 @@
 package io.kestra.core.docs;
 
 import com.google.common.base.CaseFormat;
-import lombok.*;
+import io.kestra.core.models.tasks.retrys.AbstractRetry;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.ArrayUtils;
 
 import java.util.*;
@@ -10,6 +15,7 @@
 @Getter
 @EqualsAndHashCode
 @ToString
+@Slf4j
 public abstract class AbstractClassDocumentation<T> {
     protected Boolean deprecated;
     protected String cls;
@@ -42,7 +48,7 @@ protected AbstractClassDocumentation(JsonSchemaGenerator jsonSchemaGenerator, Cl
             .filter(entry -> !entry.getKey().equals("io.kestra.core.models.tasks.Task"))
             .map(entry -> {
                 Map<String, Object> value = (Map<String, Object>) entry.getValue();
-                value.put("properties", flatten(properties(value), required(value)));
+                value.put("properties", flatten(properties(value), required(value), isTypeToKeep(entry.getKey())));
 
                 return new AbstractMap.SimpleEntry<>(
                     entry.getKey(),
@@ -80,7 +86,14 @@ protected AbstractClassDocumentation(JsonSchemaGenerator jsonSchemaGenerator, Cl
 
     protected static Map<String, Object> flatten(Map<String, Object> map, List<String> required) {
         map.remove("type");
-        return flatten(map, required, null);
+        return flatten(map, required, (String) null);
+    }
+
+    protected static Map<String, Object> flatten(Map<String, Object> map, List<String> required, Boolean keepType) {
+        if (!keepType) {
+            map.remove("type");
+        }
+        return flatten(map, required, (String) null);
     }
 
     @SuppressWarnings("unchecked")
@@ -114,6 +127,19 @@ protected static Map<String, Object> flatten(Map<String, Object> map, List<Strin
         return result;
     }
 
+    // Some task can have the `type` property but not to represent the task
+    // so we cant to keep it in the doc
+    private Boolean isTypeToKeep(String key){
+        try {
+            if (AbstractRetry.class.isAssignableFrom(Class.forName(key))) {
+                return true;
+            }
+        } catch (ClassNotFoundException ignored) {
+            log.debug(ignored.getMessage(), ignored);
+        }
+        return false;
+    }
+
     protected static String flattenKey(String current, String parent) {
         return (parent != null ? parent + "." : "") + current;
     }
diff --git a/ui/src/components/flows/tasks/Task.js b/ui/src/components/flows/tasks/Task.js
index 81cf131868..2ec5a4a846 100644
--- a/ui/src/components/flows/tasks/Task.js
+++ b/ui/src/components/flows/tasks/Task.js
@@ -58,6 +58,10 @@ export default {
                 return "number";
             }
 
+            if (Object.prototype.hasOwnProperty.call(property, "anyOf")) {
+                return "anyOf";
+            }
+
             return property.type || "dynamic";
         },
         // eslint-disable-next-line no-unused-vars
diff --git a/ui/src/components/flows/tasks/TaskAnyOf.vue b/ui/src/components/flows/tasks/TaskAnyOf.vue
new file mode 100644
index 0000000000..f2bdc65da3
--- /dev/null
+++ b/ui/src/components/flows/tasks/TaskAnyOf.vue
@@ -0,0 +1,113 @@
+<template>
+    <el-input
+        :model-value="JSON.stringify(values)"
+        :disabled="true"
+    >
+        <template #append>
+            <el-button :icon="Eye" @click="isOpen = true"/>
+        </template>
+    </el-input>
+
+
+    <el-drawer
+        v-if="isOpen"
+        v-model="isOpen"
+        destroy-on-close
+        size=""
+        :append-to-body="true"
+    >
+        <template #header>
+            <code>{{ root }}</code>
+        </template>
+        <el-form-item>
+            <template #label>
+                <span class="d-flex flex-grow-1">
+                    <span class="me-auto">
+                        <code> Any of</code>&nbsp;
+                    </span>
+                </span>
+            </template>
+            <el-select
+                :model-value="selectedSchema"
+                @update:model-value="onSelect"
+            >
+                <el-option
+                    v-for="schema in schemaOptions"
+                    :key="schema.label"
+                    :label="schema.label"
+                    :value="schema.value"
+                />
+            </el-select>
+        </el-form-item>
+        <el-form label-position="top" v-if="selectedSchema">
+            <component
+                :is="`task-${getType(currentSchema)}`"
+                v-if="currentSchema"
+                :model-value="modelValue"
+                @update:model-value="onInput"
+                :schema="currentSchema"
+                :definitions="definitions"
+            />
+        </el-form>
+        <template #footer>
+            <el-button :icon="ContentSave" @click="isOpen = false" type="primary">
+                {{ $t("save") }}
+            </el-button>
+        </template>
+    </el-drawer>
+</template>
+
+<script setup>
+    import Eye from "vue-material-design-icons/Eye.vue";
+    import ContentSave from "vue-material-design-icons/ContentSave.vue";
+    import Markdown from "../../layout/Markdown.vue";
+    import Help from "vue-material-design-icons/HelpBox.vue";
+    import TaskObject from "./TaskObject.vue";
+</script>
+
+<script>
+    import Task from "./Task"
+
+    export default {
+        mixins: [Task],
+        data() {
+            return {
+                isOpen: false,
+                schemas: [],
+                selectedSchema: undefined
+            };
+        },
+        created() {
+            this.schemas = this.schema?.anyOf ?? []
+        },
+        methods: {
+            onSelect(value) {
+                this.selectedSchema = value
+                // Set up default values
+                if (this.currentSchema.properties && this.modelValue === undefined) {
+                    const defaultValues = {};
+                    for (let prop in this.currentSchema.properties) {
+                        if(this.currentSchema.properties[prop].$required && this.currentSchema.properties[prop].default) {
+                            defaultValues[prop] = this.currentSchema.properties[prop].default
+                        }
+                    }
+                    this.onInput(defaultValues);
+                }
+            }
+        },
+        computed: {
+            currentSchema() {
+                return this.definitions[this.selectedSchema] ?? {type: this.selectedSchema}
+            },
+            schemaOptions() {
+                return this.schemas.map(schema => {
+                    const label = schema.$ref ? schema.$ref.split("/").pop() : schema.type
+                    return {
+                        label: label.capitalize(),
+                        value: label
+                    }
+                })
+            }
+        },
+    };
+</script>
diff --git a/ui/src/components/flows/tasks/TaskObject.vue b/ui/src/components/flows/tasks/TaskObject.vue
index 7da04dbdd6..9188034965 100644
--- a/ui/src/components/flows/tasks/TaskObject.vue
+++ b/ui/src/components/flows/tasks/TaskObject.vue
@@ -26,7 +26,7 @@
             <component
                 :is="`task-${getType(schema)}`"
                 :model-value="getPropertiesValue(key)"
-                @update:model-value="onInput(key, $event)"
+                @update:model-value="onObjectInput(key, $event)"
                 :root="getKey(key)"
                 :schema="schema"
                 :required="isRequired(key)"
@@ -40,6 +40,7 @@
             :root="root"
             :schema="schema"
             :definitions="definitions"
+            @update:model-value="onInput"
         />
     </template>
 </template>
@@ -69,16 +70,11 @@
             properties() {
                 if (this.schema) {
                     const properties = this.schema.properties
-
                     return this.sortProperties(properties)
                 }
 
                 return undefined;
-            },
-            editorValue() {
-                const stringify = YamlUtils.stringify(toRaw(this.modelValue));
-                return stringify.trim() === "{}" ? "" : stringify;
-            },
+            }
         },
         methods: {
             getPropertiesValue(properties) {
@@ -125,7 +121,7 @@
                         return result
                     }, {});
             },
-            onInput(properties, value) {
+            onObjectInput(properties, value) {
                 const currentValue = this.modelValue || {};
                 currentValue[properties] = value;
                 this.$emit("update:modelValue", currentValue);
diff --git a/ui/src/utils/init.js b/ui/src/utils/init.js
index f7ed873c78..5f4a98982c 100644
--- a/ui/src/utils/init.js
+++ b/ui/src/utils/init.js
@@ -43,6 +43,7 @@ import TaskNumber from "../components/flows/tasks/TaskNumber.vue";
 import TaskObject from "../components/flows/tasks/TaskObject.vue";
 import TaskString from "../components/flows/tasks/TaskString.vue";
 import TaskTask from "../components/flows/tasks/TaskTask.vue";
+import TaskAnyOf from "../components/flows/tasks/TaskAnyOf.vue";
 
 export default (app, routes, stores, translations) => {
     // charts
@@ -137,6 +138,7 @@ export default (app, routes, stores, translations) => {
     app.component("TaskComplex", TaskComplex)
     app.component("TaskString", TaskString)
     app.component("TaskTask", TaskTask)
+    app.component("TaskAnyOf", TaskAnyOf)
 
     return {store, router};
 }


Test patch: