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

bbovenzi 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 fb235e6926a Preserve newlines in XCom tab string values (#74112)
fb235e6926a is described below

commit fb235e6926a67d839d1a009df13b2a15fea73eda
Author: Basant_Jamwal <[email protected]>
AuthorDate: Thu Oct 8 23:07:48 2026 +0530

    Preserve newlines in XCom tab string values (#74112)
    
    * Preserve newlines in XCom tab string values
    
    * Fix end-of-file and line-ending issues in XComEntry.test.tsx
    
    * Fix line endings in XComEntry test
    
    * Fix formatting in XComEntry
---
 .../airflow/ui/src/pages/XCom/XComEntry.test.tsx   | 28 +++++++++++++++++++++-
 .../src/airflow/ui/src/pages/XCom/XComEntry.tsx    | 19 ++++++++++-----
 2 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
index 72138d3a487..e82fd314bdd 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.test.tsx
@@ -38,6 +38,19 @@ beforeAll(() => server.listen({ onUnhandledFrame: "bypass" 
}));
 afterEach(() => server.resetHandlers());
 afterAll(() => server.close());
 
+const renderValue = (value: string) => {
+  server.use(http.get(entryUrl, () => HttpResponse.json({ key: "return_value", 
value })));
+
+  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 
false } } });
+
+  render(
+    <QueryClientProvider client={queryClient}>
+      <XComEntry {...entryProps} xcomKey="return_value" />
+    </QueryClientProvider>,
+    { wrapper: Wrapper },
+  );
+};
+
 describe("XComEntry", () => {
   it.each([
     { detail: "XCom entry was deleted", status: 404 },
@@ -213,4 +226,17 @@ describe("XComEntry", () => {
 
     
expect(within(dialog).getByTestId("error-alert")).toHaveTextContent(/network 
error/iu);
   });
-});
+
+  it("keeps newlines in string values", async () => {
+    renderValue("a\nb");
+
+    expect((await 
screen.findByTestId("xcom-value")).textContent).toContain("a\nb");
+  });
+
+  it("keeps newlines and still renders links", async () => {
+    renderValue("Line 1\nSee https://airflow.apache.org\nLine 3");
+
+    expect(await screen.findByRole("link")).toHaveAttribute("href", 
"https://airflow.apache.org";);
+    expect(screen.getByTestId("xcom-value").textContent).toContain("Line 
1\nSee ");
+  });
+});
\ No newline at end of file
diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx 
b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
index aad31bc6aff..9a8675ce893 100644
--- a/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComEntry.tsx
@@ -44,13 +44,11 @@ type XComEntryProps = {
 
 const renderTextWithLinks = (text: string) => {
   const urls = text.match(urlRegex);
-  const parts = text.split(/\s+/u);
+  const parts = text.split(/(\s+)/u);
 
   return (
     <>
-      {parts.map((part, index) => {
-        const isLastPart = index === parts.length - 1;
-
+      {parts.map((part) => {
         if (urls?.includes(part)) {
           return (
             <Link
@@ -66,7 +64,7 @@ const renderTextWithLinks = (text: string) => {
           );
         }
 
-        return `${part}${isLastPart ? "" : " "}`;
+        return part;
       })}
     </>
   );
@@ -127,6 +125,7 @@ export const XComEntry = ({ dagId, mapIndex, open = false, 
runId, taskId, xcomKe
   const xcomValue = data?.value;
   const isObjectOrArray = Array.isArray(xcomValue) || (xcomValue !== null && 
typeof xcomValue === "object");
   const valueFormatted = typeof xcomValue === "string" ? xcomValue : 
JSON.stringify(xcomValue, undefined, 4);
+  const hasNewline = typeof xcomValue === "string" && xcomValue.includes("\n");
 
   return isLoading ? (
     <Skeleton
@@ -140,7 +139,15 @@ export const XComEntry = ({ dagId, mapIndex, open = false, 
runId, taskId, xcomKe
       {isObjectOrArray ? (
         <RenderedJsonField collapsed={!open} content={xcomValue as object} 
enableClipboard={false} />
       ) : (
-        <Text>{renderTextWithLinks(valueFormatted)}</Text>
+        <Text
+          maxHeight="200px"
+          minWidth={hasNewline ? "250px" : undefined}
+          overflowWrap="anywhere"
+          overflowY="auto"
+          whiteSpace="pre-wrap"
+        >
+          {renderTextWithLinks(valueFormatted)}
+        </Text>
       )}
       {xcomValue === undefined || xcomValue === null ? undefined : (
         <ClipboardRoot value={valueFormatted}>

Reply via email to