From c274adf4d805d3949a8da5c7584f6dfe7d28855d Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Fri, 12 Dec 2025 11:27:02 +0100
Subject: [PATCH] Fix raw json field display in read only (#16502)
Preview
Fixes https://github.com/twentyhq/twenty/issues/16311
---
.../display/components/JsonFieldDisplay.tsx | 47 +++++++++++-
.../meta-types/hooks/useJsonFieldDisplay.ts | 4 +-
.../components/ExpandedFieldDisplay.tsx | 72 +++++++++++++++++++
3 files changed, 120 insertions(+), 3 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedFieldDisplay.tsx
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/JsonFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/JsonFieldDisplay.tsx
index d054f1aa00..e75d03bf30 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/JsonFieldDisplay.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/display/components/JsonFieldDisplay.tsx
@@ -1,9 +1,29 @@
import { useJsonFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay';
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
+import { ExpandedFieldDisplay } from '@/ui/layout/expandable-list/components/ExpandedFieldDisplay';
+import { t } from '@lingui/core/macro';
+import { useRef, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
+import { isTwoFirstDepths, JsonTree } from 'twenty-ui/json-visualizer';
+import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
export const JsonFieldDisplay = () => {
- const { fieldValue, maxWidth } = useJsonFieldDisplay();
+ const { copyToClipboard } = useCopyToClipboard();
+
+ const { fieldValue, maxWidth, isRecordFieldReadOnly } = useJsonFieldDisplay();
+
+ const [isJsonTreeViewOpen, setIsJsonTreeViewOpen] = useState(false);
+ const anchorRef = useRef(null);
+
+ const handleClick = () => {
+ if (isRecordFieldReadOnly) {
+ setIsJsonTreeViewOpen(true);
+ }
+ };
+
+ const handleClickOutside = () => {
+ setIsJsonTreeViewOpen(false);
+ };
if (!isDefined(fieldValue)) {
return <>>;
@@ -11,5 +31,28 @@ export const JsonFieldDisplay = () => {
const value = JSON.stringify(fieldValue);
- return ;
+ return (
+ <>
+
+
+
+ {isJsonTreeViewOpen && (
+
+
+
+ )}
+ >
+ );
};
diff --git a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay.ts
index a8a3f0e0ca..bafe6c73f2 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay.ts
+++ b/packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay.ts
@@ -7,7 +7,8 @@ import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecor
import { FieldContext } from '../../contexts/FieldContext';
export const useJsonFieldDisplay = () => {
- const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
+ const { recordId, fieldDefinition, maxWidth, isRecordFieldReadOnly } =
+ useContext(FieldContext);
const fieldName = fieldDefinition.metadata.fieldName;
@@ -25,5 +26,6 @@ export const useJsonFieldDisplay = () => {
maxWidth,
fieldDefinition,
fieldValue: formattedFieldValue,
+ isRecordFieldReadOnly,
};
};
diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedFieldDisplay.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedFieldDisplay.tsx
new file mode 100644
index 0000000000..d3f864c512
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandedFieldDisplay.tsx
@@ -0,0 +1,72 @@
+import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
+import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
+import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
+import styled from '@emotion/styled';
+import { flip, FloatingPortal, offset, useFloating } from '@floating-ui/react';
+import { type ReactNode } from 'react';
+
+type ExpandedFieldDisplayProps = {
+ anchorElement?: HTMLElement;
+ children: ReactNode;
+ onClickOutside?: () => void;
+};
+
+const StyledExpandedFieldContainer = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(1)};
+ padding: ${({ theme }) => theme.spacing(2)};
+ overflow: auto;
+ box-sizing: border-box;
+ height: 300px;
+ width: 400px;
+ position: relative;
+ overflow-y: auto;
+`;
+
+const StyledContainer = styled.div`
+ display: flex;
+ z-index: ${RootStackingContextZIndices.DropdownPortalBelowModal};
+`;
+
+export const ExpandedFieldDisplay = ({
+ anchorElement,
+ children,
+ onClickOutside,
+}: ExpandedFieldDisplayProps) => {
+ const { refs, floatingStyles } = useFloating({
+ placement: 'bottom-start',
+ middleware: [
+ flip(),
+ offset({
+ mainAxis: -29,
+ crossAxis: -10,
+ }),
+ ],
+ elements: { reference: anchorElement },
+ });
+
+ useListenClickOutside({
+ refs: [refs.domReference, refs.floating],
+ callback: () => {
+ onClickOutside?.();
+ },
+ listenerId: 'expanded-field-display',
+ });
+
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+};