Skip to content

Commit 6827978

Browse files
fix(ui): truncates long JSON cells in list view (#9214)
Original Issue: #9111 ## Reproduce Create a collection with a JSON field (or use an existing one). Add this data: ``` {"arr":[{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "},{"test":"test "}]} ``` ### Before The full JSON object is displayed: <img width="1336" height="404" alt="image" src="https://github.com/user-attachments/assets/01417d3f-d3d2-4063-9aab-7b46a5bc6070" /> ### After Now it should be properly truncated: <img width="1335" height="67" alt="image" src="https://github.com/user-attachments/assets/1cd9cd53-92c9-4aba-8c43-9b991d0ad1d5" />
1 parent a46e3a2 commit 6827978

4 files changed

Lines changed: 119 additions & 7 deletions

File tree

packages/ui/src/elements/Table/DefaultCell/fields/JSON/index.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
color: var(--theme-elevation-800);
1212
border-radius: $style-radius-m;
1313
padding: 0 base(0.25);
14+
max-width: 99.9%;
1415
[dir='ltr'] & {
1516
padding-left: base(0.0875 + 0.25);
1617
}
@@ -20,5 +21,11 @@
2021

2122
background: var(--theme-elevation-100);
2223
color: var(--theme-elevation-800);
24+
25+
span {
26+
overflow: hidden;
27+
text-overflow: ellipsis;
28+
white-space: nowrap;
29+
}
2330
}
2431
}

packages/ui/src/elements/Table/DefaultCell/fields/JSON/index.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@ import type { DefaultCellComponentProps, JSONFieldClient } from 'payload'
33

44
import React from 'react'
55

6+
import { useTranslation } from '../../../../../providers/Translation/index.js'
67
import './index.scss'
8+
import { stringifyTruncated } from '../../../../../utilities/stringifyTruncated.js'
79

810
export const JSONCell: React.FC<DefaultCellComponentProps<JSONFieldClient>> = ({ cellData }) => {
9-
const textToShow = cellData?.length > 100 ? `${cellData.substring(0, 100)}\u2026` : cellData
10-
11-
return (
12-
<code className="json-cell">
13-
<span>{JSON.stringify(textToShow)}</span>
14-
</code>
15-
)
11+
const { t } = useTranslation()
12+
try {
13+
const cellDataString = stringifyTruncated(cellData, 100)
14+
return (
15+
<code className="json-cell">
16+
<span>{cellDataString}</span>
17+
</code>
18+
)
19+
} catch (_ignore) {
20+
return (
21+
<code className="json-cell">
22+
<span>{t('general:error')}</span>
23+
</code>
24+
)
25+
}
1626
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Safely stringify a value and truncate it to a maximum length.
3+
*
4+
* Converts any value to a JSON string representation and truncates the resulting
5+
* string if it exceeds the maximum length. If truncation occurs, an ellipsis (…)
6+
* is appended to indicate incomplete output.
7+
*
8+
* @param value - The value to stringify (can be any type including objects, arrays, primitives)
9+
* @param maxLength - The maximum character length of the output string
10+
* @returns A JSON string representation, truncated with "…" if it exceeds maxLength
11+
*/
12+
export function stringifyTruncated(value: unknown, maxLength: number): string {
13+
const stringifiedJSON = JSON.stringify(value)
14+
const totalChars = stringifiedJSON.length
15+
16+
// Only truncate if the string is significantly longer (>1.5x the max length)
17+
if (totalChars / maxLength > 1.5) {
18+
return `${stringifiedJSON.substring(0, maxLength)}\u2026`
19+
}
20+
21+
return stringifiedJSON
22+
}

test/fields/collections/JSON/e2e.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,79 @@ describe('JSON', () => {
7171
await expect(jsonCell).toHaveText(JSON.stringify(jsonDoc.json))
7272
})
7373

74+
test('should truncate long JSON values in list view', async () => {
75+
// Create a document with very long JSON (>150 chars, should truncate)
76+
const longJsonData = {
77+
veryLongProperty:
78+
'This is a very long string value that will definitely exceed the 100 character universal truth when stringified.',
79+
anotherProperty: 'Additional data to ensure we exceed the limit',
80+
nested: { deep: { value: 'More nested data' } },
81+
}
82+
83+
const longDoc = await payload.create({
84+
collection: jsonFieldsSlug,
85+
data: { json: longJsonData },
86+
})
87+
88+
// Create a document with short JSON (<100 chars)
89+
const shortJsonData = { short: 'value' }
90+
91+
const shortDoc = await payload.create({
92+
collection: jsonFieldsSlug,
93+
data: { json: shortJsonData },
94+
})
95+
96+
await page.goto(url.list)
97+
98+
// Verify long JSON is truncated with ellipsis
99+
const longJsonCell = page.locator(`tr[data-id="${longDoc.id}"] .cell-json`)
100+
101+
await expect(async () => {
102+
const longCellText = await longJsonCell.textContent()
103+
expect(longCellText).toContain('…')
104+
expect(longCellText?.length).toBeLessThanOrEqual(101) // 100 chars + ellipsis
105+
}).toPass()
106+
107+
// Verify short JSON is displayed fully without truncation
108+
const shortJsonCell = page.locator(`tr[data-id="${shortDoc.id}"] .cell-json`)
109+
110+
await expect(shortJsonCell).toHaveText(JSON.stringify(shortJsonData))
111+
await expect(async () => {
112+
const shortCellText = await shortJsonCell.textContent()
113+
expect(shortCellText).not.toContain('…')
114+
}).toPass()
115+
})
116+
117+
test('should not truncate slightly long JSON values (>100 but <=150 chars)', async () => {
118+
// Create JSON that's between 100-150 chars (should NOT truncate due to 1.5x rule)
119+
// This string is ~120 characters when stringified
120+
const slightlyLongJsonData = {
121+
property1: 'This value is specifically designed to be over one hundred characters',
122+
property2: 'but under 150 total',
123+
}
124+
125+
const stringified = JSON.stringify(slightlyLongJsonData)
126+
expect(stringified.length).toBeGreaterThan(100)
127+
expect(stringified.length).toBeLessThanOrEqual(150)
128+
129+
const doc = await payload.create({
130+
collection: jsonFieldsSlug,
131+
data: { json: slightlyLongJsonData },
132+
})
133+
134+
await page.goto(url.list)
135+
136+
// Verify the JSON is displayed fully without truncation
137+
const jsonCell = page.locator(`tr[data-id="${doc.id}"] .cell-json`)
138+
139+
await expect(jsonCell).toHaveText(stringified)
140+
await expect(async () => {
141+
const cellText = jsonCell
142+
await expect(cellText).not.toContainText('…')
143+
await expect(cellText).toHaveText(stringified)
144+
}).toPass()
145+
})
146+
74147
test('should create', async () => {
75148
const input = '{"foo": "bar"}'
76149
await page.goto(url.create)

0 commit comments

Comments
 (0)