This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ec850dfd627 Improve HITL form UX (#68397)
ec850dfd627 is described below

commit ec850dfd627af4505a3914112fce3f6a2667d924
Author: Brent Bovenzi <[email protected]>
AuthorDate: Mon Jun 15 23:15:10 2026 -0600

    Improve HITL form UX (#68397)
    
    * Improve HITL form UX
    
    * Fix required action filter
    
    * Fix required action filter button
    
    * Fix HITL form showing [object Object] default and missing subject heading
    
    * Invalidate cash after success hitl submission
    
    ---------
    
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../src/components/FlexibleForm/FlexibleForm.tsx   | 74 ++++++++++++++++++++++
 .../airflow/ui/src/components/NeedsReviewBadge.tsx |  2 +-
 .../ui/src/components/NeedsReviewButton.tsx        |  2 +-
 .../src/airflow/ui/src/constants/filterConfigs.tsx |  2 +-
 .../DagsList/DagsFilters/RequiredActionFilter.tsx  |  5 +-
 .../pages/HITLTaskInstances/HITLResponseForm.tsx   | 14 ++--
 .../airflow/ui/src/queries/useUpdateHITLDetail.ts  |  2 +
 airflow-core/src/airflow/ui/src/utils/hitl.ts      |  6 +-
 8 files changed, 87 insertions(+), 20 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx 
b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
index 792ca6e25a8..2e1e5c5568a 100644
--- a/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FlexibleForm/FlexibleForm.tsx
@@ -28,6 +28,28 @@ import { Accordion } from "../ui";
 import { Row } from "./Row";
 import { isRequired } from "./isParamRequired";
 
+const FlatSection = ({
+  children,
+  hasError,
+  title,
+}: {
+  readonly children: React.ReactNode;
+  readonly hasError: boolean;
+  readonly title: string;
+}) => (
+  <Stack gap={2}>
+    <Text color={hasError ? "fg.error" : undefined} fontWeight="medium">
+      {title}
+      {hasError ? (
+        <Icon color="fg.error" margin="-1" ml={1}>
+          <MdError />
+        </Icon>
+      ) : undefined}
+    </Text>
+    {children}
+  </Stack>
+);
+
 const computeSectionErrors = (
   params: Record<string, ParamSpec>,
   defaultSection: string,
@@ -54,6 +76,7 @@ export type FlexibleFormProps = {
   readonly isHITL?: boolean;
   readonly key?: string;
   readonly namespace?: string;
+  readonly noAccordion?: boolean;
   readonly setError: (error: boolean) => void;
   readonly subHeader?: string;
 };
@@ -65,6 +88,7 @@ export const FlexibleForm = ({
   initialParamsDict,
   isHITL,
   namespace = "default",
+  noAccordion,
   setError,
   subHeader,
 }: FlexibleFormProps) => {
@@ -111,6 +135,56 @@ export const FlexibleForm = ({
     setError(Boolean(error) || newSectionError.size > 0);
   };
 
+  if (noAccordion) {
+    return Object.keys(params).length > 0 ? (
+      <>
+        {Object.entries(params).map(([, secParam]) => {
+          const currentSection = secParam.schema.section ?? 
flexibleFormDefaultSection;
+
+          if (processedSections.has(currentSection)) {
+            return undefined;
+          }
+          processedSections.set(currentSection, true);
+
+          return (
+            <FlatSection
+              hasError={Boolean(sectionError.get(currentSection))}
+              key={currentSection}
+              title={currentSection}
+            >
+              {Boolean(subHeader) ? (
+                <Text color="fg.muted" fontSize="xs">
+                  {subHeader}
+                </Text>
+              ) : undefined}
+              <Stack separator={<StackSeparator py={2} />}>
+                {Boolean(flexFormDescription) ? (
+                  <ReactMarkdown>{flexFormDescription}</ReactMarkdown>
+                ) : undefined}
+                {Object.entries(params)
+                  .filter(
+                    ([, param]) =>
+                      param.schema.section === currentSection ||
+                      (currentSection === flexibleFormDefaultSection && 
!Boolean(param.schema.section)),
+                  )
+                  .map(([name]) => (
+                    <Row key={name} name={name} namespace={namespace} 
onUpdate={onUpdate} />
+                  ))}
+              </Stack>
+            </FlatSection>
+          );
+        })}
+      </>
+    ) : isHITL ? (
+      <FlatSection
+        hasError={Boolean(sectionError.get(flexibleFormDefaultSection))}
+        title={flexibleFormDefaultSection}
+      >
+        {Boolean(flexFormDescription) ? 
<ReactMarkdown>{flexFormDescription}</ReactMarkdown> : undefined}
+      </FlatSection>
+    ) : undefined;
+  }
+
   return Object.keys(params).length > 0 ? (
     Object.entries(params).map(([, secParam]) => {
       const currentSection = secParam.schema.section ?? 
flexibleFormDefaultSection;
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
index 7ab09137629..e270a32e455 100644
--- a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
@@ -43,7 +43,7 @@ export const NeedsReviewBadge = ({ dagId, pendingActions }: 
Props) => {
         data-testid="needs-review-badge"
         
to={`/dags/${dagId}/required_actions?${SearchParamsKeys.RESPONSE_RECEIVED}=false`}
       >
-        <StateBadge colorPalette="deferred" fontSize="md" variant="solid">
+        <StateBadge colorPalette="awaiting_input" fontSize="md" 
variant="solid">
           <LuUserRoundPen />
           {pendingActions.length}
         </StateBadge>
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
index 8e48c0c6fbe..ade24b26f78 100644
--- a/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
@@ -58,7 +58,7 @@ export const NeedsReviewButton = ({
   return hitlTIsCount > 0 ? (
     <Box maxW="250px">
       <StatsCard
-        colorScheme="deferred"
+        colorScheme="awaiting_input"
         count={hitlTIsCount}
         icon={<LuUserRoundPen />}
         isLoading={isLoading}
diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx 
b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
index 6e1c6870cba..3da64282ff5 100644
--- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -280,7 +280,7 @@ export const useFilterConfigs = () => {
       options: [
         { label: translate("hitl:filters.response.all"), value: "all" },
         {
-          label: <StateBadge 
state="deferred">{translate("hitl:filters.response.pending")}</StateBadge>,
+          label: <StateBadge 
state="awaiting_input">{translate("hitl:filters.response.pending")}</StateBadge>,
           value: "false",
         },
         {
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx
 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx
index d9c6fed2518..8a56b5a1ff1 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx
@@ -18,7 +18,6 @@
  */
 import { Button } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
-import { LuUserRoundPen } from "react-icons/lu";
 
 import { StateBadge } from "src/components/StateBadge";
 
@@ -36,9 +35,7 @@ export const RequiredActionFilter = ({ needsReview, onToggle 
}: Props) => {
       onClick={onToggle}
       variant={needsReview ? "solid" : "outline"}
     >
-      <StateBadge colorPalette="deferred">
-        <LuUserRoundPen />
-      </StateBadge>
+      <StateBadge state="awaiting_input" />
       {translate("requiredAction_other")}
     </Button>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
index 87bc57c1d42..c8b1fd4db64 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Button, Box, Spacer, HStack, Accordion, Text } from 
"@chakra-ui/react";
+import { Button, Box, Spacer, HStack, Text } from "@chakra-ui/react";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
 import { FiSend } from "react-icons/fi";
@@ -108,14 +108,7 @@ export const HITLResponseForm = ({ hitlDetail }: 
HITLResponseFormProps) => {
             : undefined}
         </Text>
       ) : undefined}
-      <Accordion.Root
-        defaultValue={[hitlDetail.subject]}
-        mb={4}
-        mt={4}
-        overflow="visible"
-        size="lg"
-        variant="enclosed"
-      >
+      <Box mb={4} mt={4} overflow="visible">
         <FlexibleForm
           disabled={!isPending || hitlDetail.response_received}
           flexFormDescription={hitlDetail.body ?? undefined}
@@ -126,9 +119,10 @@ export const HITLResponseForm = ({ hitlDetail }: 
HITLResponseFormProps) => {
           isHITL
           key={hitlDetail.subject}
           namespace="hitl"
+          noAccordion
           setError={setErrors}
         />
-      </Accordion.Root>
+      </Box>
 
       <Box as="footer" display="flex" justifyContent="flex-end" mt={4}>
         <HStack w="full">
diff --git a/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts 
b/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
index 358b8b87bbc..b75bca8cfbd 100644
--- a/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
@@ -26,6 +26,7 @@ import {
   UseGanttServiceGetGanttDataKeyFn,
   useTaskInstanceServiceGetHitlDetailsKey,
   useTaskInstanceServiceGetHitlDetailKey,
+  useTaskInstanceServiceGetHitlDetailTryDetailKey,
   useTaskInstanceServiceUpdateHitlDetail,
   useTaskInstanceServiceGetTaskInstanceKey,
   useTaskInstanceServiceGetTaskInstancesKey,
@@ -58,6 +59,7 @@ export const useUpdateHITLDetail = ({
       [useTaskInstanceServiceGetTaskInstanceKey, { dagId, dagRunId, mapIndex, 
taskId }],
       [useTaskInstanceServiceGetHitlDetailsKey, { dagIdPrefixPattern: dagId, 
dagRunId }],
       [useTaskInstanceServiceGetHitlDetailKey, { dagId, dagRunId }],
+      [useTaskInstanceServiceGetHitlDetailTryDetailKey, { dagId, dagRunId }],
       UseGanttServiceGetGanttDataKeyFn({ dagId, runId: dagRunId }),
       ...tiPerAttemptQueryKeys,
     ];
diff --git a/airflow-core/src/airflow/ui/src/utils/hitl.ts 
b/airflow-core/src/airflow/ui/src/utils/hitl.ts
index ab1ab135526..cba56351d45 100644
--- a/airflow-core/src/airflow/ui/src/utils/hitl.ts
+++ b/airflow-core/src/airflow/ui/src/utils/hitl.ts
@@ -76,7 +76,7 @@ export const getHITLParamsDict = (
   searchParams: URLSearchParams,
 ): ParamsSpec => {
   const paramsDict: ParamsSpec = {};
-  const { preloadedHITLOptions, preloadedHITLParams } = 
getPreloadHITLFormData(searchParams, hitlDetail);
+  const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams, 
hitlDetail);
   const isApprovalTask =
     hitlDetail.options.includes("Approve") &&
     hitlDetail.options.includes("Reject") &&
@@ -120,7 +120,7 @@ export const getHITLParamsDict = (
       const paramData = hitlDetail.params[key] as ParamsSpec | undefined;
 
       // Check if there's a preloaded value from URL params
-      let finalValue = preloadedHITLParams[key] ?? value;
+      let finalValue = hitlDetail.params_input?.[key] ?? paramData?.value ?? 
value;
 
       // If preloaded value is a string that might be JSON, try to parse it
       if (typeof finalValue === "string" && finalValue.trim().startsWith("{")) 
{
@@ -170,7 +170,7 @@ export const getHITLParamsDict = (
       paramsDict[key] = {
         description,
         schema,
-        value: paramData?.value ?? finalValue,
+        value: finalValue ?? paramData?.value,
       };
     });
   }

Reply via email to