Repository: kestra-io/kestra (java)

Issue:
Auto scroll Plugins list to the currently open page
### Feature description

When you go to the Plugins page on the left, and click on a plugin it will put you on the first sub page for that plugin but doesn't show you the others. The user needs to scroll down the list to find the other ones related to that plugin. It would be a nice quality of life thing to have the list scroll down to the active page.

https://github.com/kestra-io/kestra/assets/34094921/68c706e8-ee34-4aa5-81f7-b183f4ebf041

This also happens when you refresh the page, it doesn't remember where you were scrolled to. This means you have to scroll down the list to find the related pages to the plugin.

https://github.com/kestra-io/kestra/assets/34094921/55380d55-cca1-4ade-b847-9a4cb3bb27d7


Pull request description:
feat(ui): added Auto scroll Plugins list to the currently open page 
Enhancements to user experience:

Formatting fixes:

### What changes are being made and why?

This pull request includes few improvements to the `ui/src/components/plugins/Toc.vue` file, focusing on enhancing the user experience by adding new features and fixing formatting issues.
there was an impovement needed in ui , which was described in issue   (#3730)
---
-->video  of updated ui
https://github.com/user-attachments/assets/8ab36d9b-e154-4278-8e1f-1bfc291346f4

-> I utilized localhost for saving the state of the scroll , so even if we refresh the page , the plugin will scroll to that location .

### How the changes have been QAed?
The changes have been QAed by testing the ui in different browsers.

closes #3730 
Thank you for your contribution. ❤️  -->

Patch:
diff --git a/core/src/main/java/io/kestra/core/models/flows/Input.java b/core/src/main/java/io/kestra/core/models/flows/Input.java
index 14d31187a..611df131b 100644
--- a/core/src/main/java/io/kestra/core/models/flows/Input.java
+++ b/core/src/main/java/io/kestra/core/models/flows/Input.java
@@ -41,7 +41,8 @@ import lombok.experimental.SuperBuilder;
     @JsonSubTypes.Type(value = TimeInput.class, name = "TIME"),
     @JsonSubTypes.Type(value = URIInput.class, name = "URI"),
     @JsonSubTypes.Type(value = MultiselectInput.class, name = "MULTISELECT"),
-    @JsonSubTypes.Type(value = YamlInput.class, name = "YAML")
+    @JsonSubTypes.Type(value = YamlInput.class, name = "YAML"),
+    @JsonSubTypes.Type(value = EmailInput.class, name = "EMAIL"),
 })
 @JsonInclude(JsonInclude.Include.NON_DEFAULT)
 public abstract class Input<T> implements Data {
diff --git a/core/src/main/java/io/kestra/core/models/flows/Type.java b/core/src/main/java/io/kestra/core/models/flows/Type.java
index e5a4e2735..4e8641525 100644
--- a/core/src/main/java/io/kestra/core/models/flows/Type.java
+++ b/core/src/main/java/io/kestra/core/models/flows/Type.java
@@ -24,7 +24,8 @@ public enum Type {
     SECRET(SecretInput.class.getName()),
     ARRAY(ArrayInput.class.getName()),
     MULTISELECT(MultiselectInput.class.getName()),
-    YAML(YamlInput.class.getName());
+    YAML(YamlInput.class.getName()),
+    EMAIL(EmailInput.class.getName());
 
     private final String clsName;
 
diff --git a/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java b/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java
new file mode 100644
index 000000000..bfb18e437
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/models/flows/input/EmailInput.java
@@ -0,0 +1,25 @@
+package io.kestra.core.models.flows.input;
+
+import io.kestra.core.models.flows.Input;
+import io.kestra.core.models.validations.ManualConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+
+import java.util.regex.Pattern;
+
+public class EmailInput extends Input<String> {
+
+    private static final String EMAIL_PATTERN = "^$|^[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$";
+
+    @Override
+    public void validate(String input) throws ConstraintViolationException {
+        if(!Pattern.matches(EMAIL_PATTERN, input)){
+            throw ManualConstraintViolation.toConstraintViolationException(
+                "The input must be a valid email",
+                this,
+                EmailInput.class,
+                getId(),
+                input
+            );
+        }
+    }
+}
diff --git a/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java b/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
index b31b1f38b..05d7418bc 100644
--- a/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
+++ b/core/src/main/java/io/kestra/core/runners/FlowInputOutput.java
@@ -393,7 +393,7 @@ public class FlowInputOutput {
     private Object parseType(Execution execution, Type type, String id, Type elementType, Object current) throws Exception {
         try {
             return switch (type) {
-                case SELECT, ENUM, STRING -> current;
+                case SELECT, ENUM, STRING, EMAIL -> current;
                 case SECRET -> {
                     if (secretKey.isEmpty()) {
                         throw new Exception("Unable to use a `SECRET` input/output as encryption is not configured");
@@ -401,7 +401,8 @@ public class FlowInputOutput {
                     yield EncryptionService.encrypt(secretKey.get(), (String) current);
                 }
                 case INT -> current instanceof Integer ? current : Integer.valueOf((String) current);
-                case FLOAT -> current instanceof Float ? current : Float.valueOf((String) current);
+                // Assuming that after the render we must have a double/int, so we can safely use its toString representation
+                case FLOAT -> current instanceof Float ? current : Float.valueOf(current.toString());
                 case BOOLEAN -> current instanceof Boolean ? current : Boolean.valueOf((String) current);
                 case DATETIME -> Instant.parse(((String) current));
                 case DATE -> LocalDate.parse(((String) current));
diff --git a/ui/src/components/content/CardLogos.vue b/ui/src/components/content/CardLogos.vue
index dff5edfff..bcc488be7 100644
--- a/ui/src/components/content/CardLogos.vue
+++ b/ui/src/components/content/CardLogos.vue
@@ -6,7 +6,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-dark-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-dark-version.png')"
                 alt="Dark version logo"
             >
             <p class="title">
@@ -23,7 +23,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-light-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-light-version.png')"
                 alt="Light version logo"
             >
             <p class="title">
@@ -40,7 +40,7 @@
                 width="222.67px"
                 height="125px"
                 loading="lazy"
-                :src="$store.getters['doc/resourceUrl']('/v1/docs/tutorial/logos/logo-monogram-version.png')"
+                :src="$store.getters['doc/resourceUrl']('/docs/tutorial/logos/logo-monogram-version.png')"
                 alt="Monogram version logo"
             >
             <p class="title">
diff --git a/ui/src/components/executions/ForEachStatus.vue b/ui/src/components/executions/ForEachStatus.vue
index 1b321495f..d855eeaa3 100644
--- a/ui/src/components/executions/ForEachStatus.vue
+++ b/ui/src/components/executions/ForEachStatus.vue
@@ -19,7 +19,7 @@
             </router-link>
             <div v-for="state in State.allStates()" :key="state.key">
                 <router-link :to="goToExecutionsList(state.key)" class="el-button count-button" v-if="localSubflowStatus[state.key] >= 0">
-                    {{ capitalizeFirstLetter(state.key) }}
+                    {{ capitalizeFirstLetter(getStateToBeDisplayed(state.key)) }}
                     <span class="counter">{{ localSubflowStatus[state.key] }}</span>
                     <div class="dot rounded-5" :class="`bg-${state.colorClass}`" />
                 </router-link>
@@ -29,6 +29,7 @@
 </template>
 <script>
     import {cssVariable} from "@kestra-io/ui-libs/src/utils/global";
+    import {stateDisplayValues} from "../../utils/constants";
     import State from "../../utils/state";
     import throttle from "lodash/throttle"
 
@@ -79,6 +80,13 @@
             capitalizeFirstLetter(str) {
                 return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
             },
+            getStateToBeDisplayed(str){
+                if(str === State.RUNNING){
+                    return stateDisplayValues.INPROGRESS;
+                }else{
+                    return str;
+                }
+            },
             goToExecutionsList(state) {
                 const queries = {
                     triggerExecutionId: this.executionId,
diff --git a/ui/src/components/executions/Overview.vue b/ui/src/components/executions/Overview.vue
index 4be99804d..e9236c744 100644
--- a/ui/src/components/executions/Overview.vue
+++ b/ui/src/components/executions/Overview.vue
@@ -49,22 +49,22 @@
 
         <div v-if="execution.trigger" class="my-5">
             <h5>{{ $t("trigger") }}</h5>
-            <KestraCascader :options="transform(triggerVariables)" />
+            <KestraCascader :options="transform({...execution.trigger, ...(execution.trigger.trigger ? execution.trigger.trigger : {})})" class="overflow-auto" />
         </div>
 
         <div v-if="execution.inputs" class="my-5">
             <h5>{{ $t("inputs") }}</h5>
-            <KestraCascader :options="transform(execution.inputs)" />
+            <KestraCascader :options="transform(execution.inputs)" class="overflow-auto" />
         </div>
 
         <div v-if="execution.variables" class="my-5">
             <h5>{{ $t("variables") }}</h5>
-            <KestraCascader :options="transform(execution.variables)" />
+            <KestraCascader :options="transform(execution.variables)" class="overflow-auto" />
         </div>
 
         <div v-if="execution.outputs" class="my-5">
             <h5>{{ $t("outputs") }}</h5>
-            <KestraCascader :options="transform(execution.outputs)" />
+            <KestraCascader :options="transform(execution.outputs)" class="overflow-auto" />
         </div>
     </div>
 </template>
@@ -213,14 +213,6 @@
                     })
                 })
                 return inputs;
-            },
-            // This is used to display correctly trigger variables
-            triggerVariables() {
-                let trigger = this.execution.trigger
-                trigger["trigger"] = this.execution.trigger.variables
-                delete trigger["variables"]
-
-                return trigger
             }
         },
     };
diff --git a/ui/src/components/inputs/InputsForm.vue b/ui/src/components/inputs/InputsForm.vue
index 8c9078c39..c6acb15a0 100644
--- a/ui/src/components/inputs/InputsForm.vue
+++ b/ui/src/components/inputs/InputsForm.vue
@@ -11,7 +11,7 @@
                 :full-height="false"
                 :input="true"
                 :navbar="false"
-                v-if="input.type === 'STRING' || input.type === 'URI'"
+                v-if="input.type === 'STRING' || input.type === 'URI' || input.type === 'EMAIL'"
                 v-model="inputs[input.id]"
                 @update:model-value="onChange"
                 @confirm="onSubmit"
diff --git a/ui/src/components/plugins/Toc.vue b/ui/src/components/plugins/Toc.vue
index eaf8b24c7..86b5c1cb4 100644
--- a/ui/src/components/plugins/Toc.vue
+++ b/ui/src/components/plugins/Toc.vue
@@ -7,26 +7,24 @@
             clearable
         />
         <el-collapse accordion v-model="activeNames">
-            <template
-                :key="plugin.title"
-                v-for="(plugin) in sortedPlugins(pluginsList)"
-            >
+            <template :key="plugin.title" v-for="(plugin) in sortedPlugins(pluginsList)">
                 <el-collapse-item
                     v-if="isVisible(plugin)"
                     :name="plugin.group"
                     :title="plugin.title.capitalize()"
                     :key="plugin.group"
+                    :ref="`plugin-${plugin.group}`"
                 >
                     <ul class="toc-h3">
                         <li v-for="(types, namespace) in group(plugin, plugin.tasks)" :key="namespace">
                             <h6>{{ namespace }}</h6>
                             <ul class="toc-h4">
-                                <li v-for="(classes, type) in types" :key="type+'-'+ namespace">
+                                <li v-for="(classes, type) in types" :key="type + '-' + namespace">
                                     <h6>{{ $filters.cap(type) }}</h6>
                                     <ul class="section-nav toc-h5">
                                         <li v-for="cls in classes" :key="cls">
                                             <router-link
-                                                @click="$emit('routerChange')"
+                                                @click="$emit('routerChange'); handlePluginChange(namespace)"
                                                 :to="{name: 'plugins/view', params: {cls: namespace + '.' + cls}}"
                                             >
                                                 <div class="icon">
@@ -38,7 +36,8 @@
                                                 </div>
                                                 <span
                                                     :class="$route.params.cls === (namespace + '.' + cls) ? 'selected mx-2' : 'mx-2'"
-                                                >{{ cls }}</span>
+                                                >{{
+                                                    cls }}</span>
                                             </router-link>
                                         </li>
                                     </ul>
@@ -66,11 +65,14 @@
             }
         },
         mounted() {
+
             this.plugins.forEach(plugin => {
                 if (plugin.tasks.includes(this.$route.params.cls)) {
                     this.activeNames = [plugin.group]
+                    localStorage.setItem("activePlugin", plugin.group);
                 }
             })
+            this.scrollToActivePlugin();
         },
         components: {
             TaskIcon
@@ -117,6 +119,26 @@
             }
         },
         methods: {
+
+            scrollToActivePlugin() {
+                const activePlugin = localStorage.getItem("activePlugin");
+                if (activePlugin) {
+                    // Use Vue's $refs to scroll to the specific plugin group
+                    this.$nextTick(() => {
+                        const pluginElement = this.$refs[`plugin-${activePlugin}`];
+                        if (pluginElement && pluginElement[0]) {
+                            pluginElement[0].$el.scrollIntoView({behavior: "smooth", block: "start"});
+                        }
+                    });
+                }
+            },
+
+            // When user navigates to a different plugin, save the new plugin group to localStorage
+            handlePluginChange(pluginGroup) {
+                this.activeNames = [pluginGroup];
+                localStorage.setItem("activePlugin", pluginGroup); // Save to localStorage
+            },
+
             sortedPlugins(plugins) {
                 return plugins
                     .sort((a, b) => {
@@ -158,70 +180,71 @@
 </script>
 
 <style lang="scss">
-    .plugins-list {
-        &.enhance-readability {
-            padding: calc(var(--spacer) * 1.5);
-            background-color: var(--bs-gray-100);
-        }
+.plugins-list {
+    &.enhance-readability {
+        padding: calc(var(--spacer) * 1.5);
+        background-color: var(--bs-gray-100);
+    }
 
-        &::-webkit-scrollbar {
-            width: 2px;
-        }
+    &::-webkit-scrollbar {
+        width: 2px;
+    }
 
-        &::-webkit-scrollbar-track {
-            -webkit-border-radius: 10px;
-        }
+    &::-webkit-scrollbar-track {
+        -webkit-border-radius: 10px;
+    }
 
-        &::-webkit-scrollbar-thumb {
-            -webkit-border-radius: 10px;
-            background: var(--bs-gray-600);
-        }
+    &::-webkit-scrollbar-thumb {
+        -webkit-border-radius: 10px;
+        background: var(--bs-gray-600);
+    }
 
-        .el-collapse-item__header {
-            font-size: 0.875rem;
-        }
+    .el-collapse-item__header {
+        font-size: 0.875rem;
+    }
+
+    ul {
+        list-style: none;
+        padding-inline-start: 0;
+        margin-bottom: 0;
+        font-size: var(--font-size-xs);
+        margin-left: calc(var(--spacer) / 2);
+    }
+
+    h6,
+    a {
+        word-break: break-all;
+        color: var(--el-collapse-header-text-color);
+    }
 
-        ul {
-            list-style: none;
-            padding-inline-start: 0;
-            margin-bottom: 0;
-            font-size: var(--font-size-xs);
-            margin-left: calc(var(--spacer) / 2);
+    .toc-h3 {
+        .icon {
+            width: var(--font-size-sm);
+            height: var(--font-size-sm);
+            display: inline-block;
+            position: relative;
         }
 
-        h6, a {
-            word-break: break-all;
-            color: var(--el-collapse-header-text-color);
+        h6 {
+            font-size: 1.1em;
         }
 
-        .toc-h3 {
-            .icon {
-                width: var(--font-size-sm);
-                height: var(--font-size-sm);
-                display: inline-block;
-                position: relative;
-            }
+        .toc-h4 {
+            margin-left: var(--spacer);
 
             h6 {
-                font-size: 1.1em;
+                font-size: var(--font-size-sm);
+                margin-bottom: calc(var(--spacer) / 2);
             }
 
-            .toc-h4 {
-                margin-left: var(--spacer);
-
-                h6 {
-                    font-size: var(--font-size-sm);
-                    margin-bottom: calc(var(--spacer) / 2);
-                }
-
-                li {
-                    margin-bottom: calc(var(--spacer) / 2);
-                }
+            li {
+                margin-bottom: calc(var(--spacer) / 2);
             }
         }
     }
+}
 
-    .selected {
-        color: var(--bs-purple);
-    }
+.selected {
+    color: var(--bs-purple);
+}
 </style>
diff --git a/ui/src/utils/constants.js b/ui/src/utils/constants.js
index 217ab86ef..7bbd1dfbf 100644
--- a/ui/src/utils/constants.js
+++ b/ui/src/utils/constants.js
@@ -47,3 +47,7 @@ export const executeFlowBehaviours = {
     NEW_TAB: "new tab"
 }
 
+export const stateDisplayValues = {
+    INPROGRESS: "IN-PROGRESS"
+}
+


Test patch:
diff --git a/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java b/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
index 8ec1e1ced..6878c1ce2 100644
--- a/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
+++ b/webserver/src/test/java/io/kestra/webserver/controllers/api/PluginControllerTest.java
@@ -213,7 +213,7 @@ class PluginControllerTest {
                 Argument.listOf(InputType.class)
             );
 
-            assertThat(doc.size(), is(17));
+            assertThat(doc.size(), is(18));
         });
     }