Skip to content

Commit

Permalink
Select field updates (#6638)
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown authored Sep 23, 2021
1 parent a517b1a commit 7f5caff
Show file tree
Hide file tree
Showing 34 changed files with 468 additions and 171 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-clouds-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/keystone': major
---

In the `select` field, `dataType` has been renamed to `type`, `defaultValue` is now a static value and `isRequired` has moved to `validation.isRequired`. The `select` field can also be made non-nullable at the database-level with the `isNullable` option which defaults to `true`. `graphql.read.isNonNull` can also be set if the field has `isNullable: false` and you have no read access control and you don't intend to add any in the future, it will make the GraphQL output field non-nullable. `graphql.create.isNonNull` can also be set if you have no create access control and you don't intend to add any in the future, it will make the GraphQL create input field non-nullable. The `select` can now also be cleared in the Admin UI when `ui.displayMode` is `segmented-control`.
5 changes: 5 additions & 0 deletions .changeset/silver-wolves-tap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-ui/segmented-control': major
---

Removed uncontrolled input behaviour
35 changes: 13 additions & 22 deletions design-system/packages/segmented-control/src/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@
*/

import {
ChangeEvent,
ChangeEventHandler,
HTMLAttributes,
ReactNode,
forwardRef,
useEffect,
useRef,
useState,
InputHTMLAttributes,
} from 'react';

import {
Expand All @@ -29,7 +29,6 @@ import {
jsx,
ManagedChangeHandler,
useId,
useManagedState,
useTheme,
VisuallyHidden,
css,
Expand All @@ -47,14 +46,12 @@ type SegmentedControlProps = {
animate?: boolean;
/** Whether the controls should take up the full width of their container. */
fill?: boolean;
/** Provide an initial index for an uncontrolled segmented control. */
initialIndex?: Index;
/** Function to be called when one of the segments is selected. */
onChange?: ManagedChangeHandler<Index>;
onChange: ManagedChangeHandler<Index>;
/** Provide labels for each segment. */
segments: string[];
/** The the selected index of the segmented control. */
selectedIndex?: Index;
selectedIndex: Index | undefined;
/** The size of the controls. */
size?: SizeKey;
/** The width of the controls. */
Expand All @@ -64,25 +61,15 @@ type SegmentedControlProps = {
export const SegmentedControl = ({
animate = false,
fill = false,
initialIndex: initialIndexProp = -1,
onChange: onChangeProp,
onChange,
segments,
size = 'medium',
width = 'large',
selectedIndex: selectedIndexProp,
selectedIndex,
...props
}: SegmentedControlProps) => {
const rootRef = useRef<HTMLDivElement>(null);
const [selectedRect, setSelectedRect] = useState({});
const [selectedIndex, setIndex] = useManagedState<Index>(
selectedIndexProp,
initialIndexProp,
onChangeProp
);

const handleChange = (index: Index) => (event: ChangeEvent<HTMLInputElement>) => {
setIndex(index, event);
};

// Because we use radio buttons for the segments, they should share a unique `name`
const name = String(useId());
Expand Down Expand Up @@ -141,7 +128,9 @@ export const SegmentedControl = ({
isSelected={isSelected}
key={label}
name={name}
onChange={handleChange(idx)}
onChange={event => {
onChange(idx, event);
}}
size={size}
value={idx}
>
Expand Down Expand Up @@ -194,16 +183,18 @@ const Root = forwardRef<HTMLDivElement, RootProps>(({ fill, size, width, ...prop
);
});

type ItemProps = {
type BaseInputProps = {
children: ReactNode;
fill: boolean;
isAnimated: boolean;
isSelected: boolean;
onChange: ChangeEventHandler;
onChange: ChangeEventHandler<HTMLInputElement>;
name: string;
size: SizeKey;
value: Index;
} & HTMLAttributes<HTMLInputElement>;
};

type ItemProps = BaseInputProps & Omit<InputHTMLAttributes<HTMLInputElement>, keyof BaseInputProps>;

const Item = (props: ItemProps) => {
const { children, fill, isAnimated, isSelected, onChange, size, value, ...attrs } = props;
Expand Down
10 changes: 9 additions & 1 deletion design-system/website/pages/components/fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const BasicDatePicker = () => {
export default function FieldsPage() {
const { spacing } = useTheme();
const [selectVal, setSelectVal] = useState<{ label: string; value: string } | null>(null);
const [segmentedControlVal, setSegmentedControlVal] = useState<number | undefined>(undefined);
return (
<Page>
<h1>Form Fields</h1>
Expand Down Expand Up @@ -110,7 +111,14 @@ export default function FieldsPage() {
</FieldWrapper>
<h2>Segmented Controls</h2>
<FieldWrapper>
<SegmentedControl animate segments={['one', 'two', 'three']} />
<SegmentedControl
animate
segments={['one', 'two', 'three']}
selectedIndex={segmentedControlVal}
onChange={val => {
setSegmentedControlVal(val);
}}
/>
</FieldWrapper>
<h2>Checkboxes</h2>
<FieldWrapper>
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/docs/apis/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ The `sqlite` provider is not intended to be used in production systems, and has
- `text`: The `text` field type does not support setting a filter as case sensitive or insensitive.
Assuming default collation, all the filters except `contains`, `startsWith` and `endsWith` will be case sensitive
and `contains`, `startsWith` and `endsWith` will be case insensitive but only for ASCII characters.
- `select`: Using the `dataType: 'enum'` will use a GraphQL `String` type, rather than an `Enum` type.
- `select`: Using the `type: 'enum'`, the value will be represented as a string in the database.

## ui

Expand Down
35 changes: 23 additions & 12 deletions docs/pages/docs/apis/fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,27 +372,37 @@ export default config({
### select

A `select` field represents the selection of one of fixed set of values.
Values can be either strings, integers, or enum values, as determined by the `dataType` option.
This will determine their GraphQL data type, as well as their database storage type.
Values can be either strings, integers, or enum values, as determined by the `type` option.
This will determine their GraphQL data type, as well as their database storage type except for `enum` on SQLite
where the GraphQL type will be an enum but it will be represented as a string in the database.

Options:

- `dataType` (default: `'string'`): Sets the type of the values of this field.
- `type` (default: `'string'`): Sets the type of the values of this field.
Must be one of `['string', 'enum', 'integer']`.
- `options`: An array of `{ label, value }`.
`label` is a string to be displayed in the Admin UI.
`value` is either a `string` (for `{ dataType: 'string' }` or `{ dataType: 'enum' }`), or a `number` (for `{ dataType: 'integer' }`).
`value` is either a `string` (for `{ type: 'string' }` or `{ type: 'enum' }`), or a `number` (for `{ type: 'integer' }`).
The `value` will be used in the GraphQL API and stored in the database.
- `defaultValue`: (default: `undefined`): Can be either a string/integer value or an async function which takes an argument `({ context, originalInput })` and returns a string/integer value.
This value will be used for the field when creating items if no explicit value is set, and must be one of the values defined in `options`.
`context` is a [`KeystoneContext`](./context) object.
`originalInput` is an object containing the data passed in to the `create` mutation.
- `isRequired` (default: `false`): If `true` then this field can never be set to `null`.
- `isNullable` (default: `true`): If `false` then this field will be made non-nullable in the database and it will never be possible to set as `null`.
- `defaultValue` (default: `false`): This value will be used for the field when creating items if no explicit value is set.
- `validation.isRequired` (default: `false`): If `true` then this field can never be set to `null`.
Unlike `isNullable`, this will require that a value is provided in the Admin UI.
It will also validate this when creating and updating an item through the GraphQL API but it will not enforce it at the database level.
If you would like to enforce it being non-null at the database-level and in the Admin UI, you can set both `isNullable: false` and `validation.isRequired: true`.
- `isIndexed` (default: `false`)
- If `true` then this field will be indexed by the database.
- If `'unique'` then all values of this field must be unique.
- `ui` (default: `{ displayMode: 'select' }`): Configures the display mode of the field in the Admin UI.
- `ui.displayMode` (default: `'select'`): Configures the display mode of the field in the Admin UI.
Can be one of `['select', 'segmented-control']`.
- `graphql.read.isNonNull` (default: `false`): If you have no read access control and you don't intend to add any in the future,
you can set this to true and the output field will be non-nullable. This is only allowed when you have no read access control because otherwise,
when access is denied, `null` will be returned which will cause an error since the field is non-nullable and the error
will propagate up until a nullable field is found which means the entire item will be unreadable and when doing an `items` query, all the items will be unreadable.
- `graphql.create.isNonNull` (default: `false`): If you have no create access control and you want to explicitly show that this is field is non-nullable in the create input
you can set this to true and the create field will be non-nullable and have a default value at the GraphQL level.
This is only allowed when you have no create access control because otherwise, the item will always fail access control
if a user doesn't have access to create the particular field regardless of whether or not they specify the field in the create.

```typescript
import { config, list } from '@keystone-next/keystone';
Expand All @@ -403,13 +413,14 @@ export default config({
ListName: list({
fields: {
fieldName: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: '...', value: '...' },
/* ... */
],
defaultValue: '...',
isRequired: true,
validation: { isRequired: true, },
isNullable: false,
isIndexed: 'unique',
ui: { displayMode: 'select' },
}),
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/docs/apis/filters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ The `json` field type does not support filters.

### select

- If the `dataType` is `string`(the default), the same filters as `text` will be available.
- If the `dataType` is `integer`, the same filters as `integer` will be available.
- If the `dataType` is `enum`, the following filters will be available:
- If the `type` is `string`(the default), the same filters as `text` will be available.
- If the `type` is `integer`, the same filters as `integer` will be available.
- If the `type` is `enum`, the following filters will be available:
| **Filter name** | **Type** | **Description** |
| --------------- | ---------- | ------------------- |
| `equals` | `ListKeyFieldKeyType` | Equals |
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/updates/new-graphql-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
2 changes: 1 addition & 1 deletion examples-staging/assets-cloud/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const lists = {
fields: {
title: text({ isRequired: true }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples-staging/assets-local/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const lists = {
fields: {
title: text({ isRequired: true }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples-staging/basic/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ model PhoneNumber {
model Post {
id String @id @default(cuid())
title String?
status String?
status String @default("draft")
content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]")
publishDate DateTime?
author User? @relation("Post_author", fields: [authorId], references: [id])
Expand Down
5 changes: 5 additions & 0 deletions examples-staging/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@ export const lists = {
ui: {
displayMode: 'segmented-control',
},
isNullable: false,
validation: {
isRequired: true,
},
defaultValue: 'draft',
}),
content: document({
ui: { views: require.resolve('./admin/fieldViews/Content.tsx') },
Expand Down
2 changes: 1 addition & 1 deletion examples-staging/ecommerce/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ model Product {
description String?
photo ProductImage? @relation("Product_photo", fields: [photoId], references: [id])
photoId String? @unique @map("photo")
status String?
status String? @default("DRAFT")
price Int?
user User? @relation("Product_user", fields: [userId], references: [id])
userId String? @map("user")
Expand Down
2 changes: 1 addition & 1 deletion examples/blog/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const lists = {
fields: {
title: text({ isRequired: true, isFilterable: true }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-admin-ui-logo/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-admin-ui-navigation/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-admin-ui-pages/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field-view/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-field/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
title: text({ isRequired: true }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
20 changes: 12 additions & 8 deletions examples/default-values/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
// Dynamic default: Use the label field to determine the priority
defaultValue: ({ originalInput }) => {
if (originalInput.label && originalInput.label.toLowerCase().includes('urgent')) {
return 'high';
} else {
return 'low';
}
hooks: {
resolveInput({ resolvedData, originalInput }) {
if (originalInput.priority === undefined) {
if (originalInput.label && originalInput.label.toLowerCase().includes('urgent')) {
return 'high';
} else {
return 'low';
}
}
return resolvedData.priority;
},
},
}),
// Static default: When a task is first created, it is incomplete
Expand Down
2 changes: 1 addition & 1 deletion examples/document-field/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const lists = {
title: text({ isRequired: true }),
slug: text({ isRequired: true, isIndexed: 'unique' }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples/extend-graphql-schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const lists = {
fields: {
title: text({ isRequired: true }),
status: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
Expand Down
2 changes: 1 addition & 1 deletion examples/task-manager/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const lists = {
fields: {
label: text({ isRequired: true }),
priority: select({
dataType: 'enum',
type: 'enum',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
Expand Down
Loading

0 comments on commit 7f5caff

Please sign in to comment.