Skip to content

Commit 0a221e1

Browse files
committed
implement
1 parent 8714746 commit 0a221e1

File tree

16 files changed

+832
-194
lines changed

16 files changed

+832
-194
lines changed

docs/content/docs/config/lists.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ The `actions` property of the list configuration object is where you define acti
6868
An action can be triggered on individual items or in bulk from the list view in the Admin UI, or directly using GraphQL.
6969

7070
```typescript
71-
import { config, list } from '@keystone-6/core';
71+
import { config, list, action } from '@keystone-6/core';
7272
import { allowAll } from '@keystone-6/core/access';
7373
import { text, integer } from '@keystone-6/core/fields';
7474

@@ -81,7 +81,7 @@ export default config({
8181
votes: integer({ defaultValue: 0 }),
8282
},
8383
actions: {
84-
vote: {
84+
vote: action({
8585
access: allowAll,
8686
async resolve ({ where }, context) {
8787
if (!where) return null
@@ -94,7 +94,7 @@ export default config({
9494
label: 'Vote +1',
9595
icon: 'voteIcon',
9696
},
97-
},
97+
}),
9898
},
9999
}),
100100
},

examples/actions/schema.graphql

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,17 @@ type Mutation {
138138
deletePost(where: PostWhereUniqueInput!): Post
139139
deletePosts(where: [PostWhereUniqueInput!]!): [Post]
140140
votePost(where: PostWhereUniqueInput!): Post
141-
votePosts(where: [PostWhereUniqueInput!]!): [Post]
141+
votePosts(data: [VotePostArgs!]!): [Post]
142142
reportAPost(where: PostWhereUniqueInput!): Post
143-
reportSomePosts(where: [PostWhereUniqueInput!]!): [Post]
143+
reportSomePosts(data: [ReportAPostArgs!]!): [Post]
144+
}
145+
146+
input VotePostArgs {
147+
where: PostWhereUniqueInput!
148+
}
149+
150+
input ReportAPostArgs {
151+
where: PostWhereUniqueInput!
144152
}
145153

146154
type Query {
@@ -263,12 +271,14 @@ type KeystoneAdminUIActionMetaMessages {
263271
}
264272

265273
type KeystoneAdminUIActionMetaGraphQL {
274+
fields: [String!]!
266275
names: KeystoneAdminUIActionMetaGraphQLNames!
267276
}
268277

269278
type KeystoneAdminUIActionMetaGraphQLNames {
270279
one: String!
271280
many: String
281+
data: String
272282
}
273283

274284
type KeystoneAdminUIActionMetaItemView {

packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/common.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Heading, Text } from '@keystar/ui/typography'
1414
import { gql, type TypedDocumentNode, useApolloClient } from '../../../../admin-ui/apollo'
1515
import { Container, CONTAINER_MAX } from '../../../../admin-ui/components/Container'
1616
import { ErrorDetailsDialog } from '../../../../admin-ui/components/Errors'
17+
import { serializeActionData } from '../../../../admin-ui/utils/actionData'
1718
import type { ActionMeta, ListMeta } from '../../../../types'
1819

1920
export function ItemPageHeader({
@@ -150,13 +151,30 @@ function ItemActions({
150151

151152
const { messages: m } = action
152153
try {
153-
const data = await apolloClient.mutate({
154-
mutation: gql`mutation ${action.graphql.names.one}($id: ID!) {
154+
const dataForAction =
155+
value && initialValue ? serializeActionData(list, action, value, initialValue) : {}
156+
const mutation = (
157+
action.graphql.fields.length && action.graphql.names.data
158+
? gql`mutation ${action.graphql.names.one}($id: ID!, $data: ${action.graphql.names.data}!) {
159+
result: ${action.graphql.names.one}(where: { id: $id }, data: $data) {
160+
id
161+
}
162+
}`
163+
: gql`mutation ${action.graphql.names.one}($id: ID!) {
155164
result: ${action.graphql.names.one}(where: { id: $id }) {
156165
id
157166
}
158-
}` as TypedDocumentNode<{ result: { id: string } }, { id: string }>,
159-
variables: { id: item.id as string },
167+
}`
168+
) as TypedDocumentNode<
169+
{ result: { id: string } },
170+
{ id: string; data?: Record<string, unknown> }
171+
>
172+
const data = await apolloClient.mutate({
173+
mutation,
174+
variables:
175+
action.graphql.fields.length && action.graphql.names.data
176+
? { id: item.id as string, data: dataForAction }
177+
: { id: item.id as string },
160178
})
161179

162180
if (!action.itemView.hideToast) {

packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.tsx

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { EmptyState } from '../../../../admin-ui/components/EmptyState'
4141
import { GraphQLErrorNotice } from '../../../../admin-ui/components/GraphQLErrorNotice'
4242
import { PageContainer } from '../../../../admin-ui/components/PageContainer'
4343
import { useList } from '../../../../admin-ui/context'
44+
import { serializeActionData } from '../../../../admin-ui/utils/actionData'
4445
import { useSearchFilter } from '../../../../fields/types/relationship/views/useFilter'
4546
import type { ActionMeta, FieldMeta, JSONValue, ListMeta } from '../../../../types'
4647
import {
@@ -343,10 +344,14 @@ function ListPage({ listKey }: ListPageProps) {
343344
() => columns.map(fieldKey => list.fields[fieldKey]).filter(Boolean),
344345
[columns, list.fields]
345346
)
347+
346348
const queriedFields = useMemo(
347349
() => [
348350
...new Set([
349351
...shownFields,
352+
...list.actions
353+
.filter(action => action.listView.actionMode === 'enabled')
354+
.flatMap(action => action.graphql.fields),
350355
...getQueriedFieldKeysForActions(columns, list.actions, 'listView')
351356
.map(fieldKey => list.fields[fieldKey])
352357
.filter(Boolean),
@@ -397,7 +402,7 @@ function ListPage({ listKey }: ListPageProps) {
397402
count: ${list.graphql.names.listQueryCountName}(where: $where)
398403
}
399404
`
400-
}, [list, shownFields]),
405+
}, [list, queriedFields]),
401406
{
402407
fetchPolicy: 'cache-and-network',
403408
errorPolicy: 'all',
@@ -462,6 +467,7 @@ function ListPage({ listKey }: ListPageProps) {
462467
label: 'Delete',
463468
icon: 'trash2Icon',
464469
graphql: {
470+
fields: [],
465471
names: {
466472
one: list.graphql.names.deleteMutationName,
467473
many: list.graphql.names.deleteManyMutationName,
@@ -703,6 +709,7 @@ function ListPage({ listKey }: ListPageProps) {
703709
return (
704710
<ActionItemsDialog
705711
itemIds={selectedItemIds}
712+
items={data?.items ?? []}
706713
action={action}
707714
list={list}
708715
onSuccess={remaining => {
@@ -830,27 +837,47 @@ type ActionErrorResult = {
830837
function ActionItemsDialog({
831838
list,
832839
itemIds,
840+
items,
833841
onSuccess,
834842
onErrors,
835843
action,
836844
}: {
837845
list: ListMeta
838846
itemIds: string[]
847+
items: Record<string, unknown>[]
839848
onSuccess: (remaining: Set<string>) => void
840849
onErrors: (result: ActionErrorResult) => void
841850
action: ActionMeta
842851
}) {
843-
const [actionOnItems] = useMutation<{ results?: ({ id: string } | null)[] }>(
844-
gql`mutation($where: [${list.graphql.names.whereUniqueInputName}!]!) {
852+
const actionMutation =
853+
action.key === 'delete'
854+
? gql`mutation($where: [${list.graphql.names.whereUniqueInputName}!]!) {
845855
results: ${action.graphql.names.many}(where: $where) {
846856
id
847857
}
848-
}`,
849-
{
850-
variables: { where: itemIds.map(id => ({ id })) },
851-
errorPolicy: 'all',
852-
}
853-
)
858+
}`
859+
: gql`mutation($data: [${action.graphql.names.one[0].toUpperCase()}${action.graphql.names.one.slice(1)}Args!]!) {
860+
results: ${action.graphql.names.many}(data: $data) {
861+
id
862+
}
863+
}`
864+
const [actionOnItems] = useMutation<{ results?: ({ id: string } | null)[] }>(actionMutation, {
865+
variables:
866+
action.key === 'delete'
867+
? { where: itemIds.map(id => ({ id })) }
868+
: {
869+
data: itemIds.flatMap(id => {
870+
const row = items.find(item => String(item.id) === id)
871+
if (!row) {
872+
return []
873+
}
874+
const deserialized = deserializeItemToValue(list.fields, row)
875+
const data = serializeActionData(list, action, deserialized, deserialized)
876+
return Object.keys(data).length ? { where: { id }, data } : { where: { id } }
877+
}),
878+
},
879+
errorPolicy: 'all',
880+
})
854881
const { messages: m } = action
855882

856883
async function onTryAction() {

packages/core/src/admin-ui/admin-meta-graphql.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,11 @@ export const adminMetaQuery = gql`
6969
successMany
7070
}
7171
graphql {
72+
fields
7273
names {
7374
one
7475
many
76+
data
7577
}
7678
}
7779
itemView {

packages/core/src/admin-ui/context.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,22 @@ export function useListItem(
264264
> {
265265
const list = useList(listKey)
266266
const query = useMemo(() => {
267-
const selectedFields = Object.values(list.fields)
268-
.filter(field => {
269-
if (field.key === 'id') return true
270-
return field.itemView.fieldMode !== 'hidden'
271-
})
267+
const selectedFieldKeys = new Set<string>(
268+
Object.values(list.fields)
269+
.filter(field => {
270+
if (field.key === 'id') return true
271+
return field.itemView.fieldMode !== 'hidden'
272+
})
273+
.map(field => field.key)
274+
)
275+
for (const action of list.actions) {
276+
for (const fieldKey of action.graphql.fields) {
277+
selectedFieldKeys.add(fieldKey)
278+
}
279+
}
280+
const selectedFields = [...selectedFieldKeys]
281+
.map(fieldKey => list.fields[fieldKey])
282+
.filter(Boolean)
272283
.map(field => field.controller.graphqlSelection)
273284
.join('\n')
274285

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { ListMeta, ActionMeta } from '../../types'
2+
import { pick } from './pick'
3+
import { serializeValueToOperationItem } from './utils'
4+
5+
export function serializeActionData(
6+
list: ListMeta,
7+
action: ActionMeta,
8+
value: Record<string, unknown>,
9+
initialValue: Record<string, unknown>
10+
): Record<string, unknown> {
11+
const fields = pick(list.fields, action.graphql.fields)
12+
const pickedInitialValue = pick(initialValue, action.graphql.fields)
13+
const pickedValue = pick(value, action.graphql.fields)
14+
return serializeValueToOperationItem('update', fields, pickedValue, pickedInitialValue)
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function pick<T extends Record<string, unknown>, K extends keyof T>(
2+
value: T,
3+
keys: readonly K[]
4+
): Pick<T, K> {
5+
const result: Partial<Pick<T, K>> = {}
6+
for (const key of keys) {
7+
if (Object.prototype.hasOwnProperty.call(value, key)) {
8+
result[key] = value[key]
9+
}
10+
}
11+
return result as Pick<T, K>
12+
}

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { list, config, group } from './schema'
1+
export { list, config, group, action } from './schema'
22
export type { ListConfig, BaseFields } from './types'
33
// this re-exports `g` and `graphql`
44
// note the usage of export * over explicitly listing the exports

packages/core/src/lib/admin-meta-graphql.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,17 @@ const KeystoneAdminUIActionMeta = g.object<ActionMetaSource>()({
154154
type: g.object<ActionMetaSource['graphql']>()({
155155
name: 'KeystoneAdminUIActionMetaGraphQL',
156156
fields: {
157+
fields: g.field({
158+
type: g.nonNull(g.list(g.nonNull(g.String))),
159+
}),
157160
names: g.field({
158161
type: g.nonNull(
159162
g.object<ActionMetaSource['graphql']['names']>()({
160163
name: 'KeystoneAdminUIActionMetaGraphQLNames',
161164
fields: {
162165
one: g.field({ type: g.nonNull(g.String) }),
163166
many: g.field({ type: g.String }),
167+
data: g.field({ type: g.String }),
164168
},
165169
})
166170
),

0 commit comments

Comments
 (0)