From 57e9527c39771ce8bbd0bed960fddfe6b3ee01a2 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Wed, 23 Nov 2022 15:51:41 +1000 Subject: [PATCH] Add new Structure field type (#7936) Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com> Co-authored-by: Daniel Cousens --- .changeset/late-ties-multiply.md | 5 + .changeset/late-ties-search.md | 5 + docs/pages/docs/guides/document-fields.md | 5 +- packages/fields-document/package.json | 11 +- .../DocumentEditor/component-blocks/api.tsx | 274 +++++++--- .../component-blocks/form-from-preview.tsx | 128 ++++- .../component-blocks/get-value.ts | 127 +++++ .../DocumentEditor/primitives/orderable.tsx | 13 +- .../fields-document/src/component-blocks.tsx | 1 + packages/fields-document/src/index.ts | 2 + .../src/structure-graphql-input.tsx | 506 ++++++++++++++++++ .../src/structure-graphql-output.tsx | 197 +++++++ .../fields-document/src/structure-views.tsx | 135 +++++ packages/fields-document/src/structure.ts | 143 +++++ .../structure-views/package.json | 4 + tests/sandbox/configs/all-the-things.ts | 9 +- tests/sandbox/schema.graphql | 70 +++ tests/sandbox/schema.prisma | 2 + tests/sandbox/structure-nested.tsx | 40 ++ tests/sandbox/structure.tsx | 14 + 20 files changed, 1596 insertions(+), 95 deletions(-) create mode 100644 .changeset/late-ties-multiply.md create mode 100644 .changeset/late-ties-search.md create mode 100644 packages/fields-document/src/DocumentEditor/component-blocks/get-value.ts create mode 100644 packages/fields-document/src/structure-graphql-input.tsx create mode 100644 packages/fields-document/src/structure-graphql-output.tsx create mode 100644 packages/fields-document/src/structure-views.tsx create mode 100644 packages/fields-document/src/structure.ts create mode 100644 packages/fields-document/structure-views/package.json create mode 100644 tests/sandbox/structure-nested.tsx create mode 100644 tests/sandbox/structure.tsx diff --git a/.changeset/late-ties-multiply.md b/.changeset/late-ties-multiply.md new file mode 100644 index 00000000000..ee1aee3fb05 --- /dev/null +++ b/.changeset/late-ties-multiply.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/fields-document": minor +--- + +Adds a new `integer` component field type diff --git a/.changeset/late-ties-search.md b/.changeset/late-ties-search.md new file mode 100644 index 00000000000..1a543d1c0b1 --- /dev/null +++ b/.changeset/late-ties-search.md @@ -0,0 +1,5 @@ +--- +"@keystone-6/fields-document": minor +--- + +Adds a new `structure` field type, a composable JSON data structure with a powerful GraphQL API diff --git a/docs/pages/docs/guides/document-fields.md b/docs/pages/docs/guides/document-fields.md index 2f58c83f7c7..74d97c7a6a2 100644 --- a/docs/pages/docs/guides/document-fields.md +++ b/docs/pages/docs/guides/document-fields.md @@ -687,6 +687,9 @@ component({ `@keystone-6/core/component-blocks` ships with a set of form fields for common purposes: - `fields.text({ label: '...', defaultValue: '...' })` +{% if $nextRelease %} +- `fields.integer({ label: '...', defaultValue: '...' })` +{% /if %} - `fields.url({ label: '...', defaultValue: '...' })` - `fields.select({ label: '...', options: [{ label:'A', value:'a' }, { label:'B', value:'b' }] defaultValue: 'a' })` - `fields.checkbox({ label: '...', defaultValue: false })` @@ -869,4 +872,4 @@ heading="Document Field Demo" href="/docs/guides/document-field-demo" %} Test drive the many features of Keystone’s Document field on this website. {% /well %} -{% /related-content %} \ No newline at end of file +{% /related-content %} diff --git a/packages/fields-document/package.json b/packages/fields-document/package.json index e77cbf91f37..b57aa17c85a 100644 --- a/packages/fields-document/package.json +++ b/packages/fields-document/package.json @@ -18,6 +18,10 @@ "module": "./primitives/dist/keystone-6-fields-document-primitives.esm.js", "default": "./primitives/dist/keystone-6-fields-document-primitives.cjs.js" }, + "./structure-views": { + "module": "./structure-views/dist/keystone-6-fields-document-structure-views.esm.js", + "default": "./structure-views/dist/keystone-6-fields-document-structure-views.cjs.js" + }, "./component-blocks": { "module": "./component-blocks/dist/keystone-6-fields-document-component-blocks.esm.js", "default": "./component-blocks/dist/keystone-6-fields-document-component-blocks.cjs.js" @@ -28,14 +32,16 @@ "dist", "component-blocks", "primitives", - "views" + "views", + "structure-views" ], "preconstruct": { "entrypoints": [ "component-blocks.tsx", "index.ts", "primitives.ts", - "views.tsx" + "views.tsx", + "structure-views.tsx" ] }, "peerDependencies": { @@ -53,6 +59,7 @@ "@keystone-ui/core": "^5.0.2", "@keystone-ui/fields": "^7.1.2", "@keystone-ui/icons": "^6.0.2", + "@keystone-ui/modals": "^6.0.2", "@keystone-ui/popover": "^6.0.2", "@keystone-ui/tooltip": "^6.0.2", "@types/react": "^18.0.9", diff --git a/packages/fields-document/src/DocumentEditor/component-blocks/api.tsx b/packages/fields-document/src/DocumentEditor/component-blocks/api.tsx index 4b2fd78a7cb..387dfa03791 100644 --- a/packages/fields-document/src/DocumentEditor/component-blocks/api.tsx +++ b/packages/fields-document/src/DocumentEditor/component-blocks/api.tsx @@ -1,5 +1,6 @@ /** @jsxRuntime classic */ /** @jsx jsx */ +import { graphql } from '@keystone-6/core'; import { jsx } from '@keystone-ui/core'; import { FieldContainer, @@ -52,6 +53,21 @@ export type FormField = { validate(value: unknown): boolean; }; +export type FormFieldWithGraphQLField = FormField< + Value, + Options +> & { + graphql: { + output: graphql.Field< + { value: Value }, + Record>, + graphql.OutputType, + 'value' + >; + input: graphql.NullableInputType; + }; +}; + type InlineMarksConfig = | 'inherit' | { @@ -100,6 +116,8 @@ export type ChildField = { export type ArrayField = { kind: 'array'; element: ElementField; + // this is written with unknown to avoid typescript being annoying about circularity or variance things + label?(props: unknown): string; }; export type RelationshipField = { @@ -128,14 +146,39 @@ export type ConditionalField< values: ConditionalValues; }; +// this is written like this rather than ArrayField to avoid TypeScript erroring about circularity +type ArrayFieldInComponentSchema = { + kind: 'array'; + element: ComponentSchema; + // this is written with unknown to avoid typescript being annoying about circularity or variance things + label?(props: unknown): string; +}; + export type ComponentSchema = | ChildField | FormField | ObjectField | ConditionalField, { [key: string]: ComponentSchema }> | RelationshipField - // this is written like this rather than ArrayField to avoid TypeScript erroring about circularity - | { kind: 'array'; element: ComponentSchema }; + | ArrayFieldInComponentSchema; + +// this is written like this rather than ArrayField to avoid TypeScript erroring about circularity +type ArrayFieldInComponentSchemaForGraphQL = { + kind: 'array'; + element: ComponentSchemaForGraphQL; + // this is written with unknown to avoid typescript being annoying about circularity or variance things + label?(props: unknown): string; +}; + +export type ComponentSchemaForGraphQL = + | FormFieldWithGraphQLField + | ObjectField> + | ConditionalField< + FormFieldWithGraphQLField, + { [key: string]: ComponentSchemaForGraphQL } + > + | RelationshipField + | ArrayFieldInComponentSchemaForGraphQL; export const fields = { text({ @@ -144,7 +187,7 @@ export const fields = { }: { label: string; defaultValue?: string; - }): FormField { + }): FormFieldWithGraphQLField { return { kind: 'form', Input({ value, onChange, autoFocus }) { @@ -166,6 +209,57 @@ export const fields = { validate(value) { return typeof value === 'string'; }, + graphql: { + input: graphql.String, + output: graphql.field({ type: graphql.String }), + }, + }; + }, + integer({ + label, + defaultValue = 0, + }: { + label: string; + defaultValue?: number; + }): FormFieldWithGraphQLField { + const validate = (value: unknown) => { + return typeof value === 'number' && Number.isFinite(value); + }; + return { + kind: 'form', + Input({ value, onChange, autoFocus, forceValidation }) { + const [blurred, setBlurred] = useState(false); + const [inputValue, setInputValue] = useState(String(value)); + const showValidation = forceValidation || (blurred && !validate(value)); + + return ( + + {label} + setBlurred(true)} + autoFocus={autoFocus} + value={inputValue} + onChange={event => { + const raw = event.target.value; + setInputValue(raw); + if (/^[+-]?\d+$/.test(raw)) { + onChange(Number(raw)); + } else { + onChange(NaN); + } + }} + /> + {showValidation && Please specify an integer} + + ); + }, + options: undefined, + defaultValue, + validate, + graphql: { + input: graphql.Int, + output: graphql.field({ type: graphql.Int }), + }, }; }, url({ @@ -174,7 +268,7 @@ export const fields = { }: { label: string; defaultValue?: string; - }): FormField { + }): FormFieldWithGraphQLField { const validate = (value: unknown) => { return typeof value === 'string' && (value === '' || isValidURL(value)); }; @@ -187,9 +281,7 @@ export const fields = { {label} { - setBlurred(true); - }} + onBlur={() => setBlurred(true)} autoFocus={autoFocus} value={value} onChange={event => { @@ -203,6 +295,10 @@ export const fields = { options: undefined, defaultValue, validate, + graphql: { + input: graphql.String, + output: graphql.field({ type: graphql.String }), + }, }; }, select