Skip to content

Commit 066e789

Browse files
emmatownCopilotdcousens
committed
AND, OR and NOT in client side conditional filters
Co-authored-by: Copilot <[email protected]> Co-authored-by: dcousens <[email protected]> Co-authored-by: Daniel Cousens <[email protected]>
1 parent 3e4f684 commit 066e789

File tree

8 files changed

+498
-77
lines changed

8 files changed

+498
-77
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@keystone-6/core": minor
3+
---
4+
5+
Add `AND`, `OR` and `NOT` support for conditional client-state filters

docs/content/docs/config/lists.md

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,46 @@
11
---
2-
title: "Lists API"
3-
description: "Reference docs for Keystone’s Lists API, which defines the data model of your system."
2+
title: 'Lists API'
3+
description: 'Reference docs for Keystone’s Lists API, which defines the data model of your system.'
44
---
55

66
The `lists` property of the [system configuration](./config) object is where you define the data model, or schema, of your Keystone system.
77
It accepts an object with list names as keys, and `list()` configurations as values.
88

99
```typescript
10-
import { config, list } from '@keystone-6/core';
10+
import { config, list } from '@keystone-6/core'
1111

1212
export default config({
13-
lists: ({
13+
lists: {
1414
SomeListName: list({
15-
fields: { /* ... */ },
16-
actions: { /* ... */ },
17-
access: { /* ... */ },
18-
ui: { /* ... */ },
19-
hooks: { /* ... */ },
20-
graphql: { /* ... */ },
21-
db: { /* ... */ },
15+
fields: {
16+
/* ... */
17+
},
18+
actions: {
19+
/* ... */
20+
},
21+
access: {
22+
/* ... */
23+
},
24+
ui: {
25+
/* ... */
26+
},
27+
hooks: {
28+
/* ... */
29+
},
30+
graphql: {
31+
/* ... */
32+
},
33+
db: {
34+
/* ... */
35+
},
2236
isSingleton: false,
2337
defaultIsFilterable: false,
2438
defaultIsOrderable: false,
2539
}),
2640
/* ... */
27-
}),
41+
},
2842
/* ... */
29-
});
43+
})
3044
```
3145

3246
This document will explain the configuration options which can be used with the `list()` function.
@@ -43,21 +57,23 @@ The `fields` option defines the names, types, and configuration of the fields in
4357
This configuration option takes an object with field names as keys and configured field types as values.
4458

4559
```typescript
46-
import { config, list } from '@keystone-6/core';
47-
import { text } from '@keystone-6/core/fields';
60+
import { config, list } from '@keystone-6/core'
61+
import { text } from '@keystone-6/core/fields'
4862

4963
export default config({
5064
lists: {
5165
SomeListName: list({
5266
fields: {
53-
someFieldName: text({ /* ... */ }),
67+
someFieldName: text({
68+
/* ... */
69+
}),
5470
/* ... */
5571
},
5672
}),
5773
/* ... */
5874
},
5975
/* ... */
60-
});
76+
})
6177
```
6278

6379
For full details on the available field types and their configuration options please see the [Fields API](../fields/overview).
@@ -68,9 +84,9 @@ The `actions` property of the list configuration object is where you define acti
6884
An action can be triggered on individual items or in bulk from the list view in the Admin UI, or directly using GraphQL.
6985

7086
```typescript
71-
import { config, list } from '@keystone-6/core';
72-
import { allowAll } from '@keystone-6/core/access';
73-
import { text, integer } from '@keystone-6/core/fields';
87+
import { config, list } from '@keystone-6/core'
88+
import { allowAll } from '@keystone-6/core/access'
89+
import { text, integer } from '@keystone-6/core/fields'
7490

7591
export default config({
7692
lists: {
@@ -83,7 +99,7 @@ export default config({
8399
actions: {
84100
vote: {
85101
access: allowAll,
86-
async resolve ({ where }, context) {
102+
async resolve({ where }, context) {
87103
if (!where) return null
88104
return await context.prisma.post.update({
89105
where: { id: where.id },
@@ -98,7 +114,7 @@ export default config({
98114
},
99115
}),
100116
},
101-
});
117+
})
102118
```
103119

