@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
1515import { resolveContentRef } from '@/lib/references' ;
1616import { tcls } from '@/lib/tailwind' ;
1717import { filterOutNullable } from '@/lib/typescript' ;
18- 1918import type { BlockProps } from '../Block' ;
2019import { Blocks } from '../Blocks' ;
2120import { FileIcon } from '../FileIcon' ;
2221import type { TableRecordKV } from './Table' ;
23- import { type VerticalAlignment , getColumnAlignment } from './utils' ;
22+ import {
23+ type VerticalAlignment ,
24+ getColumnAlignment ,
25+ isContentRef ,
26+ isDocumentTableImageRecord ,
27+ isStringArray ,
28+ } from './utils' ;
2429
2530const alignmentMap : Record < 'text-left' | 'text-center' | 'text-right' , string > = {
2631 'text-left' : '**:text-left text-left' ,
@@ -58,18 +63,28 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
5863 return null ;
5964 }
6065
66+ // Because definition and value depends on column, we have to check typing in each case at runtime.
67+ // Validation should have been done at the API level, but we can't know typing based on `definition.type`.
68+ // OpenAPI types cannot really handle discriminated unions based on a dynamic key.
6169 switch ( definition . type ) {
62- case 'checkbox' :
70+ case 'checkbox' : {
71+ if ( value === null || typeof value !== 'boolean' ) {
72+ return null ;
73+ }
6374 return (
6475 < Checkbox
6576 className = { tcls ( 'w-5' , 'h-5' ) }
66- checked = { value as boolean }
77+ checked = { value }
6778 disabled = { true }
6879 aria-labelledby = { ariaLabelledBy }
6980 />
7081 ) ;
82+ }
7183 case 'rating' : {
72- const rating = value as number ;
84+ if ( typeof value !== 'number' ) {
85+ return null ;
86+ }
87+ const rating = value ;
7388 const max = definition . max ;
7489
7590 return (
@@ -108,15 +123,21 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
108123 </ Tag >
109124 ) ;
110125 }
111- case 'number' :
126+ case 'number' : {
127+ if ( typeof value !== 'number' ) {
128+ return null ;
129+ }
112130 return (
113131 < Tag
114132 className = { tcls ( 'text-base' , 'tabular-nums' , 'tracking-tighter' ) }
115133 aria-labelledby = { ariaLabelledBy }
116134 > { `${ value } ` } </ Tag >
117135 ) ;
136+ }
118137 case 'text' : {
119- // @ts -ignore
138+ if ( typeof value !== 'string' ) {
139+ return null ;
140+ }
120141 const fragment = getNodeFragmentByName ( block , value ) ;
121142 if ( ! fragment ) {
122143 return < Tag className = { tcls ( [ 'w-full' , verticalAlignment ] ) } > { '' } </ Tag > ;
@@ -149,8 +170,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
149170 ) ;
150171 }
151172 case 'files' : {
173+ if ( ! isStringArray ( value ) ) {
174+ return null ;
175+ }
152176 const files = await Promise . all (
153- ( value as string [ ] ) . map ( ( fileId ) =>
177+ value . map ( ( fileId ) =>
154178 context . contentContext
155179 ? resolveContentRef (
156180 {
@@ -221,10 +245,12 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
221245 ) ;
222246 }
223247 case 'content-ref' : {
224- const contentRef = value ? ( value as ContentRef ) : null ;
248+ if ( value === null || ! isContentRef ( value ) ) {
249+ return null ;
250+ }
225251 const resolved =
226- contentRef && context . contentContext
227- ? await resolveContentRef ( contentRef , context . contentContext , {
252+ value && context . contentContext
253+ ? await resolveContentRef ( value , context . contentContext , {
228254 resolveAnchorText : true ,
229255 iconStyle : [ 'mr-2' , 'text-tint-subtle' ] ,
230256 } )
@@ -239,11 +265,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
239265 < StyledLink
240266 href = { resolved . href }
241267 insights = {
242- contentRef
268+ value
243269 ? {
244270 type : 'link_click' ,
245271 link : {
246- target : contentRef ,
272+ target : value ,
247273 position : SiteInsightsLinkPosition . Content ,
248274 } ,
249275 }
@@ -257,8 +283,11 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
257283 ) ;
258284 }
259285 case 'users' : {
286+ if ( ! isStringArray ( value ) ) {
287+ return null ;
288+ }
260289 const resolved = await Promise . all (
261- ( value as string [ ] ) . map ( async ( userId ) => {
290+ value . map ( async ( userId ) => {
262291 const contentRef : ContentRefUser = {
263292 kind : 'user' ,
264293 user : userId ,
@@ -295,10 +324,13 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
295324 ) ;
296325 }
297326 case 'select' : {
327+ if ( ! isStringArray ( value ) ) {
328+ return null ;
329+ }
298330 return (
299331 < Tag aria-labelledby = { ariaLabelledBy } >
300332 < span className = { tcls ( 'inline-flex' , 'gap-2' , 'flex-wrap' ) } >
301- { ( value as string [ ] ) . map ( ( selectId ) => {
333+ { value . map ( ( selectId ) => {
302334 const option = definition . options . find (
303335 ( option ) => option . value === selectId
304336 ) ;
@@ -329,8 +361,15 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
329361 ) ;
330362 }
331363 case 'image' : {
364+ if ( ! isDocumentTableImageRecord ( value ) ) {
365+ return null ;
366+ }
367+ 332368 const image = context . contentContext
333- ? await resolveContentRef ( value as ContentRef , context . contentContext )
369+ ? await resolveContentRef (
370+ 'ref' in value ? value . ref : value ,
371+ context . contentContext
372+ )
334373 : null ;
335374
336375 if ( ! image ) {
0 commit comments