diff --git a/examples-next/todo/CHANGELOG.md b/examples-next/images/CHANGELOG.md similarity index 100% rename from examples-next/todo/CHANGELOG.md rename to examples-next/images/CHANGELOG.md diff --git a/examples-next/todo/keystone.ts b/examples-next/images/keystone.ts similarity index 76% rename from examples-next/todo/keystone.ts rename to examples-next/images/keystone.ts index d1723a9ad76..54961d048bb 100644 --- a/examples-next/todo/keystone.ts +++ b/examples-next/images/keystone.ts @@ -26,6 +26,16 @@ export default withAuth( url: process.env.DATABASE_URL || 'postgres://keystone5:k3yst0n3@localhost:5432/todo-example', }, lists, + images: { + /* could be 'cloud' */ + upload: 'local', + local: { + /* where images get stored when uploaded */ + uploadPath: './public/images', + /* the basePath for where image are served from */ + publicPath: '/images', + }, + }, ui: { isAccessAllowed: ({ session }) => !!session, }, diff --git a/examples-next/todo/package.json b/examples-next/images/package.json similarity index 100% rename from examples-next/todo/package.json rename to examples-next/images/package.json diff --git a/examples-next/todo/schema.ts b/examples-next/images/schema.ts similarity index 94% rename from examples-next/todo/schema.ts rename to examples-next/images/schema.ts index f590920ff40..6a8d1bb3c79 100644 --- a/examples-next/todo/schema.ts +++ b/examples-next/images/schema.ts @@ -1,5 +1,5 @@ import { createSchema, list } from '@keystone-next/keystone/schema'; -import { checkbox, password, relationship, text, timestamp } from '@keystone-next/fields'; +import { checkbox, image, password, relationship, text, timestamp } from '@keystone-next/fields'; // this implementation for createdBy and updatedBy is currently wrong so they're disabled for now const trackingFields = { @@ -69,6 +69,7 @@ export const lists = createSchema({ name: text({ isRequired: true }), email: text(), password: password(), + profilePicture: image(), tasks: relationship({ ref: 'Todo.assignedTo', many: true, diff --git a/examples-next/images/types.ts b/examples-next/images/types.ts new file mode 100644 index 00000000000..c49fc0b3b94 --- /dev/null +++ b/examples-next/images/types.ts @@ -0,0 +1,38 @@ +type ImageMode = 'local' | 'cloud'; +type ImageFormat = 'jpeg' | 'jpg' | 'png' | 'gif' | 'webp'; +type ImageObjectFit = 'fixed' | 'intrinsic' | 'responsive' | 'fill'; +type BlurHash = { hash: string; x: number; y: number }; + +export type Image = { + mode: ImageMode; + id: string; + extension: ImageFormat; + filesize: number; + name: string; + width: number; + height: number; + blurHash: BlurHash; +}; + +export type ImageInputType = { + upload: object; + id: string; + mode: ImageMode; +}; + +export type ImageOutputType = { + mode: ImageMode; + id: string; + ext: string; + src: string; + width: number; + height: number; + filesize: number; + blurHash: BlurHash; + transform: ( + width: number, + height: number, + quality: number, + objectFit: ImageObjectFit + ) => { src: string; width: number; height: number }; +}; diff --git a/packages-next/fields/src/index.ts b/packages-next/fields/src/index.ts index a31b6e77010..27a55b60a32 100644 --- a/packages-next/fields/src/index.ts +++ b/packages-next/fields/src/index.ts @@ -3,6 +3,7 @@ export { relationship } from './types/relationship'; export { text } from './types/text'; export { password } from './types/password'; export { timestamp } from './types/timestamp'; +export { image } from './types/image'; export { integer } from './types/integer'; export { float } from './types/float'; export { decimal } from './types/decimal'; diff --git a/packages-next/fields/src/types/image/index.ts b/packages-next/fields/src/types/image/index.ts new file mode 100644 index 00000000000..86878942770 --- /dev/null +++ b/packages-next/fields/src/types/image/index.ts @@ -0,0 +1,23 @@ +// @ts-ignore +import { Text } from '@keystone-next/fields-legacy'; +import type { FieldType, BaseGeneratedListTypes } from '@keystone-next/types'; +import { resolveView } from '../../resolve-view'; +import type { FieldConfig } from '../../interfaces'; + +export type ImageFieldConfig< + TGeneratedListTypes extends BaseGeneratedListTypes +> = FieldConfig & { + isRequired?: boolean; + isIndexed?: boolean; +}; + +export const image = ( + config: ImageFieldConfig = {} +): FieldType => ({ + type: Text, + config, + views: resolveView('image/views'), + getAdminMeta: () => ({ + /* ?? */ + }), +}); diff --git a/packages-next/fields/src/types/image/tests/test-fixtures.ts b/packages-next/fields/src/types/image/tests/test-fixtures.ts new file mode 100644 index 00000000000..42160898991 --- /dev/null +++ b/packages-next/fields/src/types/image/tests/test-fixtures.ts @@ -0,0 +1,43 @@ +import { AdapterName } from '@keystone-next/test-utils-legacy'; +import { text } from '..'; + +export const name = 'Text'; +export const typeFunction = text; +export const exampleValue = () => 'foo'; +export const exampleValue2 = () => 'bar'; +export const supportsUnique = true; +export const fieldName = 'testField'; + +export const getTestFields = () => ({ name: text(), testField: text() }); + +export const initItems = () => { + return [ + { name: 'a', testField: '' }, + { name: 'b', testField: 'other' }, + { name: 'c', testField: 'FOOBAR' }, + { name: 'd', testField: 'fooBAR' }, + { name: 'e', testField: 'foobar' }, + { name: 'f', testField: null }, + { name: 'g' }, + ]; +}; + +export const storedValues = () => [ + { name: 'a', testField: '' }, + { name: 'b', testField: 'other' }, + { name: 'c', testField: 'FOOBAR' }, + { name: 'd', testField: 'fooBAR' }, + { name: 'e', testField: 'foobar' }, + { name: 'f', testField: null }, + { name: 'g', testField: null }, +]; + +export const supportedFilters = (adapterName: AdapterName) => [ + 'null_equality', + 'equality', + adapterName !== 'prisma_sqlite' && 'equality_case_insensitive', + 'in_empty_null', + 'in_value', + adapterName !== 'prisma_sqlite' && 'string', + adapterName !== 'prisma_sqlite' && 'string_case_insensitive', +]; diff --git a/packages-next/fields/src/types/image/views/index.tsx b/packages-next/fields/src/types/image/views/index.tsx new file mode 100644 index 00000000000..0beaf958935 --- /dev/null +++ b/packages-next/fields/src/types/image/views/index.tsx @@ -0,0 +1,58 @@ +/* @jsx jsx */ + +import { CellContainer, CellLink } from '@keystone-next/admin-ui/components'; +import { + CardValueComponent, + CellComponent, + FieldController, + FieldControllerConfig, + FieldProps, +} from '@keystone-next/types'; +import { jsx } from '@keystone-ui/core'; +import { FieldContainer, FieldLabel, TextInput } from '@keystone-ui/fields'; + +export const Field = ({ field, value, onChange, autoFocus }: FieldProps) => ( + + {field.label} + {onChange ? ( + onChange(event.target.value)} + value={value} + /> + ) : ( + value + )} + +); + +export const Cell: CellComponent = ({ item, field, linkTo }) => { + let value = item[field.path] + ''; + return linkTo ? {value} : {value}; +}; +Cell.supportsLinkTo = true; + +export const CardValue: CardValueComponent = ({ item, field }) => { + return ( + + {field.label} + {item[field.path]} + + ); +}; + +type Config = FieldControllerConfig; + +export const controller = (config: Config): FieldController => { + return { + path: config.path, + label: config.label, + graphqlSelection: config.path, + defaultValue: '', + deserialize: data => { + const value = data[config.path]; + return typeof value === 'string' ? value : ''; + }, + serialize: value => ({ [config.path]: value }), + }; +}; diff --git a/packages-next/types/src/config/index.ts b/packages-next/types/src/config/index.ts index 43f0b73352f..ad6f08c92e5 100644 --- a/packages-next/types/src/config/index.ts +++ b/packages-next/types/src/config/index.ts @@ -24,9 +24,12 @@ import type { ListAccessControl, FieldAccessControl } from './access-control'; import type { ListHooks } from './hooks'; export type KeystoneConfig = { + // e.g 'thinkmill:243809dsjkfdls-r2y8osdfjsdf-23y8osf', + cloud?: string; lists: ListSchemaConfig; db: DatabaseConfig; ui?: AdminUIConfig; + images?: ImagesConfig; server?: ServerConfig; session?: () => SessionStrategy; graphql?: GraphQLConfig; @@ -106,6 +109,22 @@ export type AdminFileToWrite = | { mode: 'write'; src: string; outputPath: string } | { mode: 'copy'; inputPath: string; outputPath: string }; +// config.images + +export type ImagesConfig = + | { + upload: 'local'; + local: { + /** The path local images are uploaded to */ + uploadPath?: string; + /** The path local images will be served from, outside of keystone */ + publicPath?: string; + }; + } + | { + upload: 'cloud'; + }; + // config.server export type ServerConfig = {