104120
Each action accepts the following options:
@@ -118,12 +134,12 @@ Each action accepts the following options:
118134
- `success`, `successMany`: Success toast messages.
119135
- `fail`, `failMany`: Failure toast messages.
120136
- `itemView`: Controls for the item view.
121-
- `actionMode` (default: `'enabled'`): Can be `'enabled'`, `'disabled'`, or `'hidden'`, or a function that returns one of those values.
137+
- `actionMode` (default: `'enabled'`): Can be `'enabled'`, `'disabled'`, or `'hidden'`, a conditional filter object, or a function that returns one of those values. Conditional filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups.
122138
- `navigation` (default: `'follow'`): Controls navigation after the action completes. `'follow'` navigates to the returned item (or list view if `null`), `'refetch'` stays and refreshes the item, `'return'` goes back to the list view.
123139
- `hidePrompt` (default: `false`): Do not show a confirmation dialog.
124140
- `hideToast` (default: `false`): Do not show a toast notification.
125141
- `listView`: Controls for the list view.
126-
- `actionMode` (default: `'enabled'`): Can be `'enabled'` or `'hidden'`, or a function that returns one of those values.
142+
- `actionMode` (default: `'enabled'`): Can be `'enabled'` or `'hidden'`, a conditional filter object, or a function that returns one of those values. Conditional filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups.
127143

128144
## access
129145

