Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image field type #5396

Merged
merged 91 commits into from
Apr 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
434ace0
Field implementation
emmatown Apr 7, 2021
45c7dd6
First draft at createImagesContext internals
rohan-deshpande Apr 7, 2021
5dd9c19
Removed ts-ignore
rohan-deshpande Apr 7, 2021
759ff85
fixes
gwyneplaine Apr 9, 2021
9d76cc5
some other fixes to things
gwyneplaine Apr 9, 2021
ef9a078
initial image field implementation works
gwyneplaine Apr 9, 2021
bf9d602
blurHash feature fixed
gwyneplaine Apr 9, 2021
b62cc6a
refs working e2e
gwyneplaine Apr 9, 2021
0e47920
image field ui
gwyneplaine Apr 12, 2021
88d1e4b
Field implementation
emmatown Apr 7, 2021
bd4ea23
First draft at createImagesContext internals
rohan-deshpande Apr 7, 2021
3d78655
Removed ts-ignore
rohan-deshpande Apr 7, 2021
727d4b1
fixes
gwyneplaine Apr 9, 2021
f0b114e
some other fixes to things
gwyneplaine Apr 9, 2021
95ba439
initial image field implementation works
gwyneplaine Apr 9, 2021
48e9d31
blurHash feature fixed
gwyneplaine Apr 9, 2021
43b9466
refs working e2e
gwyneplaine Apr 9, 2021
46ef9bf
image field ui
gwyneplaine Apr 12, 2021
a85d90b
Hardened image ref validation, removed sharp and blurhash from API
rohan-deshpande Apr 12, 2021
2f0f72e
UI changes based on feedback
gwyneplaine Apr 12, 2021
1d742ae
Remove unused packages from keystone package, removed blurHash from i…
rohan-deshpande Apr 12, 2021
0415428
Fixed typo
rohan-deshpande Apr 12, 2021
8169c5e
yarn manypkg fix
rohan-deshpande Apr 12, 2021
93cf85b
merge resolution and UI changes
gwyneplaine Apr 12, 2021
8b14ef0
dont remove the ref if its compelte and untouched
gwyneplaine Apr 12, 2021
7687338
Hardened ref validation by checking if the file exists on disk for lo…
rohan-deshpande Apr 12, 2021
6a5be48
Merge branch 'image-field-type' of https://github.com/keystonejs/keys…
rohan-deshpande Apr 12, 2021
f1a41b9
Merge branch 'master' into image-field-type
rohan-deshpande Apr 12, 2021
dab70ec
clean up ui, fix validation in the back
gwyneplaine Apr 12, 2021
8d5f8da
Merge branch 'image-field-type' of https://github.com/keystonejs/keys…
gwyneplaine Apr 12, 2021
3850229
fix types
gwyneplaine Apr 12, 2021
ca8781d
Changed default baseUrl to /images, made baseUrl and storagePath opti…
rohan-deshpande Apr 13, 2021
75d5809
Fix things
emmatown Apr 13, 2021
a033cc3
Update packages-next/fields/src/types/image/Implementation.ts
emmatown Apr 13, 2021
43f6baa
ui adjustments
gwyneplaine Apr 13, 2021
bd5554e
Merge branch 'image-field-type' of https://github.com/keystonejs/keys…
gwyneplaine Apr 13, 2021
a4d4649
convert Implementation to ts
gwyneplaine Apr 13, 2021
0911579
clean up UI slightly
gwyneplaine Apr 13, 2021
55cf8f7
add explicit error handling to gqlAuxFieldResolvers
gwyneplaine Apr 13, 2021
d9b1c43
Merge branch 'master' into image-field-type
gwyneplaine Apr 13, 2021
9fadd7c
replace List type with BaseKeystoneList type
gwyneplaine Apr 13, 2021
ee9a4b0
fix type error in createSystem
gwyneplaine Apr 13, 2021
1636135
refactor image field to be stored as multiple separate fields in pris…
gwyneplaine Apr 14, 2021
c0a7c37
fixing types
gwyneplaine Apr 14, 2021
b6e832b
Merge branch 'master' into image-field-type
gwyneplaine Apr 14, 2021
9d871e4
corrections to prisma split logic and moved previousFile logic into an
gwyneplaine Apr 14, 2021
4ae3135
remove lint issues
gwyneplaine Apr 14, 2021
4bf3323
fix cell component in image field, add src to graphql query in view c…
gwyneplaine Apr 14, 2021
019b73f
Update packages-next/fields/src/types/image/Implementation.ts
gwyneplaine Apr 14, 2021
01fe72e
remove JSON.stringify
gwyneplaine Apr 14, 2021
0086e1b
convert extensions into an enum for prisma, when its not sqlite
gwyneplaine Apr 15, 2021
d3eb1ee
remove validateInput, add ImageMode, rename Extensions to ImageExtens…
gwyneplaine Apr 15, 2021
09ff21a
Update packages-next/fields/src/Implementation.ts
gwyneplaine Apr 15, 2021
44b61a4
Update packages-next/fields/src/Implementation.ts
gwyneplaine Apr 15, 2021
8085456
Update packages-next/fields/src/Implementation.ts
gwyneplaine Apr 15, 2021
9218b49
Update packages-next/fields/src/types/image/Implementation.ts
gwyneplaine Apr 15, 2021
302fad9
Update packages-next/fields/src/types/image/Implementation.ts
gwyneplaine Apr 15, 2021
9798538
changesets
gwyneplaine Apr 15, 2021
f2c9797
Merge branch 'image-field-type' of https://github.com/keystonejs/keys…
gwyneplaine Apr 15, 2021
8f654ef
Merge branch 'master' into image-field-type
gwyneplaine Apr 15, 2021
d133477
removed sharp
gwyneplaine Apr 15, 2021
516326d
remove sharp types and blurhash
gwyneplaine Apr 15, 2021
58dfa8f
Update .changeset/shaggy-dots-appear.md
emmatown Apr 15, 2021
7d630f0
Merge branch 'master' into image-field-type
gwyneplaine Apr 15, 2021
e783e64
remove logs
gwyneplaine Apr 15, 2021
dce3421
Merge branch 'image-field-type' of https://github.com/keystonejs/keys…
gwyneplaine Apr 15, 2021
f7091a5
remove logs, fix regex
gwyneplaine Apr 15, 2021
f292466
add ignore case to regex
gwyneplaine Apr 15, 2021
8b01135
revert
gwyneplaine Apr 15, 2021
b42bebd
fix gitignore in basic example
gwyneplaine Apr 15, 2021
ed89386
gitignore
gwyneplaine Apr 15, 2021
af2f4de
remove public images
gwyneplaine Apr 15, 2021
0f46a47
more secure property check
gwyneplaine Apr 15, 2021
e41840b
remove comment
gwyneplaine Apr 15, 2021
9533bad
test-files removed
gwyneplaine Apr 15, 2021
3b19836
gitignore
gwyneplaine Apr 15, 2021
29c5ba4
rollback enums
gwyneplaine Apr 15, 2021
66ebce7
remove unused MODE constant
gwyneplaine Apr 15, 2021
70bb2c8
change all relevant ext usages into extension, remove log
gwyneplaine Apr 15, 2021
94729e1
remove parseInt from filesize in image prisma adapter
gwyneplaine Apr 15, 2021
af29476
remove jpeg from supported list
gwyneplaine Apr 15, 2021
b2bc37f
remove getRef and parseRef from context, add getImageRef and parseIma…
gwyneplaine Apr 16, 2021
c780299
Update packages-next/fields/src/types/image/views/Field.tsx
gwyneplaine Apr 16, 2021
d70d58a
remove manual split in favor of regex match
gwyneplaine Apr 16, 2021
73d7da5
simplify validation code
gwyneplaine Apr 16, 2021
dbdeb9e
Update packages/utils/src/index.ts
emmatown Apr 16, 2021
cdf9d76
Merge branch 'master' into image-field-type
gwyneplaine Apr 16, 2021
1fa8a67
prettier
gwyneplaine Apr 16, 2021
feac5e6
Clean up ui
emmatown Apr 16, 2021
0d540f3
Fix some more stuff
emmatown Apr 16, 2021
ec3a1e2
Use image-type instead of file-type
emmatown Apr 16, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/real-parents-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/admin-ui': minor
---

