Skip to content

Commit e86097f

Browse files
committed
Add optional Cell component to fields for custom collection list rendering
Fields can now define an optional Cell component to control how their values are rendered in collection list views. The image field provides a default Cell that renders 32x32 rounded thumbnails with a fallback to displaying the path string if the image fails to load.
1 parent ad0c4b2 commit e86097f

5 files changed

Lines changed: 69 additions & 11 deletions

File tree

packages/keystatic/src/app/CollectionPage.tsx

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -565,17 +565,42 @@ function CollectionTable(
565565
...(hideStatusColumn ? [] : [statusCell]),
566566
nameCell,
567567
...collection.columns.map(column => {
568-
let val;
569-
val = item.data?.[column];
568+
const raw = item.data?.[column];
569+
const field = collection.schema[column];
570+
const FieldCell =
571+
field &&
572+
'kind' in field &&
573+
field.kind === 'form' &&
574+
'Cell' in field &&
575+
field.Cell;
576+
577+
if (raw == null) {
578+
return (
579+
<Cell
580+
key={column + item.name}
581+
textValue=""
582+
>
583+
<Text weight="medium">{undefined}</Text>
584+
</Cell>
585+
);
586+
}
570587

571-
if (val == null) {
572-
val = undefined;
573-
} else {
574-
val = val + '';
588+
const strVal = raw + '';
589+
590+
if (FieldCell) {
591+
return (
592+
<Cell
593+
key={column + item.name}
594+
textValue={strVal}
595+
>
596+
<FieldCell value={raw} />
597+
</Cell>
598+
);
575599
}
600+
576601
return (
577-
<Cell key={column + item.name} textValue={val}>
578-
<Text weight="medium">{val}</Text>
602+
<Cell key={column + item.name} textValue={strVal}>
603+
<Text weight="medium">{strVal}</Text>
579604
</Cell>
580605
);
581606
}),
@@ -597,6 +622,7 @@ function CollectionTable(
597622
);
598623
}
599624

625+
600626
function getItemPath(
601627
basePath: string,
602628
collection: string,

packages/keystatic/src/form/api.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReactElement, ReactNode } from 'react';
1+
import { ReactElement, ReactNode, ComponentType } from 'react';
22
import { Glob } from '../config';
33

44
import { ChildField } from './fields/child';
@@ -48,6 +48,7 @@ export type BasicFormField<
4848
parse(value: FormFieldStoredValue): ReaderValue;
4949
};
5050
label?: string;
51+
Cell?: ComponentType<{ value: any }>;
5152
};
5253

5354
export type SlugFormField<
@@ -85,6 +86,7 @@ export type SlugFormField<
8586
): ReaderValueAsSlugField;
8687
};
8788
label?: string;
89+
Cell?: ComponentType<{ value: any }>;
8890
};
8991

9092
export type AssetFormField<
@@ -127,6 +129,7 @@ export type AssetFormField<
127129
parse(value: FormFieldStoredValue): ReaderValue;
128130
};
129131
label?: string;
132+
Cell?: ComponentType<{ value: any }>;
130133
};
131134

132135
export type AssetsFormField<

packages/keystatic/src/form/fields/empty-field-ui.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export let SlugFieldInput = empty,
1919
IntegerFieldInput = empty,
2020
NumberFieldInput = empty,
2121
ImageFieldInput = empty,
22+
ImageCell = empty,
2223
FileFieldInput = empty,
2324
DatetimeFieldInput = empty,
2425
DateFieldInput = empty,

packages/keystatic/src/form/fields/image/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AssetFormField } from '../../api';
33
import { FieldDataError } from '../error';
44
import { RequiredValidation, assertRequired } from '../utils';
55
import { getSrcPrefix } from './getSrcPrefix';
6-
import { ImageFieldInput } from '#field-ui/image';
6+
import { ImageFieldInput, ImageCell } from '#field-ui/image';
77

88
export function image<IsRequired extends boolean | undefined>({
99
label,
@@ -34,6 +34,7 @@ export function image<IsRequired extends boolean | undefined>({
3434
kind: 'form',
3535
formKind: 'asset',
3636
label,
37+
Cell: ImageCell,
3738
Input(props) {
3839
return (
3940
<ImageFieldInput

packages/keystatic/src/form/fields/image/ui.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ButtonGroup, ActionButton } from '@keystar/ui/button';
22
import { FieldDescription, FieldLabel, FieldMessage } from '@keystar/ui/field';
33
import { Flex, Box } from '@keystar/ui/layout';
4-
import { tokenSchema } from '@keystar/ui/style';
4+
import { css, tokenSchema } from '@keystar/ui/style';
5+
import { Text } from '@keystar/ui/typography';
56
import { TextField } from '@keystar/ui/text-field';
67

78
import { useIsInDocumentEditor } from '../document/DocumentEditor';
@@ -171,3 +172,29 @@ export function ImageFieldInput(
171172
</Flex>
172173
);
173174
}
175+
176+
export function ImageCell({ value }: { value: string | null }) {
177+
const [errored, setErrored] = useState(false);
178+
if (!value) return null;
179+
if (errored) {
180+
return (
181+
<Text color="neutralTertiary" size="small">
182+
{value}
183+
</Text>
184+
);
185+
}
186+
return (
187+
<img
188+
src={value}
189+
alt=""
190+
onError={() => setErrored(true)}
191+
className={css({
192+
width: tokenSchema.size.scale[400],
193+
height: tokenSchema.size.scale[400],
194+
objectFit: 'cover',
195+
borderRadius: tokenSchema.size.radius.small,
196+
flexShrink: 0,
197+
})}
198+
/>
199+
);
200+
}

0 commit comments

Comments
 (0)