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 Screenshot 2025-12-11 at 15 54 07 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} + + + + + ); +};