Skip to content

Commit cad86f3

Browse files
committed
Fix issue where zero value is not displayed in list summary
1 parent 0676cbb commit cad86f3

3 files changed

Lines changed: 343 additions & 2 deletions

File tree

src/lib/services/contents/entry/fields.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ export const getFieldDisplayValue = ({
238238
value = applyTransformations({ fieldConfig, value, transformations });
239239
}
240240

241-
return value ? String(value) : '';
241+
// Return an empty string if the value is null or undefined
242+
return String(value ?? '');
242243
};
243244

244245
/**

src/lib/services/contents/entry/fields.test.js

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getCollection, isEntryCollection } from '$lib/services/contents/collect
44
import {
55
fieldConfigCacheMap,
66
getField,
7+
getFieldDisplayValue,
78
isFieldRequired,
89
} from '$lib/services/contents/entry/fields';
910

@@ -1127,3 +1128,338 @@ describe('Test isFieldRequired()', () => {
11271128
expect(isFieldRequired({ fieldConfig: { name, required: [] }, locale })).toBe(false);
11281129
});
11291130
});
1131+
1132+
describe('Test getFieldDisplayValue()', () => {
1133+
const mockCollection = {
1134+
name: 'posts',
1135+
folder: 'content/posts',
1136+
_type: 'entry',
1137+
fields: [
1138+
{ name: 'title', widget: 'string' },
1139+
{ name: 'body', widget: 'markdown' },
1140+
{ name: 'published', widget: 'boolean' },
1141+
{ name: 'publishDate', widget: 'datetime', format: 'YYYY-MM-DD' },
1142+
{
1143+
name: 'author',
1144+
widget: 'relation',
1145+
collection: 'authors',
1146+
value_field: 'name',
1147+
display_fields: ['name', 'email'],
1148+
},
1149+
{
1150+
name: 'category',
1151+
widget: 'select',
1152+
options: [
1153+
{ label: 'Blog', value: 'blog' },
1154+
{ label: 'News', value: 'news' },
1155+
],
1156+
},
1157+
{
1158+
name: 'simpleTags',
1159+
widget: 'list',
1160+
// No field, fields, or types - this makes it a simple list
1161+
},
1162+
{
1163+
name: 'tags',
1164+
widget: 'list',
1165+
field: { name: 'tag', widget: 'string' },
1166+
},
1167+
{
1168+
name: 'images',
1169+
widget: 'list',
1170+
fields: [
1171+
{ name: 'src', widget: 'image' },
1172+
{ name: 'alt', widget: 'string' },
1173+
],
1174+
},
1175+
],
1176+
};
1177+
1178+
beforeEach(() => {
1179+
fieldConfigCacheMap.clear();
1180+
vi.clearAllMocks();
1181+
// @ts-expect-error - Simplified mock for testing
1182+
mockGetCollection.mockReturnValue(mockCollection);
1183+
});
1184+
1185+
afterEach(() => {
1186+
vi.restoreAllMocks();
1187+
fieldConfigCacheMap.clear();
1188+
});
1189+
1190+
describe('Basic value handling', () => {
1191+
test('should return string representation of primitive values', () => {
1192+
const valueMap = {
1193+
title: 'Hello World',
1194+
published: true,
1195+
count: 42,
1196+
rating: 4.5,
1197+
};
1198+
1199+
expect(
1200+
getFieldDisplayValue({
1201+
collectionName: 'posts',
1202+
valueMap,
1203+
keyPath: 'title',
1204+
locale: 'en',
1205+
}),
1206+
).toBe('Hello World');
1207+
1208+
expect(
1209+
getFieldDisplayValue({
1210+
collectionName: 'posts',
1211+
valueMap,
1212+
keyPath: 'published',
1213+
locale: 'en',
1214+
}),
1215+
).toBe('true');
1216+
1217+
expect(
1218+
getFieldDisplayValue({
1219+
collectionName: 'posts',
1220+
valueMap,
1221+
keyPath: 'count',
1222+
locale: 'en',
1223+
}),
1224+
).toBe('42');
1225+
1226+
expect(
1227+
getFieldDisplayValue({
1228+
collectionName: 'posts',
1229+
valueMap,
1230+
keyPath: 'rating',
1231+
locale: 'en',
1232+
}),
1233+
).toBe('4.5');
1234+
});
1235+
1236+
test('should return empty string for null and undefined values', () => {
1237+
const valueMap = {
1238+
nullValue: null,
1239+
// undefinedValue is not set
1240+
};
1241+
1242+
expect(
1243+
getFieldDisplayValue({
1244+
collectionName: 'posts',
1245+
valueMap,
1246+
keyPath: 'nullValue',
1247+
locale: 'en',
1248+
}),
1249+
).toBe('');
1250+
1251+
expect(
1252+
getFieldDisplayValue({
1253+
collectionName: 'posts',
1254+
valueMap,
1255+
keyPath: 'undefinedValue',
1256+
locale: 'en',
1257+
}),
1258+
).toBe('');
1259+
});
1260+
1261+
test('should return empty string for false boolean value', () => {
1262+
const valueMap = {
1263+
published: false,
1264+
};
1265+
1266+
expect(
1267+
getFieldDisplayValue({
1268+
collectionName: 'posts',
1269+
valueMap,
1270+
keyPath: 'published',
1271+
locale: 'en',
1272+
}),
1273+
).toBe('false');
1274+
});
1275+
1276+
test('should return empty string for zero value', () => {
1277+
const valueMap = {
1278+
count: 0,
1279+
};
1280+
1281+
expect(
1282+
getFieldDisplayValue({
1283+
collectionName: 'posts',
1284+
valueMap,
1285+
keyPath: 'count',
1286+
locale: 'en',
1287+
}),
1288+
).toBe('0');
1289+
});
1290+
1291+
test('should return empty string for empty string value', () => {
1292+
const valueMap = {
1293+
title: '',
1294+
};
1295+
1296+
expect(
1297+
getFieldDisplayValue({
1298+
collectionName: 'posts',
1299+
valueMap,
1300+
keyPath: 'title',
1301+
locale: 'en',
1302+
}),
1303+
).toBe('');
1304+
});
1305+
});
1306+
1307+
describe('Array value handling', () => {
1308+
test('should format array values using list formatter', () => {
1309+
const valueMap = {
1310+
someArray: ['javascript', 'web development', 'tutorial'],
1311+
};
1312+
1313+
const result = getFieldDisplayValue({
1314+
collectionName: 'posts',
1315+
valueMap,
1316+
keyPath: 'someArray',
1317+
locale: 'en',
1318+
});
1319+
1320+
// List formatter typically joins with commas and "and"
1321+
expect(result).toContain('javascript');
1322+
expect(result).toContain('web development');
1323+
expect(result).toContain('tutorial');
1324+
});
1325+
1326+
test('should return empty string for empty array', () => {
1327+
const valueMap = {
1328+
someArray: [],
1329+
};
1330+
1331+
expect(
1332+
getFieldDisplayValue({
1333+
collectionName: 'posts',
1334+
valueMap,
1335+
keyPath: 'someArray',
1336+
locale: 'en',
1337+
}),
1338+
).toBe('');
1339+
});
1340+
});
1341+
1342+
describe('List widget handling', () => {
1343+
test('should format simple list values', () => {
1344+
const valueMap = {
1345+
'simpleTags.0': 'javascript',
1346+
'simpleTags.1': 'web development',
1347+
'simpleTags.2': 'tutorial',
1348+
};
1349+
1350+
const result = getFieldDisplayValue({
1351+
collectionName: 'posts',
1352+
valueMap,
1353+
keyPath: 'simpleTags',
1354+
locale: 'en',
1355+
});
1356+
1357+
expect(result).toContain('javascript');
1358+
expect(result).toContain('web development');
1359+
expect(result).toContain('tutorial');
1360+
});
1361+
1362+
test('should ignore complex list widgets (with fields or types)', () => {
1363+
const valueMap = {
1364+
'images.0.src': 'image1.jpg',
1365+
'images.0.alt': 'First image',
1366+
'images.1.src': 'image2.jpg',
1367+
'images.1.alt': 'Second image',
1368+
};
1369+
1370+
const result = getFieldDisplayValue({
1371+
collectionName: 'posts',
1372+
valueMap,
1373+
keyPath: 'images',
1374+
locale: 'en',
1375+
});
1376+
1377+
// Complex list widgets should not be formatted as simple lists
1378+
expect(result).toBe('');
1379+
});
1380+
1381+
test('should format list widgets with field property', () => {
1382+
const valueMap = {
1383+
'tags.0': 'javascript',
1384+
'tags.1': 'web development',
1385+
};
1386+
1387+
const result = getFieldDisplayValue({
1388+
collectionName: 'posts',
1389+
valueMap,
1390+
keyPath: 'tags',
1391+
locale: 'en',
1392+
});
1393+
1394+
// List widgets with field property should be formatted as simple lists
1395+
expect(result).toContain('javascript');
1396+
expect(result).toContain('web development');
1397+
});
1398+
});
1399+
1400+
describe('Transformations', () => {
1401+
test('should apply transformations when provided', () => {
1402+
const valueMap = {
1403+
title: 'hello world',
1404+
};
1405+
1406+
const result = getFieldDisplayValue({
1407+
collectionName: 'posts',
1408+
valueMap,
1409+
keyPath: 'title',
1410+
locale: 'en',
1411+
transformations: ['upper'],
1412+
});
1413+
1414+
expect(result).toBe('HELLO WORLD');
1415+
});
1416+
1417+
test('should return empty string when field is undefined and transformations are applied', () => {
1418+
const valueMap = {};
1419+
1420+
const result = getFieldDisplayValue({
1421+
collectionName: 'posts',
1422+
valueMap,
1423+
keyPath: 'nonexistent',
1424+
locale: 'en',
1425+
transformations: ['upper'],
1426+
});
1427+
1428+
expect(result).toBe('');
1429+
});
1430+
});
1431+
1432+
describe('Edge cases', () => {
1433+
test('should handle non-existent collection', () => {
1434+
mockGetCollection.mockReturnValue(undefined);
1435+
1436+
const valueMap = {
1437+
title: 'Hello World',
1438+
};
1439+
1440+
const result = getFieldDisplayValue({
1441+
collectionName: 'nonexistent',
1442+
valueMap,
1443+
keyPath: 'title',
1444+
locale: 'en',
1445+
});
1446+
1447+
expect(result).toBe('Hello World');
1448+
});
1449+
1450+
test('should handle non-existent field config', () => {
1451+
const valueMap = {
1452+
unknownField: 'some value',
1453+
};
1454+
1455+
const result = getFieldDisplayValue({
1456+
collectionName: 'posts',
1457+
valueMap,
1458+
keyPath: 'unknownField',
1459+
locale: 'en',
1460+
});
1461+
1462+
expect(result).toBe('some value');
1463+
});
1464+
});
1465+
});

src/lib/services/contents/entry/summary.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { parseEntities } from 'parse-entities';
77
import {
88
applyTransformations,
99
DATE_TRANSFORMATION_REGEX,
10+
TERNARY_TRANSFORMATION_REGEX,
1011
} from '$lib/services/common/transformations';
1112
import { getIndexFile, isCollectionIndexFile } from '$lib/services/contents/collection/index-file';
1213
import { getField, getFieldDisplayValue } from '$lib/services/contents/entry/fields';
@@ -141,7 +142,10 @@ export const replace = (placeholder, context) => {
141142
// If the `date` transformation is defined, e.g. `{{publish_date | date('YYYY-MM')}}`, use the
142143
// raw field value from the entry content. Otherwise, use the field display value. This is to
143144
// avoid applying the transformation to the display value, which leads to unexpected results.
144-
value = transformations.some((t) => DATE_TRANSFORMATION_REGEX.test(t))
145+
// Also use raw value for ternary transformations to preserve boolean truthiness.
146+
value = transformations.some(
147+
(t) => DATE_TRANSFORMATION_REGEX.test(t) || TERNARY_TRANSFORMATION_REGEX.test(t),
148+
)
145149
? valueMap[keyPath]
146150
: getFieldDisplayValue({ ...getFieldArgs, locale: defaultLocale });
147151
}

0 commit comments

Comments
 (0)