Skip to content

Commit

Permalink
Introduce structure field
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed Nov 1, 2022
1 parent 8b03443 commit f18782d
Show file tree
Hide file tree
Showing 20 changed files with 1,392 additions and 45 deletions.
4 changes: 4 additions & 0 deletions packages/fields-document/form/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "dist/keystone-6-fields-document-form.cjs.js",
"module": "dist/keystone-6-fields-document-form.esm.js"
}
7 changes: 5 additions & 2 deletions packages/fields-document/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,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": {
Expand All @@ -34,6 +36,7 @@
"@keystone-ui/core": "^5.0.1",
"@keystone-ui/fields": "^7.1.1",
"@keystone-ui/icons": "^6.0.1",
"@keystone-ui/modals": "^6.0.1",
"@keystone-ui/popover": "^6.0.1",
"@keystone-ui/tooltip": "^6.0.1",
"@types/react": "^18.0.9",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/** @jsxRuntime classic */
/** @jsx jsx */
import { graphql } from '@keystone-6/core';
import { jsx } from '@keystone-ui/core';
import {
FieldContainer,
Expand Down Expand Up @@ -52,6 +53,21 @@ export type FormField<Value extends FormFieldValue, Options> = {
validate(value: unknown): boolean;
};

export type FormFieldWithGraphQLField<Value extends FormFieldValue, Options> = FormField<
Value,
Options
> & {
graphql: {
output: graphql.Field<
{ value: Value },
Record<string, graphql.Arg<graphql.InputType, boolean>>,
graphql.OutputType,
'value'
>;
input: graphql.NullableInputType;
};
};

type InlineMarksConfig =
| 'inherit'
| {
Expand Down Expand Up @@ -137,14 +153,25 @@ export type ComponentSchema =
// this is written like this rather than ArrayField<ComponentSchema> to avoid TypeScript erroring about circularity
| { kind: 'array'; element: ComponentSchema };

export type ComponentSchemaForGraphQL =
| FormFieldWithGraphQLField<any, any>
| ObjectField<Record<string, ComponentSchemaForGraphQL>>
| ConditionalField<
FormFieldWithGraphQLField<any, any>,
{ [key: string]: ComponentSchemaForGraphQL }
>
| RelationshipField<boolean>
// this is written like this rather than ArrayField<ComponentSchemaForGraphQL> to avoid TypeScript erroring about circularity
| { kind: 'array'; element: ComponentSchemaForGraphQL };

export const fields = {
text({
label,
defaultValue = '',
}: {
label: string;
defaultValue?: string;
}): FormField<string, undefined> {
}): FormFieldWithGraphQLField<string, undefined> {
return {
kind: 'form',
Input({ value, onChange, autoFocus }) {
Expand All @@ -166,6 +193,10 @@ export const fields = {
validate(value) {
return typeof value === 'string';
},
graphql: {
input: graphql.String,
output: graphql.field({ type: graphql.String }),
},
};
},
url({
Expand All @@ -174,7 +205,7 @@ export const fields = {
}: {
label: string;
defaultValue?: string;
}): FormField<string, undefined> {
}): FormFieldWithGraphQLField<string, undefined> {
const validate = (value: unknown) => {
return typeof value === 'string' && (value === '' || isValidURL(value));
};
Expand Down Expand Up @@ -203,6 +234,10 @@ export const fields = {
options: undefined,
defaultValue,
validate,
graphql: {
input: graphql.String,
output: graphql.field({ type: graphql.String }),
},
};
},
select<Option extends { label: string; value: string }>({
Expand All @@ -213,7 +248,7 @@ export const fields = {
label: string;
options: readonly Option[];
defaultValue: Option['value'];
}): FormField<Option['value'], readonly Option[]> {
}): FormFieldWithGraphQLField<Option['value'], readonly Option[]> {
const optionValuesSet = new Set(options.map(x => x.value));
if (!optionValuesSet.has(defaultValue)) {
throw new Error(
Expand Down Expand Up @@ -244,6 +279,16 @@ export const fields = {
validate(value) {
return typeof value === 'string' && optionValuesSet.has(value);
},
graphql: {
input: graphql.String,
output: graphql.field({
type: graphql.String,
// TODO: investigate why this resolve is required here
resolve({ value }) {
return value;
},
}),
},
};
},
multiselect<Option extends { label: string; value: string }>({
Expand All @@ -254,7 +299,7 @@ export const fields = {
label: string;
options: readonly Option[];
defaultValue: readonly Option['value'][];
}): FormField<readonly Option['value'][], readonly Option[]> {
}): FormFieldWithGraphQLField<readonly Option['value'][], readonly Option[]> {
const valuesToOption = new Map(options.map(x => [x.value, x]));
return {
kind: 'form',
Expand All @@ -281,6 +326,16 @@ export const fields = {
value.every(value => typeof value === 'string' && valuesToOption.has(value))
);
},
graphql: {
input: graphql.list(graphql.nonNull(graphql.String)),
output: graphql.field({
type: graphql.list(graphql.nonNull(graphql.String)),
// TODO: investigate why this resolve is required here
resolve({ value }) {
return value;
},
}),
},
};
},
checkbox({
Expand All @@ -289,7 +344,7 @@ export const fields = {
}: {
label: string;
defaultValue?: boolean;
}): FormField<boolean, undefined> {
}): FormFieldWithGraphQLField<boolean, undefined> {
return {
kind: 'form',
Input({ value, onChange, autoFocus }) {
Expand All @@ -312,6 +367,10 @@ export const fields = {
validate(value) {
return typeof value === 'boolean';
},
graphql: {
input: graphql.Boolean,
output: graphql.field({ type: graphql.Boolean }),
},
};
},
empty(): FormField<null, undefined> {
Expand Down Expand Up @@ -384,8 +443,8 @@ export const fields = {
},
};
},
object<Fields extends Record<string, ComponentSchema>>(fields: Fields): ObjectField<Fields> {
return { kind: 'object', fields };
object<Value extends Record<string, ComponentSchema>>(value: Value): ObjectField<Value> {
return { kind: 'object', fields: value };
},
conditional<
DiscriminantField extends FormField<string | boolean, any>,
Expand All @@ -394,7 +453,8 @@ export const fields = {
}
>(
discriminant: DiscriminantField,
values: ConditionalValues
values: ConditionalValues,
opts?: {}
): ConditionalField<DiscriminantField, ConditionalValues> {
if (
(discriminant.validate('true') || discriminant.validate('false')) &&
Expand Down Expand Up @@ -463,7 +523,7 @@ type ChildFieldPreviewProps<Schema extends ChildField, ChildFieldElement> = {
};

type FormFieldPreviewProps<Schema extends FormField<any, any>> = {
readonly value: Schema['options'];
readonly value: Schema['defaultValue'];
onChange(value: Schema['defaultValue']): void;
readonly options: Schema['options'];
readonly schema: Schema;
Expand Down Expand Up @@ -500,15 +560,14 @@ type ConditionalFieldPreviewProps<
};
}[keyof Schema['values']];

// this is a separate type so that this is distributive
type RelationshipDataType<Many extends boolean> = Many extends true
? readonly HydratedRelationshipData[]
: HydratedRelationshipData | null;

type RelationshipFieldPreviewProps<Schema extends RelationshipField<boolean>> = {
readonly value: Schema['many'] extends true
? readonly HydratedRelationshipData[]
: HydratedRelationshipData | null;
onChange(
relationshipData: Schema['many'] extends true
? readonly HydratedRelationshipData[]
: HydratedRelationshipData | null
): void;
readonly value: RelationshipDataType<Schema['many']>;
onChange(relationshipData: RelationshipDataType<Schema['many']>): void;
readonly schema: Schema;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import { useKeystone } from '@keystone-6/core/admin-ui/context';
import { RelationshipSelect } from '@keystone-6/core/fields/types/relationship/views/RelationshipSelect';
import { Button } from '@keystone-ui/button';
import { Box, jsx, Stack } from '@keystone-ui/core';
import { jsx, Stack } from '@keystone-ui/core';
import { FieldContainer, FieldLabel } from '@keystone-ui/fields';
import { memo, useMemo } from 'react';
import { PlusCircleIcon } from '@keystone-ui/icons/icons/PlusCircleIcon';
import { AlertDialog } from '@keystone-ui/modals';
import { memo, useCallback, useMemo, useState } from 'react';
import { DragHandle, OrderableItem, OrderableList, RemoveButton } from '../primitives/orderable';
import {
ArrayField,
Expand All @@ -17,6 +19,8 @@ import {
RelationshipData,
RelationshipField,
} from './api';
import { previewPropsToValue, setValueToPreviewProps } from './get-value';
import { createGetPreviewProps } from './preview-props';
import { assertNever } from './utils';

type DefaultFieldProps<Key> = GenericPreviewProps<
Expand All @@ -36,12 +40,15 @@ function ArrayFieldPreview(props: DefaultFieldProps<'array'>) {
})}
</OrderableList>
<Button
autoFocus={props.autoFocus}
onClick={() => {
props.onChange([...props.elements.map(x => ({ key: x.key })), { key: undefined }]);
}}
autoFocus={props.autoFocus}
tone="active"
>
Add
<Stack gap="small" across>
<PlusCircleIcon size="smallish" /> <span>Add</span>
</Stack>
</Button>
</Stack>
);
Expand Down Expand Up @@ -159,7 +166,7 @@ export type NonChildFieldComponentSchema =
| RelationshipField<boolean>
| ArrayField<ComponentSchema>;

function isNonChildFieldPreviewProps(
export function isNonChildFieldPreviewProps(
props: GenericPreviewProps<ComponentSchema, unknown>
): props is GenericPreviewProps<NonChildFieldComponentSchema, unknown> {
return props.schema.kind !== 'child';
Expand Down Expand Up @@ -189,19 +196,86 @@ const OrderableItemInForm = memo(function OrderableItemInForm(
elementKey: string;
}
) {
const [modalState, setModalState] = useState<
{ state: 'open'; value: unknown } | { state: 'closed' }
>({ state: 'closed' });
const onModalChange = useCallback(
(cb: (value: unknown) => unknown) => {
setModalState(state => {
if (state.state === 'open') {
return { state: 'open', value: cb(state.value) };
}
return state;
});
},
[setModalState]
);
return (
<OrderableItem elementKey={props.elementKey}>
<div css={{ display: 'flex', justifyContent: 'space-between' }}>
<DragHandle />
<RemoveButton />
</div>
<Box paddingX="medium" paddingBottom="medium">
{isNonChildFieldPreviewProps(props) && <FormValueContentFromPreviewProps {...props} />}
</Box>
<Stack gap="medium">
<div css={{ display: 'flex', gap: 4 }}>
<Stack across gap="xsmall" align="center" css={{ cursor: 'pointer' }}>
<DragHandle />
</Stack>
<Button
weight="none"
onClick={() => {
setModalState({ state: 'open', value: previewPropsToValue(props) });
}}
css={{ flexGrow: 1, justifyContent: 'start' }}
>
<span css={{ fontSize: 16, fontWeight: 'bold', textAlign: 'start' }}>Item</span>
</Button>
<RemoveButton />
</div>
{isNonChildFieldPreviewProps(props) && (
<AlertDialog
title={`Edit Item`}
actions={{
confirm: {
action: () => {
if (modalState.state === 'open') {
setValueToPreviewProps(modalState.value, props);
setModalState({ state: 'closed' });
}
},
label: 'Done',
},
cancel: {
action: () => {
setModalState({ state: 'closed' });
},
label: 'Cancel',
},
}}
isOpen={modalState.state === 'open'}
>
{modalState.state === 'open' && (
<ArrayFieldItemModelContent
onChange={onModalChange}
schema={props.schema}
value={modalState.value}
/>
)}
</AlertDialog>
)}
</Stack>
</OrderableItem>
);
});

function ArrayFieldItemModelContent(props: {
schema: NonChildFieldComponentSchema;
value: unknown;
onChange: (cb: (value: unknown) => unknown) => void;
}) {
const previewProps = useMemo(
() => createGetPreviewProps(props.schema, props.onChange, () => undefined),
[props.schema, props.onChange]
)(props.value);
return <FormValueContentFromPreviewProps {...previewProps} />;
}

function findFocusableObjectFieldKey(schema: ObjectField): string | undefined {
for (const [key, innerProp] of Object.entries(schema.fields)) {
const childFocusable = canFieldBeFocused(innerProp);
Expand Down
Loading

0 comments on commit f18782d

Please sign in to comment.