Skip to content

Commit b4e9679

Browse files
Cherry-pick: Fix raw json field display in read only (twentyhq#16502)
1 parent 31f2b5a commit b4e9679

File tree

3 files changed

+120
-3
lines changed

3 files changed

+120
-3
lines changed
Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,58 @@
11
import { useJsonFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay';
22
import { JsonDisplay } from '@/ui/field/display/components/JsonDisplay';
3+
import { ExpandedFieldDisplay } from '@/ui/layout/expandable-list/components/ExpandedFieldDisplay';
4+
import { t } from '@lingui/core/macro';
5+
import { useRef, useState } from 'react';
36
import { isDefined } from 'twenty-shared/utils';
7+
import { isTwoFirstDepths, JsonTree } from 'twenty-ui/json-visualizer';
8+
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
49

510
export const JsonFieldDisplay = () => {
6-
const { fieldValue, maxWidth } = useJsonFieldDisplay();
11+
const { copyToClipboard } = useCopyToClipboard();
12+
13+
const { fieldValue, maxWidth, isRecordFieldReadOnly } = useJsonFieldDisplay();
14+
15+
const [isJsonTreeViewOpen, setIsJsonTreeViewOpen] = useState(false);
16+
const anchorRef = useRef<HTMLDivElement>(null);
17+
18+
const handleClick = () => {
19+
if (isRecordFieldReadOnly) {
20+
setIsJsonTreeViewOpen(true);
21+
}
22+
};
23+
24+
const handleClickOutside = () => {
25+
setIsJsonTreeViewOpen(false);
26+
};
727

828
if (!isDefined(fieldValue)) {
929
return <></>;
1030
}
1131

1232
const value = JSON.stringify(fieldValue);
1333

14-
return <JsonDisplay text={value} maxWidth={maxWidth} />;
34+
return (
35+
<>
36+
<div ref={anchorRef} onClick={handleClick}>
37+
<JsonDisplay text={value} maxWidth={maxWidth} />
38+
</div>
39+
{isJsonTreeViewOpen && (
40+
<ExpandedFieldDisplay
41+
anchorElement={anchorRef.current ?? undefined}
42+
onClickOutside={handleClickOutside}
43+
>
44+
<JsonTree
45+
value={fieldValue}
46+
shouldExpandNodeInitially={isTwoFirstDepths}
47+
emptyArrayLabel={t`Empty Array`}
48+
emptyObjectLabel={t`Empty Object`}
49+
emptyStringLabel={t`[empty string]`}
50+
arrowButtonCollapsedLabel={t`Expand`}
51+
arrowButtonExpandedLabel={t`Collapse`}
52+
onNodeValueClick={copyToClipboard}
53+
/>
54+
</ExpandedFieldDisplay>
55+
)}
56+
</>
57+
);
1558
};

packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useJsonFieldDisplay.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecor
77
import { FieldContext } from '../../contexts/FieldContext';
88

99
export const useJsonFieldDisplay = () => {
10-
const { recordId, fieldDefinition, maxWidth } = useContext(FieldContext);
10+
const { recordId, fieldDefinition, maxWidth, isRecordFieldReadOnly } =
11+
useContext(FieldContext);
1112

1213
const fieldName = fieldDefinition.metadata.fieldName;
1314

@@ -25,5 +26,6 @@ export const useJsonFieldDisplay = () => {
2526
maxWidth,
2627
fieldDefinition,
2728
fieldValue: formattedFieldValue,
29+
isRecordFieldReadOnly,
2830
};
2931
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
2+
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
3+
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
4+
import styled from '@emotion/styled';
5+
import { flip, FloatingPortal, offset, useFloating } from '@floating-ui/react';
6+
import { type ReactNode } from 'react';
7+
8+
type ExpandedFieldDisplayProps = {
9+
anchorElement?: HTMLElement;
10+
children: ReactNode;
11+
onClickOutside?: () => void;
12+
};
13+
14+
const StyledExpandedFieldContainer = styled.div`
15+
display: flex;
16+
flex-direction: column;
17+
gap: ${({ theme }) => theme.spacing(1)};
18+
padding: ${({ theme }) => theme.spacing(2)};
19+
overflow: auto;
20+
box-sizing: border-box;
21+
height: 300px;
22+
width: 400px;
23+
position: relative;
24+
overflow-y: auto;
25+
`;
26+
27+
const StyledContainer = styled.div`
28+
display: flex;
29+
z-index: ${RootStackingContextZIndices.DropdownPortalBelowModal};
30+
`;
31+
32+
export const ExpandedFieldDisplay = ({
33+
anchorElement,
34+
children,
35+
onClickOutside,
36+
}: ExpandedFieldDisplayProps) => {
37+
const { refs, floatingStyles } = useFloating({
38+
placement: 'bottom-start',
39+
middleware: [
40+
flip(),
41+
offset({
42+
mainAxis: -29,
43+
crossAxis: -10,
44+
}),
45+
],
46+
elements: { reference: anchorElement },
47+
});
48+
49+
useListenClickOutside({
50+
refs: [refs.domReference, refs.floating],
51+
callback: () => {
52+
onClickOutside?.();
53+
},
54+
listenerId: 'expanded-field-display',
55+
});
56+
57+
return (
58+
<FloatingPortal>
59+
<StyledContainer
60+
ref={refs.setFloating}
61+
style={floatingStyles}
62+
data-globally-prevent-click-outside="true"
63+
>
64+
<OverlayContainer>
65+
<StyledExpandedFieldContainer>
66+
{children}
67+
</StyledExpandedFieldContainer>
68+
</OverlayContainer>
69+
</StyledContainer>
70+
</FloatingPortal>
71+
);
72+
};

0 commit comments

Comments
 (0)