@@ -169,7 +185,7 @@ Options:
169185
Option `field` is the name of the field to sort by, and `direction` is either `'ASC'` or `'DESC'` for ascending and descending sorting respectively.
170186
If undefined then data will be unsorted.
171187
- `pageSize` (default: lower of `50` or [`graphql.maxTake`](#graphql)): Sets the number of items to show per page in the list view.
172-
- `initialFilter` (default: `undefined`): Sets a default filter to apply to the list view. Accepts a where input object (excluding `AND`, `OR`, `NOT`), or an async function with an argument `{ session, context }` that returns a where input object.
188+
- `initialFilter` (default: `undefined`): Sets a default column filter to apply to the list view. Accepts a where input object (excluding `AND`, `OR`, `NOT`), or an async function with an argument `{ session, context }` that returns a where input object.
173189
- `label`: The label used to identify the list in navigation etc.
174190
- `singular`: The singular form of the list key. It is used in sentences like `Are you sure you want to delete this {singular}?`
175191
- `plural`: The plural form of the list key. It is used in sentences like `Are you sure you want to delete these {plural}?`
@@ -243,7 +259,7 @@ Options:
243259
- `omit.delete` (default: `false`): If set to true, the delete mutation will be omitted from the GraphQL API for this list.
244260

245261
```typescript
246-
import { config, list } from '@keystone-6/core';
262+
import { config, list } from '@keystone-6/core'
247263

248264
export default config({
249265
lists: {
@@ -266,7 +282,7 @@ export default config({
266282
/* ... */
267283
},
268284
/* ... */
269-
});
285+
})
270286
```
271287

272288
## db
@@ -281,7 +297,7 @@ Options:
281297
- `map`: Adds a [Prisma `@@map`](https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#map-1) attribute to the Prisma model for this list which specifies a custom database table name for the list, instead of using the list key
282298

283299
```typescript
284-
import { config, list } from '@keystone-6/core';
300+
import { config, list } from '@keystone-6/core'
285301

286302
export default config({
287303
lists: {
@@ -295,7 +311,7 @@ export default config({
295311
/* ... */
296312
},
297313
/* ... */
298-
});
314+
})
299315
```
300316

301317
## isSingleton

docs/content/docs/fields/overview.md

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
title: "Fields"
3-
description: "A reference of Keystone’s field types, and the configuration options they accept."
2+
title: 'Fields'
3+
description: 'A reference of Keystone’s field types, and the configuration options they accept.'
44
---
55

66
{% hint kind="warn" %}
@@ -14,7 +14,7 @@ This document covers the different field types which are available and the confi
1414
To see how to access fields in the GraphQL API please see the [GraphQL API](../graphql/overview) docs.
1515

1616
```typescript
17-
import { config, list } from '@keystone-6/core';
17+
import { config, list } from '@keystone-6/core'
1818
import {
1919
// Scalar types
2020
checkbox,
@@ -39,24 +39,26 @@ import {
3939
// File types
4040
file,
4141
image,
42-
} from '@keystone-6/core/fields';
42+
} from '@keystone-6/core/fields'
4343

4444
// Complex types
45-
import { document } from '@keystone-6/fields-document';
46-
import { cloudinaryImage } from '@keystone-6/cloudinary';
45+
import { document } from '@keystone-6/fields-document'
46+
import { cloudinaryImage } from '@keystone-6/cloudinary'
4747

4848
export default config({
4949
lists: {
5050
SomeListName: list({
5151
fields: {
52-
someFieldName: text({ /* ... */ }),
52+
someFieldName: text({
53+
/* ... */
54+
}),
5355
/* ... */
5456
},
5557
}),
5658
/* ... */
5759
},
5860
/* ... */
59-
});
61+
})
6062
```
6163

6264
## Common configuration
@@ -92,7 +94,7 @@ Options:
9294
Defaults to the list's `ui.itemView.defaultFieldMode` config if defined.
9395
See the [Lists API](../config/lists#ui) for details.
9496
- `itemView.fieldPosition` (default: `form`): Controls which side of the page the field is placed in the Admin UI.
95-
Can be either `form` or `sidebar`, or an async function with an argument `{ session, context, listKey, fieldKey, item, itemField }` that returns one of `['form', 'sidebar']`. The `item` argument may be `null`. `form` or blank places the field on the left hand side of the item view. `sidebar` places the field on the right hand side under the ID field
97+
Can be either `form` or `sidebar`, or an async function with an argument `{ session, context, listKey, fieldKey, item, itemField }` that returns one of `['form', 'sidebar']`. The `item` argument may be `null`. `form` or blank places the field on the left hand side of the item view. `sidebar` places the field on the right hand side under the ID field
9698
- `itemView.isRequired` (default: `undefined`): Controls whether the field is marked as required in the item view. Can be a boolean or an async function with an argument `{ session, context }` that returns a boolean. When `true`, the Admin UI will show the field as required.
9799
- `listView.fieldMode` (default: `'read'`): Controls the list view page of the Admin UI.
98100
Can be one of `['read', 'hidden']`, or an async function with an argument `{ session, context }` that returns one of `['read', 'hidden']`.
@@ -115,6 +117,8 @@ Options:
115117
- `omit.create` (default: `false`): If you specify `true`, then the field will be excluded from the list's CreateInput GraphQL type.
116118
- `omit.update` (default: `false`): If you specify `true`, then the field will be excluded from the list's UpdateInput GraphQL type.
117119

120+
For Admin UI conditional config such as `fieldMode` and `isRequired`, filter objects can combine field predicates with nested `AND`, `OR`, and `NOT` groups. These are evaluated client-side in the Admin UI and do not change the GraphQL `where` input API.
121+
118122
```typescript
119123
export default config({
120124
lists: {
@@ -123,16 +127,25 @@ export default config({
123127
someFieldName: text({
124128
isFilterable: ({ context, session, fieldKey, listKey }) => true,
125129
isOrderable: ({ context, session, fieldKey, listKey }) => true,
126-
access: { /* ... */ },
127-
hooks: { /* ... */ },
130+
access: {
131+
/* ... */
132+
},
133+
hooks: {
134+
/* ... */
135+
},
128136
ui: {
129137
label: '...',
130138
views: './path/to/viewsModule',
131139
createView: {
132140
fieldMode: ({ session, context }) => 'edit',
133141
},
134142
itemView: {
135-
fieldMode: ({ session, context, item, itemField }) => 'read',
143+
fieldMode: {
144+
read: {
145+
status: { equals: 'archived' },
146+
NOT: { canEdit: { equals: true } },
147+
},
148+
},
136149
},
137150
listView: {
138151
fieldMode: ({ session, context }) => 'read',
@@ -145,24 +158,24 @@ export default config({
145158
create: true,
146159
update: true,
147160
},
148-
}
161+
},
149162
}),
150163
/* ... */
151164
},
152165
}),
153166
/* ... */
154167
},
155168
/* ... */
156-
});
169+
})
157170
```
158171

159172
## Groups
160173

161174
Fields can be grouped together in the Admin UI using the `group` function, with a customisable `label` and `description`.
162175

163176
```typescript
164-
import { config, list, group } from '@keystone-6/core';
165-
import { text } from '@keystone-6/core/fields';
177+
import { config, list, group } from '@keystone-6/core'
178+
import { text } from '@keystone-6/core/fields'
166179

167180
export default config({
168181
lists: {
@@ -172,7 +185,9 @@ export default config({
172185
label: 'Group label',
173186
description: 'Group description',
174187
fields: {
175-
someFieldName: text({ /* ... */ }),
188+
someFieldName: text({
189+
/* ... */
190+
}),
176191
/* ... */
177192
},
178193
}),
@@ -182,7 +197,7 @@ export default config({
182197
/* ... */
183198
},
184199
/* ... */
185-
});
200+
})
186201
```
187202

188203
Groups also support `defaultFieldMode` overrides for `createView`, `itemView`, and `listView`, which apply to all fields within the group unless overridden at the field level:

examples/actions/schema.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const readOnly = {
3131
},
3232
}
3333

34+
const isReportDisabled = {
35+
OR: [{ hidden: { equals: true } }, { reportedAt: { not: { equals: null } } }],
36+
} as const
37+
3438
export const lists = {
3539
Post: list({
3640
access: allowAll, // WARNING: public
@@ -122,12 +126,12 @@ export const lists = {
122126
successMany: 'Successfully reported {countSuccess} {singular|plural}',
123127
},
124128
itemView: {
125-
actionMode: { disabled: { hidden: { equals: true } } },
129+
actionMode: { disabled: isReportDisabled },
126130
navigation: 'refetch',
127131
hideToast: true,
128132
},
129133
listView: {
130-
actionMode: { disabled: { hidden: { equals: true } } },
134+
actionMode: { disabled: isReportDisabled },
131135
},
132136
},
133137
},

examples/dynamic-is-required/schema.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ import { list } from '@keystone-6/core'
33
import { allowAll } from '@keystone-6/core/access'
44
import { checkbox, relationship, select, text, timestamp } from '@keystone-6/core/fields'
55

6+
const hasHighPriority = {
7+
priority: { equals: 'high' },
8+
isComplete: { equals: false },
9+
} as const
10+
611
export const lists = {
712
Todo: list({
813
access: allowAll,
@@ -19,24 +24,20 @@ export const lists = {
1924
reasonForHighPriority: text({
2025
ui: {
2126
createView: {
27+
isRequired: hasHighPriority,
2228
fieldMode: {
2329
hidden: {
24-
priority: { not: { equals: 'high' } },
30+
NOT: hasHighPriority,
2531
},
2632
},
27-
isRequired: {
28-
priority: { equals: 'high' },
29-
},
3033
},
3134
itemView: {
35+
isRequired: hasHighPriority,
3236
fieldMode: {
3337
hidden: {
34-
priority: { not: { equals: 'high' } },
38+
NOT: hasHighPriority,
3539
},
3640
},
37-
isRequired: {
38-
priority: { equals: 'high' },
39-
},
4041
},
4142
},
4243
hooks: {

0 commit comments

Comments
 (0)