Fix raw json field display in read only (#16502)

Preview
<img width="1185" height="617" alt="Screenshot 2025-12-11 at 15 54 07"
src="https://github.com/user-attachments/assets/863fb58c-04df-4e4d-bf8c-0045c833466c"
/>

Fixes https://github.com/twentyhq/twenty/issues/16311
This commit is contained in:
Etienne
2025-12-12 11:27:02 +01:00
committed by GitHub
parent bd8ed03990
commit c274adf4d8
3 changed files with 120 additions and 3 deletions
@@ -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<HTMLDivElement>(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 <JsonDisplay text={value} maxWidth={maxWidth} />;
return (
<>
<div ref={anchorRef} onClick={handleClick}>
<JsonDisplay text={value} maxWidth={maxWidth} />
</div>
{isJsonTreeViewOpen && (
<ExpandedFieldDisplay
anchorElement={anchorRef.current ?? undefined}
onClickOutside={handleClickOutside}
>
<JsonTree
value={fieldValue}
shouldExpandNodeInitially={isTwoFirstDepths}
emptyArrayLabel={t`Empty Array`}
emptyObjectLabel={t`Empty Object`}
emptyStringLabel={t`[empty string]`}
arrowButtonCollapsedLabel={t`Expand`}
arrowButtonExpandedLabel={t`Collapse`}
onNodeValueClick={copyToClipboard}
/>
</ExpandedFieldDisplay>
)}
</>
);
};
@@ -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,
};
};
@@ -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 (
<FloatingPortal>
<StyledContainer
ref={refs.setFloating}
style={floatingStyles}
data-globally-prevent-click-outside="true"
>
<OverlayContainer>
<StyledExpandedFieldContainer>
{children}
</StyledExpandedFieldContainer>
</OverlayContainer>
</StyledContainer>
</FloatingPortal>
);
};