UNPKG

@kids-reporter/cms-core

Version:
234 lines (211 loc) 7.77 kB
import { BaseItem } from '@keystone-6/core/types' import { ListConfig, graphql } from '@keystone-6/core' import { json, virtual } from '@keystone-6/core/fields' type ManualOrderFieldConfig = { fieldName: string fieldLabel?: string targetFieldName: string targetListName: string targetListLabelField: string } /** * For `relationship` field, KeystoneJS won't take user input order into account. * Therefore, after the create/update operation is done, * the order of relationship items maybe not be the same order as the user input order. * * This function * - adds monitoring fields in the list * - adds virtual fields in the list. These virtual fields could return relationship items in order. * - decorate `list.hooks.resolveInput` to record the user input order in the monitoring fields * * For example, if we have two lists like * ``` * const User = { * fields: { * name: text(), * } * } * * const Post = { * fields: { * title: text(), * content: text(), * authors: relationship({ref: 'User', many: true}) * } * } * ``` * * if we want to snapshot the authors input order, we can use this function like * ``` * const postList = addManualOrderRelationshipFields([ * { * fieldName: 'manualOrderOfAuthors', * fieldLabel: 'authors 手動排序結果', * targetFieldName: 'authors', // the target field to record the user input order * targetListName: 'User', // relationship list name * targetListLabelField: 'name', // refer to `User.fields.name` * } * ])(Post) * ``` * * `addManualOrderRelationshipFields` will create another JSON field `manualOrderOfAuthors` and virtual field `authorsInInputOrder` in the list, * and decorate `list.hooks.resolveInput` to record the update/create operation, * if the operation modifies the order of the relationship field. * * `authorsInInputOrder` is a virtual field, which means its value is computed on-the-fly, not stored in the database. This virtual field combines relationship field `authors` and monitoring field `manualOrderOfAuthors` to sort the authors in specific input order. */ function addManualOrderRelationshipFields( manualOrderFields: ManualOrderFieldConfig[] = [], list: ListConfig<any, any> ) { manualOrderFields.forEach((mo) => { if (!list.fields?.[mo.fieldName]) { list.fields[mo.fieldName] = json({ label: mo.fieldLabel, ui: { itemView: { fieldMode: 'read', }, }, }) } // add virtual field definition addVirtualFieldToReturnItemsInInputOrder(list, mo) }) // decorate `resolveInput` hook list.hooks = list.hooks || {} const originResolveInput = list.hooks?.resolveInput list.hooks.resolveInput = async (props) => { let resolvedData = props.resolvedData if (typeof originResolveInput === 'function') { resolvedData = await originResolveInput(props) } const { item, context } = props // check if create/update item has the fields // we want to monitor for (let i = 0; i < manualOrderFields.length; i++) { const { targetFieldName, fieldName, targetListName, targetListLabelField, } = manualOrderFields[i] // if create/update operation creates/modifies the `${targetFieldName}` field if (resolvedData?.[targetFieldName]) { let currentOrder: { id: string }[] = [] // update operation due to `item` not being `undefiend` if (item) { const previousOrder: { id: string }[] = Array.isArray(item[fieldName]) ? item[fieldName] : [] // user disconnects/removes some relationship items. const disconnectIds = resolvedData[ targetFieldName ]?.disconnect?.map((obj: { id: number }) => obj.id.toString()) || [] // filtered out to-be-disconnected relationship items currentOrder = previousOrder.filter(({ id }: { id: string }) => { return disconnectIds.indexOf(id) === -1 }) } // user connects/adds some relationship item. const connectedIds = resolvedData[targetFieldName]?.connect?.map((obj: { id: number }) => obj.id.toString() ) || [] if (connectedIds.length > 0) { // Query relationship items from the database. // Therefore, we can have other fields to record in the monitoring field const sfToConnect = await context.db[targetListName].findMany({ where: { id: { in: connectedIds } }, }) // Database query results are not sorted. // We need to sort them by ourselves. for (let i = 0; i < connectedIds.length; i++) { const sf = sfToConnect.find((obj) => { return `${obj.id}` === connectedIds[i] }) if (sf) { currentOrder.push({ id: sf.id.toString(), [targetListLabelField]: sf[targetListLabelField], }) } } } // records the order in the monitoring field resolvedData[fieldName] = currentOrder } } return resolvedData } return list } /** * This functiona adds the virtual field onto Keystone6 `list` object. * For instance, if we want to use monitoring field `manualOrderOfAuthors` * to monitor relationship field `authors` in the `post` list object. * * We could write * ``` * addVirtualFieldToReturnItemsInInputOrder(post, { * fieldName: 'manualOrderOfAuthors', // monitoring field * targetFieldName: 'authors', // monitored field * targetListName: 'Author' // relationship list * }) * ``` * after executing, * `post` list will have `authorsInInputOrder` virtual field. * * Return value of this virtual field will follow * `graphql.list(lists.Author.types.output)` GraphQL schema. * * And the GQL resolver will be defined in `resolve` function. */ function addVirtualFieldToReturnItemsInInputOrder( list: ListConfig<any, any>, manualOrderField: ManualOrderFieldConfig ) { const virtualFieldName = `${manualOrderField.targetFieldName}InInputOrder` list.fields[virtualFieldName] = virtual({ field: (lists) => { return graphql.field({ type: graphql.list( lists?.[manualOrderField.targetListName]?.types.output ), async resolve(item: Record<string, unknown>, args, context) { const manualOrderFieldValue = item?.[manualOrderField.fieldName] || [] if (!Array.isArray(manualOrderFieldValue)) { return [] } // collect ids from relationship items const ids = manualOrderFieldValue.map((value) => value.id) // query items from database const unorderedItems = await context.db?.[ manualOrderField.targetListName ].findMany({ where: { id: { in: ids } }, }) const orderedItems: BaseItem[] = [] // sort items according to input order manualOrderFieldValue.forEach((value) => { const writer = unorderedItems.find( (ui) => `${ui?.id}` === `${value?.id}` ) if (writer) { orderedItems.push(writer) } }) return orderedItems }, }) }, ui: { // keystone somehow needs `ui.query` even we "hidden" the field in the next two lines. query: `{ id, ${manualOrderField.targetFieldName} }`, itemView: { fieldMode: 'hidden' }, createView: { fieldMode: 'hidden' }, }, }) } export default addManualOrderRelationshipFields