Skip to content

Commit 62f07bc

Browse files
committed
Refactor image thumbnails to use optional Cell component on fields
Replace special-case image detection in CollectionPage with a generic Cell component that any field can define. The image field now provides its own ImageCell for thumbnail rendering in collection list views.
1 parent 00f0dee commit 62f07bc

5 files changed

Lines changed: 44 additions & 59 deletions

File tree

packages/keystatic/src/app/CollectionPage.tsx

Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import {
3838
import { Heading, Text } from '@keystar/ui/typography';
3939

4040
import { Config } from '../config';
41-
import { ComponentSchema } from '../form/api';
4241
import { sortBy } from './collection-sort';
4342
import l10nMessages from './l10n';
4443
import { useRouter } from './router';
@@ -567,10 +566,13 @@ function CollectionTable(
567566
nameCell,
568567
...collection.columns.map(column => {
569568
const raw = item.data?.[column];
570-
const isImage = isImageColumn(
571-
collection.schema,
572-
column
573-
);
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;
574576

575577
if (raw == null) {
576578
return (
@@ -585,13 +587,13 @@ function CollectionTable(
585587

586588
const strVal = raw + '';
587589

588-
if (isImage && isImagePath(strVal)) {
590+
if (FieldCell) {
589591
return (
590592
<Cell
591593
key={column + item.name}
592594
textValue={strVal}
593595
>
594-
<ImageThumbnail src={strVal} />
596+
<FieldCell value={raw} />
595597
</Cell>
596598
);
597599
}
@@ -620,55 +622,6 @@ function CollectionTable(
620622
);
621623
}
622624

623-
const IMAGE_EXTENSIONS = new Set([
624-
'jpg',
625-
'jpeg',
626-
'png',
627-
'gif',
628-
'webp',
629-
'avif',
630-
'svg',
631-
'ico',
632-
'bmp',
633-
]);
634-
635-
function isImagePath(value: string): boolean {
636-
const ext = value.split('.').pop()?.toLowerCase();
637-
return ext != null && IMAGE_EXTENSIONS.has(ext);
638-
}
639-
640-
function isImageColumn(
641-
schema: Record<string, ComponentSchema>,
642-
columnKey: string
643-
): boolean {
644-
const field = schema[columnKey];
645-
return field != null && 'formKind' in field && field.formKind === 'asset';
646-
}
647-
648-
function ImageThumbnail({ src }: { src: string }) {
649-
const [errored, setErrored] = useState(false);
650-
if (errored) {
651-
return (
652-
<Text color="neutralTertiary" size="small">
653-
{src}
654-
</Text>
655-
);
656-
}
657-
return (
658-
<img
659-
src={src}
660-
alt=""
661-
onError={() => setErrored(true)}
662-
className={css({
663-
width: tokenSchema.size.scale[400],
664-
height: tokenSchema.size.scale[400],
665-
objectFit: 'cover',
666-
borderRadius: tokenSchema.size.radius.small,
667-
flexShrink: 0,
668-
})}
669-
/>
670-
);
671-
}
672625

673626
function getItemPath(
674627
basePath: 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)