@@ -42,6 +42,13 @@ export interface ContentListSort {
4242 direction : "asc" | "desc" ;
4343}
4444
45+ export interface ContentListColumn {
46+ slug : string ;
47+ label : string ;
48+ kind : string ;
49+ options ?: Array < { value : string ; label : string } > ;
50+ }
51+
4552/** Status filter values. `"all"` clears the status filter. */
4653export type ContentStatusFilter = "all" | "published" | "draft" | "scheduled" | "archived" ;
4754
@@ -63,6 +70,8 @@ export interface ContentListProps {
6370 collection : string ;
6471 collectionLabel : string ;
6572 items : ContentItem [ ] ;
73+ /** Validated custom-field columns from the collection manifest. */
74+ listColumns ?: ContentListColumn [ ] ;
6675 trashedItems ?: TrashedContentItem [ ] ;
6776 isLoading ?: boolean ;
6877 isTrashedLoading ?: boolean ;
@@ -157,6 +166,7 @@ export function ContentList({
157166 collection,
158167 collectionLabel,
159168 items,
169+ listColumns = [ ] ,
160170 trashedItems = [ ] ,
161171 isLoading,
162172 isTrashedLoading,
@@ -315,7 +325,7 @@ export function ContentList({
315325 }
316326 } ) ( ) ;
317327 } ;
318- const colSpan = ( i18n ? 5 : 4 ) + ( bulkEnabled ? 1 : 0 ) ;
328+ const colSpan = ( i18n ? 5 : 4 ) + listColumns . length + ( bulkEnabled ? 1 : 0 ) ;
319329
320330 return (
321331 < div className = "space-y-4" >
@@ -504,6 +514,15 @@ export function ContentList({
504514 onSortChange = { onSortChange }
505515 label = { t `Title` }
506516 />
517+ { listColumns . map ( ( column ) => (
518+ < th
519+ key = { column . slug }
520+ scope = "col"
521+ className = "px-4 py-3 text-start text-sm font-medium"
522+ >
523+ { column . label }
524+ </ th >
525+ ) ) }
507526 < SortableTh
508527 field = "status"
509528 sort = { sort }
@@ -575,6 +594,7 @@ export function ContentList({
575594 onDuplicate = { onDuplicate }
576595 showLocale = { ! ! i18n }
577596 urlPattern = { urlPattern }
597+ listColumns = { listColumns }
578598 selectable = { bulkEnabled }
579599 selected = { selectedIds . has ( item . id ) }
580600 onToggleSelect = { toggleOne }
@@ -943,6 +963,7 @@ interface ContentListItemProps {
943963 onDuplicate ?: ( id : string ) => void ;
944964 showLocale ?: boolean ;
945965 urlPattern ?: string ;
966+ listColumns : ContentListColumn [ ] ;
946967 selectable ?: boolean ;
947968 selected ?: boolean ;
948969 onToggleSelect ?: ( id : string ) => void ;
@@ -955,6 +976,7 @@ function ContentListItem({
955976 onDuplicate,
956977 showLocale,
957978 urlPattern,
979+ listColumns,
958980 selectable,
959981 selected,
960982 onToggleSelect,
@@ -984,6 +1006,9 @@ function ContentListItem({
9841006 { title }
9851007 </ Link >
9861008 </ td >
1009+ { listColumns . map ( ( column ) => (
1010+ < ContentListCustomCell key = { column . slug } column = { column } value = { item . data [ column . slug ] } />
1011+ ) ) }
9871012 < td className = "px-4 py-3" >
9881013 < StatusBadge
9891014 status = { item . status }
@@ -1071,6 +1096,74 @@ function ContentListItem({
10711096 ) ;
10721097}
10731098
1099+ function ContentListCustomCell ( {
1100+ column,
1101+ value,
1102+ } : {
1103+ column : ContentListColumn ;
1104+ value : unknown ;
1105+ } ) : React . ReactNode {
1106+ const text = formatListColumnValue ( column , value ) ;
1107+ return (
1108+ < td className = "max-w-48 px-4 py-3 text-sm" >
1109+ < span className = "block truncate" title = { text } >
1110+ { text }
1111+ </ span >
1112+ </ td >
1113+ ) ;
1114+ }
1115+
1116+ function formatListColumnValue ( column : ContentListColumn , value : unknown ) : string {
1117+ if ( value === null || value === undefined || value === "" ) return "—" ;
1118+
1119+ const optionLabel = ( optionValue : unknown ) : string => {
1120+ const text = scalarListColumnValue ( optionValue ) ;
1121+ if ( text === undefined ) return "—" ;
1122+ return column . options ?. find ( ( option ) => option . value === text ) ?. label ?? text ;
1123+ } ;
1124+
1125+ switch ( column . kind ) {
1126+ case "select" :
1127+ return optionLabel ( value ) ;
1128+ case "multiSelect" : {
1129+ let values : unknown [ ] ;
1130+ if ( Array . isArray ( value ) ) {
1131+ values = value ;
1132+ } else if ( typeof value === "string" ) {
1133+ try {
1134+ const parsed : unknown = JSON . parse ( value ) ;
1135+ values = Array . isArray ( parsed ) ? parsed : [ value ] ;
1136+ } catch {
1137+ values = [ value ] ;
1138+ }
1139+ } else {
1140+ values = [ value ] ;
1141+ }
1142+ return values . length > 0 ? values . map ( optionLabel ) . join ( ", " ) : "—" ;
1143+ }
1144+ case "boolean" :
1145+ return value === true || value === 1 || value === "1" || value === "true" ? "✓" : "—" ;
1146+ case "datetime" : {
1147+ const text = scalarListColumnValue ( value ) ;
1148+ if ( text === undefined ) return "—" ;
1149+ const date = new Date ( text ) ;
1150+ return Number . isNaN ( date . getTime ( ) ) ? text : date . toLocaleDateString ( ) ;
1151+ }
1152+ case "number" :
1153+ case "string" :
1154+ default :
1155+ return scalarListColumnValue ( value ) ?? "—" ;
1156+ }
1157+ }
1158+
1159+ function scalarListColumnValue ( value : unknown ) : string | undefined {
1160+ if ( typeof value === "string" ) return value ;
1161+ if ( typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" ) {
1162+ return String ( value ) ;
1163+ }
1164+ return undefined ;
1165+ }
1166+
10741167interface TrashedListItemProps {
10751168 item : TrashedContentItem ;
10761169 onRestore ?: ( id : string ) => void ;
0 commit comments