From 708861d6037c515e6c5c164aa3ea723853d330c3 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 16 Nov 2022 10:14:28 +1000 Subject: [PATCH 1/3] Add field groups --- .changeset/warm-gifts-change.md | 5 + docs/pages/docs/fields/overview.md | 33 ++++ examples/assets-local/schema.graphql | 9 +- examples/assets-s3/schema.graphql | 9 +- examples/auth/schema.graphql | 9 +- examples/basic/schema.graphql | 9 +- examples/blog/schema.graphql | 9 +- examples/custom-admin-ui-logo/schema.graphql | 9 +- .../custom-admin-ui-navigation/schema.graphql | 9 +- examples/custom-admin-ui-pages/schema.graphql | 9 +- examples/custom-field-view/schema.graphql | 9 +- examples/custom-field/schema.graphql | 9 +- .../custom-session-validation/schema.graphql | 9 +- examples/default-values/schema.graphql | 9 +- .../keystone-server/schema.graphql | 9 +- examples/document-field/schema.graphql | 9 +- .../keystone-server/schema.graphql | 9 +- examples/ecommerce/schema.graphql | 9 +- examples/embedded-nextjs/schema.graphql | 9 +- .../schema.graphql | 9 +- .../schema.graphql | 9 +- .../schema.graphql | 9 +- .../schema.graphql | 9 +- examples/feature-boilerplate/schema.graphql | 9 +- examples/graphql-ts-gql/schema.graphql | 9 +- examples/json/schema.graphql | 9 +- examples/limits/schema.graphql | 9 +- examples/redis-session-store/schema.graphql | 9 +- examples/rest-api/schema.graphql | 9 +- examples/roles/schema.graphql | 9 +- examples/script/schema.graphql | 9 +- examples/singleton/schema.graphql | 9 +- examples/task-manager/schema.graphql | 9 +- examples/testing/schema.graphql | 9 +- examples/virtual-field/schema.graphql | 9 +- examples/with-auth/schema.graphql | 9 +- .../admin-ui/pages/ItemPage/index.tsx | 2 + .../core/src/admin-ui/admin-meta-graphql.ts | 63 ++++--- .../src/admin-ui/system/adminMetaSchema.ts | 23 ++- .../src/admin-ui/system/createAdminMeta.ts | 17 ++ packages/core/src/admin-ui/utils/Fields.tsx | 156 +++++++++++++++--- .../core/src/admin-ui/utils/useAdminMeta.tsx | 8 + .../core/src/admin-ui/utils/useCreateItem.ts | 1 + packages/core/src/index.ts | 2 +- packages/core/src/lib/core/prisma-schema.ts | 6 +- packages/core/src/lib/core/types-for-lists.ts | 32 +++- packages/core/src/lib/core/utils.ts | 4 + packages/core/src/lib/createSystem.ts | 1 + packages/core/src/schema/schema.ts | 21 +++ .../fixtures/basic-project/schema.graphql | 9 +- packages/core/src/types/admin-meta.ts | 7 + tests/api-tests/admin-meta.test.ts | 1 + tests/api-tests/field-groups.test.ts | 57 +++++++ tests/sandbox/configs/all-the-things.ts | 15 +- tests/sandbox/schema.graphql | 9 +- tests/test-projects/basic/schema.graphql | 9 +- .../crud-notifications/schema.graphql | 9 +- .../live-reloading/schema.graphql | 9 +- 58 files changed, 699 insertions(+), 106 deletions(-) create mode 100644 .changeset/warm-gifts-change.md create mode 100644 tests/api-tests/field-groups.test.ts diff --git a/.changeset/warm-gifts-change.md b/.changeset/warm-gifts-change.md new file mode 100644 index 00000000000..301efaf2db6 --- /dev/null +++ b/.changeset/warm-gifts-change.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': minor +--- + +Adds `group` function to allow grouping of fields in the Admin UI diff --git a/docs/pages/docs/fields/overview.md b/docs/pages/docs/fields/overview.md index 7ce0dc9e131..2f08b991141 100644 --- a/docs/pages/docs/fields/overview.md +++ b/docs/pages/docs/fields/overview.md @@ -142,6 +142,39 @@ export default config({ }); ``` +{% if $nextRelease %} + +## Groups + +Fields can be grouped together in the Admin UI using the `group` function with a `label` and optionally `description`. + +```typescript +import { config, list, group } from '@keystone-6/core'; +import { text } from '@keystone-6/core/fields'; + +export default config({ + lists: { + SomeListName: list({ + fields: { + ...group({ + label: 'Group label', + description: 'Group description', + fields: { + someFieldName: text({ /* ... */ }), + /* ... */ + }, + }), + /* ... */ + }, + }), + /* ... */ + }, + /* ... */ +}); +``` + +{% /if %} + ## Scalar types - [BigInt](./bigint) diff --git a/examples/assets-local/schema.graphql b/examples/assets-local/schema.graphql index eea8f02c096..4e8268a6837 100644 --- a/examples/assets-local/schema.graphql +++ b/examples/assets-local/schema.graphql @@ -292,9 +292,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -351,6 +352,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/assets-s3/schema.graphql b/examples/assets-s3/schema.graphql index eea8f02c096..4e8268a6837 100644 --- a/examples/assets-s3/schema.graphql +++ b/examples/assets-s3/schema.graphql @@ -292,9 +292,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -351,6 +352,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/auth/schema.graphql b/examples/auth/schema.graphql index 146a35d55c9..f3f48b75778 100644 --- a/examples/auth/schema.graphql +++ b/examples/auth/schema.graphql @@ -176,9 +176,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -235,6 +236,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/basic/schema.graphql b/examples/basic/schema.graphql index ae0c25a34e1..420be579164 100644 --- a/examples/basic/schema.graphql +++ b/examples/basic/schema.graphql @@ -446,9 +446,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -505,6 +506,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/blog/schema.graphql b/examples/blog/schema.graphql index 13e22de6109..a9b9b977919 100644 --- a/examples/blog/schema.graphql +++ b/examples/blog/schema.graphql @@ -253,9 +253,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -312,6 +313,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-admin-ui-logo/schema.graphql b/examples/custom-admin-ui-logo/schema.graphql index fc8b564e3c4..021b2a9c630 100644 --- a/examples/custom-admin-ui-logo/schema.graphql +++ b/examples/custom-admin-ui-logo/schema.graphql @@ -253,9 +253,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -312,6 +313,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-admin-ui-navigation/schema.graphql b/examples/custom-admin-ui-navigation/schema.graphql index fc8b564e3c4..021b2a9c630 100644 --- a/examples/custom-admin-ui-navigation/schema.graphql +++ b/examples/custom-admin-ui-navigation/schema.graphql @@ -253,9 +253,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -312,6 +313,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-admin-ui-pages/schema.graphql b/examples/custom-admin-ui-pages/schema.graphql index fc8b564e3c4..021b2a9c630 100644 --- a/examples/custom-admin-ui-pages/schema.graphql +++ b/examples/custom-admin-ui-pages/schema.graphql @@ -253,9 +253,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -312,6 +313,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-field-view/schema.graphql b/examples/custom-field-view/schema.graphql index 0450e74c32f..95ad7109451 100644 --- a/examples/custom-field-view/schema.graphql +++ b/examples/custom-field-view/schema.graphql @@ -256,9 +256,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -315,6 +316,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-field/schema.graphql b/examples/custom-field/schema.graphql index 2f9cd06b5a5..01c3b0e71e0 100644 --- a/examples/custom-field/schema.graphql +++ b/examples/custom-field/schema.graphql @@ -108,9 +108,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -167,6 +168,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/custom-session-validation/schema.graphql b/examples/custom-session-validation/schema.graphql index 590e01511cf..6c958334426 100644 --- a/examples/custom-session-validation/schema.graphql +++ b/examples/custom-session-validation/schema.graphql @@ -294,9 +294,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -353,6 +354,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index fc9fe42a703..fc4cf1d9885 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -271,9 +271,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -330,6 +331,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/document-field-customisation/keystone-server/schema.graphql b/examples/document-field-customisation/keystone-server/schema.graphql index 434d1c89d92..f898703088f 100644 --- a/examples/document-field-customisation/keystone-server/schema.graphql +++ b/examples/document-field-customisation/keystone-server/schema.graphql @@ -244,9 +244,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -303,6 +304,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/document-field/schema.graphql b/examples/document-field/schema.graphql index 9b3ec082052..dcba91adb14 100644 --- a/examples/document-field/schema.graphql +++ b/examples/document-field/schema.graphql @@ -268,9 +268,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -327,6 +328,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/e2e-boilerplate/keystone-server/schema.graphql b/examples/e2e-boilerplate/keystone-server/schema.graphql index 434d1c89d92..f898703088f 100644 --- a/examples/e2e-boilerplate/keystone-server/schema.graphql +++ b/examples/e2e-boilerplate/keystone-server/schema.graphql @@ -244,9 +244,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -303,6 +304,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/ecommerce/schema.graphql b/examples/ecommerce/schema.graphql index 8d9b8adcc76..5ca7bf92977 100644 --- a/examples/ecommerce/schema.graphql +++ b/examples/ecommerce/schema.graphql @@ -859,9 +859,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -918,6 +919,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/embedded-nextjs/schema.graphql b/examples/embedded-nextjs/schema.graphql index 78737d9f096..87b1771da1e 100644 --- a/examples/embedded-nextjs/schema.graphql +++ b/examples/embedded-nextjs/schema.graphql @@ -135,9 +135,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -194,6 +195,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/extend-graphql-schema-graphql-tools/schema.graphql b/examples/extend-graphql-schema-graphql-tools/schema.graphql index 49896bd774a..fb84aceb7af 100644 --- a/examples/extend-graphql-schema-graphql-tools/schema.graphql +++ b/examples/extend-graphql-schema-graphql-tools/schema.graphql @@ -265,9 +265,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -324,6 +325,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/extend-graphql-schema-graphql-ts/schema.graphql b/examples/extend-graphql-schema-graphql-ts/schema.graphql index ebc6b6f1bb0..9dedb0ce25b 100644 --- a/examples/extend-graphql-schema-graphql-ts/schema.graphql +++ b/examples/extend-graphql-schema-graphql-ts/schema.graphql @@ -262,9 +262,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -321,6 +322,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/extend-graphql-schema-nexus/schema.graphql b/examples/extend-graphql-schema-nexus/schema.graphql index c34caf7cb00..ca9149baaa3 100644 --- a/examples/extend-graphql-schema-nexus/schema.graphql +++ b/examples/extend-graphql-schema-nexus/schema.graphql @@ -255,9 +255,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -314,6 +315,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/extend-graphql-subscriptions/schema.graphql b/examples/extend-graphql-subscriptions/schema.graphql index 5c3bc13875f..cf78ec0efb5 100644 --- a/examples/extend-graphql-subscriptions/schema.graphql +++ b/examples/extend-graphql-subscriptions/schema.graphql @@ -256,9 +256,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -315,6 +316,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/feature-boilerplate/schema.graphql b/examples/feature-boilerplate/schema.graphql index 434d1c89d92..f898703088f 100644 --- a/examples/feature-boilerplate/schema.graphql +++ b/examples/feature-boilerplate/schema.graphql @@ -244,9 +244,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -303,6 +304,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/graphql-ts-gql/schema.graphql b/examples/graphql-ts-gql/schema.graphql index 56f8da7eeac..eb1c638179c 100644 --- a/examples/graphql-ts-gql/schema.graphql +++ b/examples/graphql-ts-gql/schema.graphql @@ -264,9 +264,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -323,6 +324,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/json/schema.graphql b/examples/json/schema.graphql index a633b3aa0e3..7ad6b6aee5d 100644 --- a/examples/json/schema.graphql +++ b/examples/json/schema.graphql @@ -220,9 +220,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -279,6 +280,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/limits/schema.graphql b/examples/limits/schema.graphql index 10a23f34a95..eac72ccab3b 100644 --- a/examples/limits/schema.graphql +++ b/examples/limits/schema.graphql @@ -125,9 +125,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -184,6 +185,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/redis-session-store/schema.graphql b/examples/redis-session-store/schema.graphql index 54700d31949..fcdf1d6d4a0 100644 --- a/examples/redis-session-store/schema.graphql +++ b/examples/redis-session-store/schema.graphql @@ -289,9 +289,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -348,6 +349,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/rest-api/schema.graphql b/examples/rest-api/schema.graphql index 295be0fd90e..9cb6b115656 100644 --- a/examples/rest-api/schema.graphql +++ b/examples/rest-api/schema.graphql @@ -254,9 +254,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -313,6 +314,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/roles/schema.graphql b/examples/roles/schema.graphql index 6f0f1a7eecb..e3e98654174 100644 --- a/examples/roles/schema.graphql +++ b/examples/roles/schema.graphql @@ -370,9 +370,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -429,6 +430,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/script/schema.graphql b/examples/script/schema.graphql index 9b1c84ccfd4..751c75fc01f 100644 --- a/examples/script/schema.graphql +++ b/examples/script/schema.graphql @@ -143,9 +143,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -202,6 +203,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/singleton/schema.graphql b/examples/singleton/schema.graphql index d9f85f5ee82..ecc0f91e903 100644 --- a/examples/singleton/schema.graphql +++ b/examples/singleton/schema.graphql @@ -307,9 +307,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -366,6 +367,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/task-manager/schema.graphql b/examples/task-manager/schema.graphql index 295be0fd90e..9cb6b115656 100644 --- a/examples/task-manager/schema.graphql +++ b/examples/task-manager/schema.graphql @@ -254,9 +254,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -313,6 +314,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/testing/schema.graphql b/examples/testing/schema.graphql index 54700d31949..fcdf1d6d4a0 100644 --- a/examples/testing/schema.graphql +++ b/examples/testing/schema.graphql @@ -289,9 +289,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -348,6 +349,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/virtual-field/schema.graphql b/examples/virtual-field/schema.graphql index 56f8da7eeac..eb1c638179c 100644 --- a/examples/virtual-field/schema.graphql +++ b/examples/virtual-field/schema.graphql @@ -264,9 +264,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -323,6 +324,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/examples/with-auth/schema.graphql b/examples/with-auth/schema.graphql index 54700d31949..fcdf1d6d4a0 100644 --- a/examples/with-auth/schema.graphql +++ b/examples/with-auth/schema.graphql @@ -289,9 +289,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -348,6 +349,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx index 6b9e9d82e76..78b64e4a4a1 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx @@ -163,6 +163,7 @@ function ItemForm({ errors={error?.graphQLErrors.filter(x => x.path?.length === 1)} /> = T | null; +// types.ts: +// plugins: +// - typescript-operations: +// enumsAsTypes: true +// skipTypename: true +// namingConvention: keep +// noExport: true +// avoidOptionals: true +// scalars: +// JSON: JSONValue export type StaticAdminMetaQuery = { keystone: { @@ -76,30 +80,39 @@ export type StaticAdminMetaQuery = { label: string; singular: string; plural: string; - description: Maybe; + description: string | null; initialColumns: Array; pageSize: number; labelField: string; - initialSort: Maybe<{ + isSingleton: boolean; + initialSort: { __typename: 'KeystoneAdminUISort'; field: string; direction: KeystoneAdminUISortDirection; + } | null; + groups: Array<{ + __typename: 'KeystoneAdminUIFieldGroupMeta'; + label: string; + description: string | null; + fields: Array<{ + __typename: 'KeystoneAdminUIFieldMeta'; + path: string; + }>; }>; - isSingleton: boolean; fields: Array<{ __typename: 'KeystoneAdminUIFieldMeta'; path: string; label: string; - description: Maybe; - fieldMeta: Maybe; + description: string | null; + fieldMeta: JSONValue | null; viewsIndex: number; - customViewsIndex: Maybe; - search: Maybe; - itemView: Maybe<{ + customViewsIndex: number | null; + search: QueryMode | null; + itemView: { __typename: 'KeystoneAdminUIFieldMetaItemView'; - fieldMode: Maybe; - fieldPosition: Maybe; - }>; + fieldPosition: KeystoneAdminUIFieldMetaItemViewFieldPosition | null; + fieldMode: KeystoneAdminUIFieldMetaItemViewFieldMode | null; + } | null; }>; }>; }; diff --git a/packages/core/src/admin-ui/system/adminMetaSchema.ts b/packages/core/src/admin-ui/system/adminMetaSchema.ts index 4c173f7705f..d721cf711ff 100644 --- a/packages/core/src/admin-ui/system/adminMetaSchema.ts +++ b/packages/core/src/admin-ui/system/adminMetaSchema.ts @@ -2,7 +2,12 @@ import { GraphQLResolveInfo } from 'graphql'; import { ScalarType, EnumType, EnumValue } from '@graphql-ts/schema'; import { QueryMode, KeystoneContext, BaseItem, MaybePromise } from '../../types'; import { graphql as graphqlBoundToKeystoneContext } from '../..'; -import { FieldMetaRootVal, ListMetaRootVal, AdminMetaRootVal } from './createAdminMeta'; +import { + FieldMetaRootVal, + ListMetaRootVal, + AdminMetaRootVal, + FieldGroupMeta, +} from './createAdminMeta'; type Context = KeystoneContext | { isAdminUIBuildProcess: true }; @@ -151,6 +156,17 @@ const KeystoneAdminUIFieldMeta = graphql.object()({ }, }); +const KeystoneAdminUIFieldGroupMeta = graphql.object()({ + name: 'KeystoneAdminUIFieldGroupMeta', + fields: { + label: graphql.field({ type: graphql.nonNull(graphql.String) }), + description: graphql.field({ type: graphql.String }), + fields: graphql.field({ + type: graphql.nonNull(graphql.list(graphql.nonNull(KeystoneAdminUIFieldMeta))), + }), + }, +}); + const KeystoneAdminUISort = graphql.object>()({ name: 'KeystoneAdminUISort', fields: { @@ -187,9 +203,12 @@ const KeystoneAdminUIListMeta = graphql.object()({ fields: graphql.field({ type: graphql.nonNull(graphql.list(graphql.nonNull(KeystoneAdminUIFieldMeta))), }), + groups: graphql.field({ + type: graphql.nonNull(graphql.list(graphql.nonNull(KeystoneAdminUIFieldGroupMeta))), + }), initialSort: graphql.field({ type: KeystoneAdminUISort }), ...contextFunctionField('isHidden', graphql.Boolean), - isSingleton: graphql.field({ type: graphql.Boolean }), + isSingleton: graphql.field({ type: graphql.nonNull(graphql.Boolean) }), }, }); diff --git a/packages/core/src/admin-ui/system/createAdminMeta.ts b/packages/core/src/admin-ui/system/createAdminMeta.ts index e8d803ddd0d..8941e868bb0 100644 --- a/packages/core/src/admin-ui/system/createAdminMeta.ts +++ b/packages/core/src/admin-ui/system/createAdminMeta.ts @@ -39,6 +39,12 @@ export type FieldMetaRootVal = { listView: { fieldMode: ContextFunction<'read' | 'hidden'> }; }; +export type FieldGroupMeta = { + label: string; + description: string | null; + fields: Array; +}; + export type ListMetaRootVal = { key: string; path: string; @@ -51,6 +57,7 @@ export type ListMetaRootVal = { initialSort: { field: string; direction: 'ASC' | 'DESC' } | null; fields: FieldMetaRootVal[]; fieldsByKey: Record; + groups: Array; itemQueryName: string; listQueryName: string; description: string | null; @@ -120,6 +127,7 @@ export function createAdminMeta( path: list.adminUILabels.path, fields: [], fieldsByKey: {}, + groups: [], pageSize: maximumPageSize, initialColumns, initialSort: @@ -212,6 +220,15 @@ export function createAdminMeta( adminMetaRoot.listsByKey[listKey].fields.push(fieldMeta); adminMetaRoot.listsByKey[listKey].fieldsByKey[fieldKey] = fieldMeta; } + for (const group of list.groups) { + adminMetaRoot.listsByKey[listKey].groups.push({ + label: group.label, + description: group.description, + fields: group.fields.map( + fieldKey => adminMetaRoot.listsByKey[listKey].fieldsByKey[fieldKey] + ), + }); + } } // we do this seperately to the above so that fields can check other fields to validate their config or etc. diff --git a/packages/core/src/admin-ui/utils/Fields.tsx b/packages/core/src/admin-ui/utils/Fields.tsx index 99cef7fb6f0..c70995d4296 100644 --- a/packages/core/src/admin-ui/utils/Fields.tsx +++ b/packages/core/src/admin-ui/utils/Fields.tsx @@ -1,8 +1,10 @@ /** @jsxRuntime classic */ /** @jsx jsx */ -import { jsx, Stack } from '@keystone-ui/core'; -import { memo, useMemo } from 'react'; -import { FieldMeta } from '../../types'; +import { jsx, Stack, useTheme, Text } from '@keystone-ui/core'; +import { memo, ReactNode, useContext, useId, useMemo } from 'react'; +import { FieldDescription } from '@keystone-ui/fields'; +import { ButtonContext } from '@keystone-ui/button'; +import { FieldGroupMeta, FieldMeta } from '../../types'; import { Value } from '.'; type RenderFieldProps = { @@ -38,6 +40,7 @@ const RenderField = memo(function RenderField({ type FieldsProps = { fields: Record; + groups?: FieldGroupMeta[]; value: Value; fieldModes?: Record | null; fieldPositions?: Record | null; @@ -55,42 +58,145 @@ export function Fields({ forceValidation, invalidFields, position = 'form', + groups = [], onChange, }: FieldsProps) { - const renderedFields = Object.keys(fields) - .map((fieldPath, index) => { - const field = fields[fieldPath]; - const val = value[fieldPath]; - const fieldMode = fieldModes === null ? 'edit' : fieldModes[fieldPath]; - const fieldPosition = fieldPositions === null ? 'form' : fieldPositions[fieldPath]; - - if (fieldMode === 'hidden') return null; - if (fieldPosition !== position) return null; + const renderedFields = Object.fromEntries( + Object.keys(fields).map((fieldKey, index) => { + const field = fields[fieldKey]; + const val = value[fieldKey]; + const fieldMode = fieldModes === null ? 'edit' : fieldModes[fieldKey]; + const fieldPosition = fieldPositions === null ? 'form' : fieldPositions[fieldKey]; + if (fieldMode === 'hidden') return [fieldKey, null]; + if (fieldPosition !== position) return [fieldKey, null]; if (val.kind === 'error') { - return ( -
+ return [ + fieldKey, +
{field.label}: {val.errors[0].message} -
- ); +
, + ]; } - - return ( + return [ + fieldKey, - ); + />, + ]; }) - .filter(Boolean); + ); + const rendered: ReactNode[] = []; + const fieldGroups = new Map(); + for (const group of groups) { + const state = { group, rendered: false }; + for (const field of group.fields) { + fieldGroups.set(field.path, state); + } + } + for (const field of Object.values(fields)) { + const fieldKey = field.path; + if (fieldGroups.has(fieldKey)) { + const groupState = fieldGroups.get(field.path)!; + if (groupState.rendered) { + continue; + } + groupState.rendered = true; + const { group } = groupState; + const renderedFieldsInGroup = group.fields.map(field => renderedFields[field.path]); + if (renderedFieldsInGroup.every(field => field === null)) { + continue; + } + rendered.push( + + {renderedFieldsInGroup} + + ); + continue; + } + if (renderedFields[fieldKey] === null) { + continue; + } + rendered.push(renderedFields[fieldKey]); + } return ( - {renderedFields} - {renderedFields.length === 0 && 'There are no fields that you can read or edit'} + {rendered.length === 0 ? 'There are no fields that you can read or edit' : rendered} ); } + +function FieldGroup(props: { label: string; description: string | null; children: ReactNode }) { + const descriptionId = useId(); + const labelId = useId(); + const theme = useTheme(); + const buttonSize = 24; + const { useButtonStyles, useButtonTokens, defaults } = useContext(ButtonContext); + const buttonStyles = useButtonStyles({ tokens: useButtonTokens(defaults) }); + const divider = ( +
+ ); + return ( +
+
+ + +
above + css={{ + ...buttonStyles, + 'summary:focus &': buttonStyles[':focus'], + padding: 0, + height: buttonSize, + width: buttonSize, + 'details[open] &': { + transform: 'rotate(90deg)', + }, + }} + > + {downChevron} +
+ {divider} + + {props.label} + +
+
+ +
+ {divider} +
+ {props.description !== null && ( + {props.description} + )} + + {props.children} + +
+ +
+
+ ); +} + +const downChevron = ( + + + +); diff --git a/packages/core/src/admin-ui/utils/useAdminMeta.tsx b/packages/core/src/admin-ui/utils/useAdminMeta.tsx index d69543a3d9e..3b56b747c97 100644 --- a/packages/core/src/admin-ui/utils/useAdminMeta.tsx +++ b/packages/core/src/admin-ui/utils/useAdminMeta.tsx @@ -69,6 +69,7 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { adminMeta.lists.forEach(list => { runtimeAdminMeta.lists[list.key] = { ...list, + groups: [], gqlNames: getGqlNames({ listKey: list.key, pluralGraphQLName: list.listQueryName }), fields: {}, }; @@ -121,6 +122,13 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { }), }; }); + for (const group of list.groups) { + runtimeAdminMeta.lists[list.key].groups.push({ + label: group.label, + description: group.description, + fields: group.fields.map(field => runtimeAdminMeta.lists[list.key].fields[field.path]), + }); + } }); if (typeof window !== 'undefined' && !adminMetaFromLocalStorage) { localStorage.setItem( diff --git a/packages/core/src/admin-ui/utils/useCreateItem.ts b/packages/core/src/admin-ui/utils/useCreateItem.ts index a8c88707cf5..9ebc088c252 100644 --- a/packages/core/src/admin-ui/utils/useCreateItem.ts +++ b/packages/core/src/admin-ui/utils/useCreateItem.ts @@ -82,6 +82,7 @@ export function useCreateItem(list: ListMeta): CreateItemHookResult { error, props: { fields: list.fields, + groups: list.groups, fieldModes: createViewFieldModes.state === 'loaded' ? createViewFieldModes.lists[list.key] : null, forceValidation, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7d650b712ad..366f6461a39 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,3 +1,3 @@ -export { list, config } from './schema/schema'; +export { list, config, group } from './schema/schema'; export type { ListSchemaConfig, ListConfig, ExtendGraphqlSchema, BaseFields } from './types'; export { graphql } from './types/schema'; diff --git a/packages/core/src/lib/core/prisma-schema.ts b/packages/core/src/lib/core/prisma-schema.ts index ab744ce4908..7157025a458 100644 --- a/packages/core/src/lib/core/prisma-schema.ts +++ b/packages/core/src/lib/core/prisma-schema.ts @@ -1,11 +1,7 @@ import { ScalarDBField, ScalarDBFieldDefault, DatabaseProvider } from '../../types'; import { ResolvedDBField } from './resolve-relationships'; import { InitialisedList } from './types-for-lists'; -import { getDBFieldKeyForFieldOnMultiField } from './utils'; - -function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) { - return a.length === b.length && a.every((x, i) => x === b[i]); -} +import { areArraysEqual, getDBFieldKeyForFieldOnMultiField } from './utils'; const modifiers = { required: '', diff --git a/packages/core/src/lib/core/types-for-lists.ts b/packages/core/src/lib/core/types-for-lists.ts index c4946003524..0b0d1338c5f 100644 --- a/packages/core/src/lib/core/types-for-lists.ts +++ b/packages/core/src/lib/core/types-for-lists.ts @@ -23,7 +23,7 @@ import { parseListAccessControl, parseFieldAccessControl, } from './access-control'; -import { getNamesFromList } from './utils'; +import { areArraysEqual, getNamesFromList } from './utils'; import { ResolvedDBField, resolveRelationships } from './resolve-relationships'; import { outputTypeField } from './queries/output-field'; import { assertFieldsValid } from './field-assertions'; @@ -44,10 +44,17 @@ export type InitialisedField = Omit; /** This will include the opposites to one-sided relationships */ resolvedDbFields: Record; + groups: FieldGroupConfig[]; pluralGraphQLName: string; types: GraphQLTypesForList; access: ResolvedListAccessControl; @@ -148,7 +155,26 @@ function getListsWithInitialisedFields( const intermediateList = intermediateLists[listKey]; const resultFields: Record = {}; - for (const [fieldKey, fieldFunc] of Object.entries(list.fields)) { + const groups: FieldGroupConfig[] = []; + + const fieldKeys = Object.keys(list.fields); + for (const [idx, [fieldKey, fieldFunc]] of Object.entries(list.fields).entries()) { + if (fieldKey.startsWith('__group')) { + const group = fieldFunc as any; + if ( + typeof group === 'object' && + group !== null && + typeof group.label === 'string' && + (group.description === null || typeof group.description === 'string') && + Array.isArray(group.fields) && + areArraysEqual(group.fields, fieldKeys.slice(idx + 1, idx + 1 + group.fields.length)) + ) { + groups.push(group); + continue; + } + throw new Error(`unexpected value for a group at ${listKey}.${fieldKey}`); + } + if (typeof fieldFunc !== 'function') { throw new Error(`The field at ${listKey}.${fieldKey} does not provide a function`); } @@ -215,12 +241,12 @@ function getListsWithInitialisedFields( access: parseListAccessControl(list.access), dbMap: list.db?.map, types: listGraphqlTypes[listKey].types, - ui: { labelField, searchFields, searchableFields: new Map(), }, + groups, hooks: list.hooks || {}, listKey, diff --git a/packages/core/src/lib/core/utils.ts b/packages/core/src/lib/core/utils.ts index 0178a14d04b..d2f3f755d87 100644 --- a/packages/core/src/lib/core/utils.ts +++ b/packages/core/src/lib/core/utils.ts @@ -196,3 +196,7 @@ export function getPrismaNamespace(context: KeystoneContext) { } return limit; } + +export function areArraysEqual(a: readonly unknown[], b: readonly unknown[]) { + return a.length === b.length && a.every((x, i) => x === b[i]); +} diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index 85896abbb8e..1c974c367dc 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -36,6 +36,7 @@ function getSudoGraphQLSchema(config: KeystoneConfig) { graphql: { ...(list.graphql || {}), omit: [] }, fields: Object.fromEntries( Object.entries(list.fields).map(([fieldKey, field]) => { + if (fieldKey.startsWith('__group')) return [fieldKey, field]; return [ fieldKey, (data: FieldData) => { diff --git a/packages/core/src/schema/schema.ts b/packages/core/src/schema/schema.ts index 70edbfa9f99..f6d7f27e53e 100644 --- a/packages/core/src/schema/schema.ts +++ b/packages/core/src/schema/schema.ts @@ -1,3 +1,4 @@ +import type { FieldGroupConfig } from '../lib/core/types-for-lists'; import type { BaseFields, BaseListTypeInfo, @@ -10,6 +11,26 @@ export function config(config: KeystoneCo return config; } +let i = 0; +export function group< + Fields extends BaseFields, + ListTypeInfo extends BaseListTypeInfo +>(config: { label: string; description?: string; fields: Fields }): Fields { + const keys = Object.keys(config.fields); + if (keys.some(key => key.startsWith('__group'))) { + throw new Error('groups cannot be nested'); + } + const groupConfig: FieldGroupConfig = { + fields: keys, + label: config.label, + description: config.description ?? null, + }; + return { + [`__group${i++}`]: groupConfig, + ...config.fields, + }; +} + export function list< Fields extends BaseFields, ListTypeInfo extends BaseListTypeInfo diff --git a/packages/core/src/scripts/tests/fixtures/basic-project/schema.graphql b/packages/core/src/scripts/tests/fixtures/basic-project/schema.graphql index 7556010546f..2967440a834 100644 --- a/packages/core/src/scripts/tests/fixtures/basic-project/schema.graphql +++ b/packages/core/src/scripts/tests/fixtures/basic-project/schema.graphql @@ -125,9 +125,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -184,6 +185,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/packages/core/src/types/admin-meta.ts b/packages/core/src/types/admin-meta.ts index 51f0b94822e..8d127c9e02f 100644 --- a/packages/core/src/types/admin-meta.ts +++ b/packages/core/src/types/admin-meta.ts @@ -91,6 +91,12 @@ export type FieldMeta = { }; }; +export type FieldGroupMeta = { + label: string; + description: string | null; + fields: FieldMeta[]; +}; + export type ListMeta = { key: string; path: string; @@ -104,6 +110,7 @@ export type ListMeta = { labelField: string; initialSort: null | { direction: 'ASC' | 'DESC'; field: string }; fields: { [path: string]: FieldMeta }; + groups: FieldGroupMeta[]; isSingleton: boolean; }; diff --git a/tests/api-tests/admin-meta.test.ts b/tests/api-tests/admin-meta.test.ts index e3ae60f2720..8f8cf8b5f8e 100644 --- a/tests/api-tests/admin-meta.test.ts +++ b/tests/api-tests/admin-meta.test.ts @@ -93,6 +93,7 @@ test( viewsIndex: 1, }, ], + groups: [], initialColumns: ['name'], initialSort: null, itemQueryName: 'User', diff --git a/tests/api-tests/field-groups.test.ts b/tests/api-tests/field-groups.test.ts new file mode 100644 index 00000000000..3505ece0765 --- /dev/null +++ b/tests/api-tests/field-groups.test.ts @@ -0,0 +1,57 @@ +import { group, list } from '@keystone-6/core'; +import { allowAll } from '@keystone-6/core/access'; +import { getContext } from '@keystone-6/core/context'; +import { integer, text } from '@keystone-6/core/fields'; +import { apiTestConfig } from './utils'; + +test('errors with nested field groups', () => { + expect(() => + getContext( + apiTestConfig({ + lists: { + User: list({ + access: allowAll, + fields: { + name: text(), + ...group({ + label: 'Group 1', + fields: { + ...group({ + label: 'Group 2', + fields: { + something: integer(), + }, + }), + }, + }), + }, + }), + }, + }), + {} + ) + ).toThrowErrorMatchingInlineSnapshot(`"groups cannot be nested"`); +}); + +test('errors if you write a group manually differently to the group function', () => { + expect(() => + getContext( + apiTestConfig({ + lists: { + User: list({ + access: allowAll, + fields: { + name: text(), + __group0: { + fields: ['name'], + label: 'Group 1', + description: null, + } as any, + }, + }), + }, + }), + {} + ) + ).toThrowErrorMatchingInlineSnapshot(`"unexpected value for a group at User.__group0"`); +}); diff --git a/tests/sandbox/configs/all-the-things.ts b/tests/sandbox/configs/all-the-things.ts index 9f3eb49b284..659af8f09ae 100644 --- a/tests/sandbox/configs/all-the-things.ts +++ b/tests/sandbox/configs/all-the-things.ts @@ -1,4 +1,4 @@ -import { list, graphql, config } from '@keystone-6/core'; +import { list, graphql, config, group } from '@keystone-6/core'; import { allowAll } from '@keystone-6/core/access'; import { checkbox, @@ -28,11 +28,14 @@ export const lists = { Thing: list({ access: allowAll, fields: { - checkbox: checkbox({ ui: { description } }), - password: password({ ui: { description } }), - toOneRelationship: relationship({ - ref: 'User', - ui: { description }, + ...group({ + label: 'Some group', + description: 'Some group description', + fields: { + checkbox: checkbox({ ui: { description } }), + password: password({ ui: { description } }), + toOneRelationship: relationship({ ref: 'User', ui: { description } }), + }, }), toOneRelationshipAlternateLabel: relationship({ ref: 'User', diff --git a/tests/sandbox/schema.graphql b/tests/sandbox/schema.graphql index b35a2b15e5b..606fe608c45 100644 --- a/tests/sandbox/schema.graphql +++ b/tests/sandbox/schema.graphql @@ -581,9 +581,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -640,6 +641,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/tests/test-projects/basic/schema.graphql b/tests/test-projects/basic/schema.graphql index 3b4e8f060e7..d1db30bf665 100644 --- a/tests/test-projects/basic/schema.graphql +++ b/tests/test-projects/basic/schema.graphql @@ -302,9 +302,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -361,6 +362,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/tests/test-projects/crud-notifications/schema.graphql b/tests/test-projects/crud-notifications/schema.graphql index fc8b564e3c4..021b2a9c630 100644 --- a/tests/test-projects/crud-notifications/schema.graphql +++ b/tests/test-projects/crud-notifications/schema.graphql @@ -253,9 +253,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -312,6 +313,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! diff --git a/tests/test-projects/live-reloading/schema.graphql b/tests/test-projects/live-reloading/schema.graphql index 6f371e8f37d..747009ab606 100644 --- a/tests/test-projects/live-reloading/schema.graphql +++ b/tests/test-projects/live-reloading/schema.graphql @@ -126,9 +126,10 @@ type KeystoneAdminUIListMeta { pageSize: Int! labelField: String! fields: [KeystoneAdminUIFieldMeta!]! + groups: [KeystoneAdminUIFieldGroupMeta!]! initialSort: KeystoneAdminUISort isHidden: Boolean! - isSingleton: Boolean + isSingleton: Boolean! } type KeystoneAdminUIFieldMeta { @@ -185,6 +186,12 @@ enum QueryMode { insensitive } +type KeystoneAdminUIFieldGroupMeta { + label: String! + description: String + fields: [KeystoneAdminUIFieldMeta!]! +} + type KeystoneAdminUISort { field: String! direction: KeystoneAdminUISortDirection! From 09f729ddb16156c9d2394ab7dabd02e76930545b Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Wed, 16 Nov 2022 14:23:56 +1100 Subject: [PATCH 2/3] update changeset copy --- .changeset/warm-gifts-change.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/warm-gifts-change.md b/.changeset/warm-gifts-change.md index 301efaf2db6..4b6684c8cae 100644 --- a/.changeset/warm-gifts-change.md +++ b/.changeset/warm-gifts-change.md @@ -2,4 +2,4 @@ '@keystone-6/core': minor --- -Adds `group` function to allow grouping of fields in the Admin UI +Adds a new `group` function for grouping fields in the Admin UI From 11572351aa7a161c300f12e1e25ea71dda1a7115 Mon Sep 17 00:00:00 2001 From: Daniel Cousens <413395+dcousens@users.noreply.github.com> Date: Wed, 16 Nov 2022 14:34:21 +1100 Subject: [PATCH 3/3] update documentation copy --- docs/pages/docs/fields/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/docs/fields/overview.md b/docs/pages/docs/fields/overview.md index 2f08b991141..20aab84ce91 100644 --- a/docs/pages/docs/fields/overview.md +++ b/docs/pages/docs/fields/overview.md @@ -146,7 +146,7 @@ export default config({ ## Groups -Fields can be grouped together in the Admin UI using the `group` function with a `label` and optionally `description`. +Fields can be grouped together in the Admin UI using the `group` function, with a customisable `label` and `description`. ```typescript import { config, list, group } from '@keystone-6/core';