Skip to content

Commit c1add44

Browse files
committed
implement
1 parent c48c76e commit c1add44

17 files changed

Lines changed: 731 additions & 199 deletions

File tree

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
},

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ function CreateItemPage({ listKey }: { listKey: string }) {
2626
<ItemPageHeader
2727
list={list}
2828
actions={[]}
29+
value={null}
2930
label="Create"
3031
title={`Create ${list.singular}`}
3132
item={null}
33+
initialValue={null}
3234
onAction={null}
3335
/>
3436
}

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,24 @@ 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({
2021
list,
2122
actions,
2223
item,
24+
value,
25+
initialValue,
2326
label,
2427
title = label,
2528
onAction,
2629
}: {
2730
list: ListMeta
2831
actions: ActionMeta[]
2932
item: Record<string, unknown> | null
33+
value: Record<string, unknown> | null
34+
initialValue: Record<string, unknown> | null
3035
label: string
3136
title: string
3237
onAction: ((action: ActionMeta, resultId: string | null) => void) | null
@@ -67,7 +72,14 @@ export function ItemPageHeader({
6772
)}
6873

6974
{item && onAction && actions.length > 0 && (
70-
<ItemActions list={list} item={item} actions={actions} onAction={onAction} />
75+
<ItemActions
76+
list={list}
77+
item={item}
78+
value={value}
79+
initialValue={initialValue}
80+
actions={actions}
81+
onAction={onAction}
82+
/>
7183
)}
7284
</Grid>
7385
)
@@ -96,11 +108,15 @@ type ActionError = {
96108
function ItemActions({
97109
list,
98110
item,
111+
initialValue,
112+
value,
99113
actions,
100114
onAction,
101115
}: {
102116
list: ListMeta
103117
item: Record<string, unknown>
118+
value: Record<string, unknown> | null
119+
initialValue: Record<string, unknown> | null
104120
actions: ActionMeta[]
105121
onAction: (action: ActionMeta, resultId: string | null) => void
106122
}) {
@@ -135,13 +151,30 @@ function ItemActions({
135151

136152
const { messages: m } = action
137153
try {
138-
const data = await apolloClient.mutate({
139-
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!) {
140164
result: ${action.graphql.names.one}(where: { id: $id }) {
141165
id
142166
}
143-
}` as TypedDocumentNode<{ result: { id: string } }, { id: string }>,
144-
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 },
145178
})
146179

147180
if (!action.itemView.hideToast) {

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ function ResetButton(props: { onReset: () => void; hasChanges?: boolean }) {
162162
function ItemForm({
163163
listKey,
164164
initialValue,
165+
value,
166+
onChange,
165167
itemLabel,
166168
onSaveSuccess,
167169
fieldModes,
@@ -170,6 +172,8 @@ function ItemForm({
170172
}: {
171173
listKey: string
172174
initialValue: Record<string, unknown>
175+
value: Record<string, unknown>
176+
onChange: (value: Record<string, unknown>) => void
173177
itemLabel: string
174178
onSaveSuccess: () => void
175179
fieldModes: Record<string, ConditionalFilter<'edit' | 'read' | 'hidden', BaseListTypeInfo>>
@@ -188,11 +192,9 @@ function ItemForm({
188192
{ errorPolicy: 'all' }
189193
)
190194

191-
const [value, setValue] = useState(() => initialValue)
192195
function resetValueState() {
193-
setValue(() => initialValue)
196+
onChange(initialValue)
194197
}
195-
useEffect(() => resetValueState(), [initialValue])
196198

197199
const invalidFields = useInvalidFields(list.fields, value, isRequireds)
198200
const [forceValidation, setForceValidation] = useState(false)
@@ -257,7 +259,7 @@ function ItemForm({
257259
invalidFields={invalidFields}
258260
fieldModes={fieldModes}
259261
fieldPositions={fieldPositions}
260-
onChange={useCallback(value => setValue(value), [setValue])}
262+
onChange={onChange}
261263
value={value}
262264
isRequireds={isRequireds}
263265
/>
@@ -271,7 +273,7 @@ function ItemForm({
271273
groups={list.groups}
272274
forceValidation={forceValidation}
273275
invalidFields={invalidFields}
274-
onChange={useCallback(value => setValue(value), [setValue])}
276+
onChange={onChange}
275277
value={value}
276278
fieldModes={fieldModes}
277279
fieldPositions={fieldPositions}
@@ -321,6 +323,10 @@ function ItemPage({ listKey }: ItemPageProps) {
321323
if (!item) return null
322324
return deserializeItemToValue(list.fields, item)
323325
}, [list.fields, data?.item])
326+
const [value, setValue] = useState<Record<string, unknown> | null>(null)
327+
useEffect(() => {
328+
setValue(initialValue)
329+
}, [initialValue])
324330

325331
const { actionsInContext, fieldModes, fieldPositions, isRequireds } = useMemo(() => {
326332
const actionModes = Object.fromEntries(
@@ -394,6 +400,8 @@ function ItemPage({ listKey }: ItemPageProps) {
394400
label={typeof pageLabel !== 'string' ? 'Loading...' : pageLabel}
395401
title={pageTitle}
396402
item={item ?? null}
403+
value={value ?? initialValue}
404+
initialValue={initialValue}
397405
onAction={onAction}
398406
/>
399407
}
@@ -429,14 +437,16 @@ function ItemPage({ listKey }: ItemPageProps) {
429437
</ItemNotFound>
430438
))}
431439
</Box>
432-
{initialValue && (
440+
{initialValue && value && (
433441
<ItemForm
434442
fieldModes={fieldModes}
435443
fieldPositions={fieldPositions}
436444
isRequireds={isRequireds}
437445
listKey={listKey}
438446
itemLabel={itemLabel}
439447
initialValue={initialValue}
448+
value={value}
449+
onChange={setValue}
440450
onSaveSuccess={refetch}
441451
/>
442452
)}

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ 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'
47+
import { deserializeItemToValue } from '../../../../admin-ui/utils'
4648
import { FilterAdd } from './FilterAdd'
4749
import { PaginationControls, snapValueToClosest } from './PaginationControls'
4850
import { Tag } from './Tag'
@@ -332,7 +334,23 @@ function ListPage({ listKey }: ListPageProps) {
332334
label: f.label,
333335
isDisabled: f.listView.fieldMode === 'read',
334336
}))
335-
const shownFields = columns.map(fieldKey => list.fields[fieldKey]).filter(Boolean)
337+
const shownFields = useMemo(
338+
() => columns.map(fieldKey => list.fields[fieldKey]).filter(Boolean),
339+
[columns, list.fields]
340+
)
341+
const queriedFields = useMemo(() => {
342+
return [
343+
...new Set([
344+
...columns,
345+
...list.actions
346+
.filter(action => action.listView.actionMode === 'enabled')
347+
.flatMap(action => action.graphql.fields),
348+
]),
349+
]
350+
.map(fieldKey => list.fields[fieldKey])
351+
.filter(Boolean)
352+
}, [columns, list])
353+
336354
const where = useMemo(
337355
() =>
338356
filters.map(filter => {
@@ -350,7 +368,7 @@ function ListPage({ listKey }: ListPageProps) {
350368
items: Record<string, unknown>[] | null
351369
count: number | null
352370
}> => {
353-
const selectedGqlFields = shownFields
371+
const selectedGqlFields = queriedFields
354372
.filter(field => field.key !== 'id') // id is always included
355373
.map(field => field.controller.graphqlSelection)
356374
.join('\n')
@@ -375,7 +393,7 @@ function ListPage({ listKey }: ListPageProps) {
375393
count: ${list.graphql.names.listQueryCountName}(where: $where)
376394
}
377395
`
378-
}, [list, shownFields]),
396+
}, [list, queriedFields]),
379397
{
380398
fetchPolicy: 'cache-and-network',
381399
errorPolicy: 'all',
@@ -431,16 +449,16 @@ function ListPage({ listKey }: ListPageProps) {
431449
setSort(defaultSort)
432450
}
433451

434-
const actions = list.actions.filter(action => action.listView.actionMode === 'enabled')
435452
const actionsForList = list.hideDelete
436-
? actions
453+
? list.actions
437454
: [
438-
...actions,
455+
...list.actions,
439456
{
440457
key: 'delete',
441458
label: 'Delete',
442459
icon: 'trash2Icon',
443460
graphql: {
461+
fields: [],
444462
names: {
445463
one: list.graphql.names.deleteMutationName,
446464
many: list.graphql.names.deleteManyMutationName,
@@ -661,6 +679,7 @@ function ListPage({ listKey }: ListPageProps) {
661679
return (
662680
<ActionItemsDialog
663681
itemIds={selectedItemIds}
682+
items={data?.items ?? []}
664683
action={action}
665684
list={list}
666685
onSuccess={remaining => {
@@ -788,27 +807,47 @@ type ActionErrorResult = {
788807
function ActionItemsDialog({
789808
list,
790809
itemIds,
810+
items,
791811
onSuccess,
792812
onErrors,
793813
action,
794814
}: {
795815
list: ListMeta
796816
itemIds: string[]
817+
items: Record<string, unknown>[]
797818
onSuccess: (remaining: Set<string>) => void
798819
onErrors: (result: ActionErrorResult) => void
799820
action: ActionMeta
800821
}) {
801-
const [actionOnItems] = useMutation<{ results?: ({ id: string } | null)[] }>(
802-
gql`mutation($where: [${list.graphql.names.whereUniqueInputName}!]!) {
822+
const actionMutation =
823+
action.key === 'delete'
824+
? gql`mutation($where: [${list.graphql.names.whereUniqueInputName}!]!) {
803825
results: ${action.graphql.names.many}(where: $where) {
804826
id
805827
}
806-
}`,
807-
{
808-
variables: { where: itemIds.map(id => ({ id })) },
809-
errorPolicy: 'all',
810-
}
811-
)
828+
}`
829+
: gql`mutation($data: [${action.graphql.names.one[0].toUpperCase()}${action.graphql.names.one.slice(1)}Args!]!) {
830+
results: ${action.graphql.names.many}(data: $data) {
831+
id
832+
}
833+
}`
834+
const [actionOnItems] = useMutation<{ results?: ({ id: string } | null)[] }>(actionMutation, {
835+
variables:
836+
action.key === 'delete'
837+
? { where: itemIds.map(id => ({ id })) }
838+
: {
839+
data: itemIds.flatMap(id => {
840+
const row = items.find(item => String(item.id) === id)
841+
if (!row) {
842+
return []
843+
}
844+
const deserialized = deserializeItemToValue(list.fields, row)
845+
const data = serializeActionData(list, action, deserialized, deserialized)
846+
return Object.keys(data).length ? { where: { id }, data } : { where: { id } }
847+
}),
848+
},
849+
errorPolicy: 'all',
850+
})
812851
const { messages: m } = action
813852

814853
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 {

0 commit comments

Comments
 (0)