Last active
August 15, 2024 22:19
-
-
Save proteye/b9fc465caeea474fef3873b7297d58ad to your computer and use it in GitHub Desktop.
Image uploading in KeystoneJS document editor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React from 'react' | |
import { NotEditable, component, fields, FormField } from '@keystone-6/fields-document/component-blocks' | |
import { TAny } from '../types' | |
import { TImageFieldData, TImageFieldOptions, TImageFieldValue } from './types' | |
import { ImageUploader } from './components/ImageUploader' | |
const customFields = { | |
image({ listKey }: TImageFieldOptions): FormField<TImageFieldValue, TImageFieldOptions> { | |
return { | |
kind: 'form', | |
Input({ value, onChange }) { | |
return <ImageUploader listKey={listKey} defaultValue={value} mode="edit" onChange={onChange} /> | |
}, | |
options: { listKey }, | |
defaultValue: null, | |
validate(value) { | |
return typeof value === 'object' | |
}, | |
} | |
}, | |
} | |
export const componentBlocks: TAny = { | |
image: component({ | |
preview: ({ fields }) => ( | |
<NotEditable> | |
<ImageUploader | |
listKey={fields.image.options.listKey} | |
defaultValue={fields.imageRel.value?.data as TImageFieldData} | |
imageAlt={fields.imageAlt.value} | |
onChange={fields.image.onChange} | |
onImageAltChange={fields.imageAlt.onChange} | |
onRelationChange={fields.imageRel.onChange} | |
/> | |
</NotEditable> | |
), | |
label: 'Image', | |
schema: { | |
imageAlt: fields.text({ | |
label: 'Image Alt', | |
defaultValue: '', | |
}), | |
image: customFields.image({ | |
listKey: 'Image', | |
}), | |
imageRel: fields.relationship({ | |
listKey: 'Image', | |
label: 'Image Relation', | |
selection: 'id, image { url }', | |
}), | |
}, | |
chromeless: true, | |
}), | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsxRuntime classic */ | |
/** @jsx jsx */ | |
import { jsx } from '@keystone-ui/core' | |
import { FC } from 'react' | |
import styles from './styles' | |
import { IImageUploaderProps } from './types' | |
import useBase from './useBase' | |
export const ImageUploader: FC<IImageUploaderProps> = (props) => { | |
const { altText, imageSrc, loading, isShowLabel, isShowImage, handleAltTextChange, handleUploadChange } = | |
useBase(props) | |
const { mode } = props | |
return ( | |
<div css={styles.container(mode)}> | |
<label id="file" css={styles.imageUploader(isShowImage)}> | |
{isShowLabel && <span>🖱 Click to select a file...</span>} | |
{loading && <span>Loading...</span>} | |
<input | |
autoComplete="off" | |
type="file" | |
accept={'image/*'} | |
style={{ display: 'none' }} | |
onChange={handleUploadChange} | |
/> | |
<img | |
src={imageSrc} | |
alt={altText} | |
css={styles.imagePreview} | |
style={{ display: isShowImage ? 'block' : 'none' }} | |
/> | |
</label> | |
{mode === 'preview' && ( | |
<div css={styles.inputWrapper}> | |
<label>Image Alt:</label> | |
<input type="text" placeholder="" css={styles.textInput} value={altText} onChange={handleAltTextChange} /> | |
</div> | |
)} | |
</div> | |
) | |
} | |
ImageUploader.defaultProps = { | |
defaultValue: null, | |
imageAlt: '', | |
mode: 'preview', | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @jsxRuntime classic */ | |
/** @jsx jsx */ | |
import { jsx } from '@keystone-ui/core' | |
import { FC } from 'react' | |
import styles from './styles' | |
import { IImageUploaderProps } from './types' | |
import useBase from './useBase' | |
export const ImageUploader: FC<IImageUploaderProps> = (props) => { | |
const { altText, imageSrc, loading, isShowLabel, isShowImage, handleAltTextChange, handleUploadChange } = | |
useBase(props) | |
const { mode } = props | |
return ( | |
<div css={styles.container(mode)}> | |
<label id="file" css={styles.imageUploader(isShowImage)}> | |
{isShowLabel && <span>🖱 Click to select a file...</span>} | |
{loading && <span>Loading...</span>} | |
<input | |
autoComplete="off" | |
type="file" | |
accept={'image/*'} | |
style={{ display: 'none' }} | |
onChange={handleUploadChange} | |
/> | |
<img | |
src={imageSrc} | |
alt={altText} | |
css={styles.imagePreview} | |
style={{ display: isShowImage ? 'block' : 'none' }} | |
/> | |
</label> | |
{mode === 'preview' && ( | |
<div css={styles.inputWrapper}> | |
<label>Image Alt:</label> | |
<input type="text" placeholder="" css={styles.textInput} value={altText} onChange={handleAltTextChange} /> | |
</div> | |
)} | |
</div> | |
) | |
} | |
ImageUploader.defaultProps = { | |
defaultValue: null, | |
imageAlt: '', | |
mode: 'preview', | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { HydratedRelationshipData } from '@keystone-6/fields-document/dist/declarations/src/DocumentEditor/component-blocks/api' | |
import { TImageFieldValue } from '../../types' | |
export interface IImageUploaderProps { | |
listKey: string | |
defaultValue?: TImageFieldValue | |
imageAlt?: string | |
mode?: 'edit' | 'preview' | |
onChange?(value: TImageFieldValue): void | |
onImageAltChange?(value: string): void | |
onRelationChange?(value: HydratedRelationshipData): void | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ChangeEvent, useCallback, useState } from 'react' | |
import { useMutation, gql } from '@keystone-6/core/admin-ui/apollo' | |
import { useList } from '@keystone-6/core/admin-ui/context' | |
import { useToasts } from '@keystone-ui/toast' | |
import { IImageUploaderProps } from './types' | |
const useBase = ({ | |
listKey, | |
defaultValue, | |
imageAlt, | |
onChange, | |
onImageAltChange, | |
onRelationChange, | |
}: IImageUploaderProps) => { | |
const [altText, setAltText] = useState(imageAlt ?? '') | |
const [imageSrc, setImageSrc] = useState(defaultValue?.image?.url ?? '') | |
const list = useList(listKey) | |
const toasts = useToasts() | |
const UPLOAD_IMAGE = gql` | |
mutation ${list.gqlNames.createMutationName}($file: Upload!) { | |
${list.gqlNames.createMutationName}(data: { image: { upload: $file } }) { | |
id, name, type, image { id, extension, filesize, height, width, url } | |
} | |
} | |
` | |
const [uploadImage, { loading }] = useMutation(UPLOAD_IMAGE) | |
const uploadFile = useCallback( | |
async (file: File) => { | |
try { | |
return await uploadImage({ | |
variables: { file }, | |
}) | |
} catch (err: any) { | |
toasts.addToast({ | |
title: `Failed to upload file: ${file.name}`, | |
tone: 'negative', | |
message: err.message, | |
}) | |
} | |
return null | |
}, | |
[toasts, uploadImage], | |
) | |
const handleAltTextChange = useCallback( | |
async (e: ChangeEvent<HTMLInputElement>) => { | |
const { value } = e.currentTarget | |
setAltText(value) | |
onImageAltChange?.(value) | |
}, | |
[onImageAltChange], | |
) | |
const handleUploadChange = useCallback( | |
async (e: ChangeEvent<HTMLInputElement>) => { | |
const selectedFile = e.currentTarget.files?.[0] | |
const src = selectedFile ? URL.createObjectURL(selectedFile) : '' | |
setImageSrc(src) | |
if (selectedFile) { | |
const result = await uploadFile(selectedFile) | |
const uploadedImage = result?.data?.createImage | |
onChange?.({ id: uploadedImage.id }) | |
if (onRelationChange) { | |
setTimeout( | |
() => onRelationChange({ id: uploadedImage.id, label: uploadedImage.name, data: uploadedImage }), | |
0, | |
) | |
} | |
} | |
}, | |
[onChange, onRelationChange], | |
) | |
return { | |
altText, | |
imageSrc, | |
loading, | |
isShowLabel: !loading && !imageSrc, | |
isShowImage: !loading && !!imageSrc, | |
handleAltTextChange, | |
handleUploadChange, | |
} | |
} | |
export default useBase |
@wenisman Hi! See the full code here: https://github.com/proteye/keystone-cms
Thanks!
@wenisman Hi! See the full code here: https://github.com/proteye/keystone-cms
Thank you so much for your help! I implemented the image upload field on my website, and it’s working perfectly. I really appreciate your guidance! 😊
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The gist is a great starting block, however you have Styles.ts code misplaced with the ImageUploader, and the type of
TImageFieldValue
is missing.