Reflected next/image exports from admin-ui for use in other relevant keystone-next packages.
5 changes: 5 additions & 0 deletions .changeset/rude-parrots-train.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/app-basic': patch
---

Added example of image field support to basic keystone-next example.
gwyneplaine marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions .changeset/shaggy-dots-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/fields': minor
---

Added new image field type.
5 changes: 5 additions & 0 deletions .changeset/stale-balloons-grin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/types': minor
---

Added types for new images functionality in keystone.
5 changes: 5 additions & 0 deletions .changeset/tough-ravens-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': minor
---

Added create-image-context, logic for parsing, storing and retrieving image data in keystone core.
1 change: 1 addition & 0 deletions examples-next/basic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public/
1 change: 1 addition & 0 deletions examples-next/basic/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default auth.withAuth(
// path: '/admin',
// isAccessAllowed,
},
images: { upload: 'local' },
lists,
extendGraphqlSchema,
session: withItemData(
Expand Down
11 changes: 3 additions & 8 deletions examples-next/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
timestamp,
select,
virtual,
image,
} from '@keystone-next/fields';
import { document } from '@keystone-next/fields-document';
// import { cloudinaryImage } from '@keystone-next/cloudinary';
Expand Down Expand Up @@ -34,21 +35,15 @@ export const lists = createSchema({
User: list({
ui: {
listView: {
initialColumns: ['name', 'posts'],
initialColumns: ['name', 'posts', 'avatar'],
},
},
fields: {
/** The user's first and last name. */
name: text({ isRequired: true }),
/** Email is used to log into the system. */
email: text({ isRequired: true, isUnique: true }),
// avatar: cloudinaryImage({
// cloudinary: {
// cloudName: '/* TODO */',
// apiKey: '/* TODO */',
// apiSecret: '/* TODO */',
// },
// }),
avatar: image(),
/** Used to log in. */
password: password(),
/** Administrators have more access to various lists and fields. */
Expand Down
4 changes: 4 additions & 0 deletions packages-next/admin-ui/image/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/admin-ui.cjs.js",
"module": "dist/admin-ui.esm.js"
}
3 changes: 2 additions & 1 deletion packages-next/admin-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
"apollo.tsx",
"context.tsx",
"router.tsx",
"next-config.ts"
"next-config.ts",
"image.tsx"
]
},
"repository": "https://github.com/keystonejs/keystone/tree/master/packages-next/admin-ui",
Expand Down
2 changes: 2 additions & 0 deletions packages-next/admin-ui/src/image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from 'next/image';
export { default } from 'next/image';
8 changes: 8 additions & 0 deletions packages-next/admin-ui/src/system/generateAdminUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export const generateAdminUI = async (
// Nuke any existing files in our target directory
await fs.remove(projectAdminPath);

if (config.images) {
const publicDirectory = Path.join(projectAdminPath, 'public');
await fs.mkdir(publicDirectory, { recursive: true });
const storagePath = Path.resolve(config.images.local?.storagePath ?? './public/images');
await fs.mkdir(storagePath, { recursive: true });
await fs.symlink(storagePath, Path.join(publicDirectory, 'images'), 'junction');
}

// Write out the files configured by the user
const userPages = config.ui?.getAdditionalFiles?.map(x => x(config)) ?? [];
const userFilesToWrite = (await Promise.all(userPages)).flat();
Expand Down
4 changes: 4 additions & 0 deletions packages-next/fields/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"devDependencies": {
"@keystone-next/server-side-graphql-client-legacy": "3.0.1",
"@keystone-next/test-utils-legacy": "16.0.0",
"@types/bytes": "^3.1.0",
"mime": "^2.5.2",
"typescript": "^4.2.3"
},
Expand All @@ -25,6 +26,7 @@
"@keystone-ui/icons": "^2.0.1",
"@keystone-ui/loading": "^2.0.1",
"@keystone-ui/modals": "^2.0.1",
"@keystone-ui/pill": "^2.0.1",
"@keystone-ui/segmented-control": "^2.0.1",
"@keystone-ui/toast": "^2.0.1",
"@keystone-ui/tooltip": "^2.0.1",
Expand All @@ -33,6 +35,8 @@
"@types/react": "^17.0.3",
"apollo-errors": "^1.9.0",
"bcryptjs": "^2.4.3",
"bytes": "^3.1.0",
"copy-to-clipboard": "^3.3.1",
"cuid": "^2.1.8",
"date-fns": "^2.20.2",
"decimal.js": "^10.2.1",
Expand Down
22 changes: 21 additions & 1 deletion packages-next/fields/src/Implementation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,28 @@ class Field<P extends string> {
return resolvedData[this.path];
}

/**
* @param {Object} data
* @param {Object} data.resolvedData The incoming item for the mutation with
* relationships and defaults already resolved
* @param {Object} data.existingItem If this is a updateX mutation, this will
* be the existing data in the database
* @param {Object} data.context The graphQL context object of the current
* request
* @param {Object} data.originalInput The raw incoming item from the mutation
* (no relationships or defaults resolved)
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async validateInput(args: any) {}
async validateInput(data: {
resolvedData: Record<P, any>;
existingItem?: Record<string, any>;
context: KeystoneContext;
originalInput: any;
listKey: string;
fieldPath: P;
operation: 'create' | 'update';
addFieldValidationError: (msg: string) => void;
}) {}

async beforeChange() {}

Expand Down
1 change: 1 addition & 0 deletions packages-next/fields/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { select } from './types/select';
export { virtual } from './types/virtual';
export { Implementation } from './Implementation';
export type { FieldConfigArgs, FieldExtraArgs } from './Implementation';
export { image } from './types/image';
216 changes: 216 additions & 0 deletions packages-next/fields/src/types/image/Implementation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { PrismaFieldAdapter, PrismaListAdapter } from '@keystone-next/adapter-prisma-legacy';
import { getImageRef, SUPPORTED_IMAGE_EXTENSIONS } from '@keystone-next/utils-legacy';
import { ImageData, KeystoneContext, BaseKeystoneList } from '@keystone-next/types';
import { Implementation } from '../../Implementation';
import { handleImageData } from './handle-image-input';

export class ImageImplementation<P extends string> extends Implementation<P> {
get _supportsUnique() {
return false;
}

gqlOutputFields() {
return [`${this.path}: ImageFieldOutput`];
}

getGqlAuxTypes() {
return [
`enum ImageMode {
local
}
input ImageFieldInput {
upload: Upload
ref: String
}
enum ImageExtension {
${SUPPORTED_IMAGE_EXTENSIONS.join('\n')}
}
type ImageFieldOutput {
mode: ImageMode!
id: ID!
filesize: Int!
width: Int!
height: Int!
extension: ImageExtension!
ref: String!
src: String!
}`,
];
}

gqlAuxFieldResolvers() {
return {
ImageFieldOutput: {
src(data: ImageData, _args: any, context: KeystoneContext) {
if (!context.images) {
throw new Error('Image context is undefined');
}
return context.images.getSrc(data.mode, data.id, data.extension);
},
ref(data: ImageData, _args: any, context: KeystoneContext) {
if (!context.images) {
throw new Error('Image context is undefined');
}
return getImageRef(data.mode, data.id, data.extension);
},
},
};
}
// Called on `User.avatar` for example
gqlOutputFieldResolvers() {
return { [`${this.path}`]: (item: Record<P, any>) => item[this.path] };
}

async resolveInput({
resolvedData,
context,
}: {
resolvedData: Record<P, any>;
context: KeystoneContext;
}) {
const data = resolvedData[this.path];
if (data === null) {
return null;
}
if (data === undefined) {
return undefined;
}
const imageData = await handleImageData(data, context);
return imageData;
}

gqlUpdateInputFields() {
return [`${this.path}: ImageFieldInput`];
}
gqlCreateInputFields() {
return [`${this.path}: ImageFieldInput`];
}
getBackingTypes() {
return { [this.path]: { optional: true, type: 'Record<string, any> | null' } };
}
}

export class PrismaImageInterface<P extends string> extends PrismaFieldAdapter<P> {
constructor(
fieldName: string,
path: P,
field: ImageImplementation<P>,
listAdapter: PrismaListAdapter,
getListByKey: (arg: string) => BaseKeystoneList | undefined,
config = {}
) {
super(fieldName, path, field, listAdapter, getListByKey, config);
// Error rather than ignoring invalid config
// We totally can index these values, it's just not trivial. See issue #1297
if (this.config.isIndexed) {
throw new Error(
`The Image field type doesn't support indexes on Prisma. ` +
`Check the config for ${this.path} on the ${this.field.listKey} list`
);
}
}

getPrismaSchema() {
return [
`${this.path}_filesize Int?`,
`${this.path}_extension String?`,
`${this.path}_width Int?`,
`${this.path}_height Int?`,
`${this.path}_mode String?`,
`${this.path}_id String?`,
];
}

getQueryConditions() {
return {};
}

setupHooks({
addPreSaveHook,
addPostReadHook,
}: {
addPreSaveHook: (hook: any) => void;
addPostReadHook: (hook: any) => void;
}) {
const field_path = this.path;
const filesize_field = `${this.path}_filesize`;
const extension_field = `${this.path}_extension`;
const width_field = `${this.path}_width`;
const height_field = `${this.path}_height`;
const mode_field = `${this.path}_mode`;
const id_field = `${this.path}_id`;

addPreSaveHook(
(item: Record<P, any>): Record<string, any> => {
if (!Object.prototype.hasOwnProperty.call(item, field_path)) {
return item;
}
if (item[field_path as P] === null) {
// If the property exists on the field but is null or falsey
// all split fields are null
// delete the original field item
// return the item
const newItem = {
[filesize_field]: null,
[extension_field]: null,
[width_field]: null,
[height_field]: null,
[id_field]: null,
[mode_field]: null,
...item,
};
delete newItem[field_path];
return newItem;
} else {
const { mode, filesize, extension, width, height, id } = item[field_path];

const newItem = {
[filesize_field]: filesize,
[extension_field]: extension,
[width_field]: width,
[height_field]: height,
[id_field]: id,
[mode_field]: mode,
...item,
};

delete newItem[field_path];

return newItem;
}
}
);
addPostReadHook(
(item: Record<string, any>): Record<P, any> => {
if (
!item[filesize_field] ||
!item[extension_field] ||
!item[width_field] ||
!item[height_field] ||
!item[id_field] ||
!item[mode_field]
) {
item[field_path] = null;
return item;
}
item[field_path] = {
filesize: item[filesize_field],
extension: item[extension_field],
width: item[width_field],
height: item[height_field],
id: item[id_field],
mode: item[mode_field],
};

delete item[filesize_field];
delete item[extension_field];
delete item[width_field];
delete item[height_field];
delete item[id_field];
delete item[mode_field];

return item;
}
);
}
}
Loading