Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 61d1a01

Browse files
Fix null contentRef handling in RecordColumnValue (#3566)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
1 parent f9a2977 commit 61d1a01

File tree

2 files changed

+87
-16
lines changed

2 files changed

+87
-16
lines changed

‎packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx‎

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@ import { getSimplifiedContentType } from '@/lib/files';
1515
import { resolveContentRef } from '@/lib/references';
1616
import { tcls } from '@/lib/tailwind';
1717
import { filterOutNullable } from '@/lib/typescript';
18-
1918
import type { BlockProps } from '../Block';
2019
import { Blocks } from '../Blocks';
2120
import { FileIcon } from '../FileIcon';
2221
import 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

2530
const 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={valueasboolean}
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-
(valueasstring[]).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-
(valueasstring[]).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-
{(valueasstring[]).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) {

‎packages/gitbook/src/components/DocumentView/Table/utils.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,35 @@ export function getColumnVerticalAlignment(column: DocumentTableDefinition): Ver
123123

124124
return 'self-center';
125125
}
126+
127+
/**
128+
* Check if a value is a ContentRef.
129+
* @param ref The value to check.
130+
* @returns True if the value is a ContentRef, false otherwise.
131+
*/
132+
export function isContentRef(ref?: DocumentTableRecord['values'][string]): ref is ContentRef {
133+
return Boolean(ref && typeof ref === 'object' && 'kind' in ref);
134+
}
135+
136+
/**
137+
* Check if a value is an array of strings.
138+
* @param value The value to check.
139+
* @returns True if the value is an array of strings, false otherwise.
140+
*/
141+
export function isStringArray(value?: DocumentTableRecord['values'][string]): value is string[] {
142+
return Array.isArray(value) && value.every((v) => typeof v === 'string');
143+
}
144+
145+
/**
146+
* Check if a value is a DocumentTableImageRecord.
147+
* @param value The value to check.
148+
* @returns True if the value is a DocumentTableImageRecord, false otherwise.
149+
*/
150+
export function isDocumentTableImageRecord(
151+
value?: DocumentTableRecord['values'][string]
152+
): value is DocumentTableImageRecord {
153+
if (isContentRef(value) && (value.kind === 'file' || value.kind === 'url')) {
154+
return true;
155+
}
156+
return Boolean(value && typeof value === 'object' && 'ref' in value && isContentRef(value.ref));
157+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /