From ea185d3f1c829703189dd3839a811a535e4736de Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 10 Dec 2024 16:32:03 +1100 Subject: [PATCH] fix structure relationship fields --- .../admin-ui/pages/ItemPage/index.tsx | 4 +- .../core/src/admin-ui/utils/useAdminMeta.tsx | 117 ++++++++--------- .../types/relationship/views/ComboboxMany.tsx | 1 - .../relationship/views/ComboboxSingle.tsx | 3 - .../fields/types/relationship/views/index.tsx | 21 ++-- .../relationship/views/useApolloQuery.ts | 27 ++-- .../component-blocks/api-shared.ts | 14 +-- .../component-blocks/form-from-preview.tsx | 118 +++++++++++------- packages/fields-document/src/index.ts | 14 +-- pnpm-lock.yaml | 19 +++ tests/sandbox/structure-relationships.tsx | 4 +- 11 files changed, 178 insertions(+), 164 deletions(-) diff --git a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx index b7963959157..96b583d4842 100644 --- a/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx +++ b/packages/core/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.tsx @@ -175,7 +175,7 @@ function ItemForm ({ position="form" fieldPositions={fieldPositions} onChange={useCallback(value => { - setValue(state => ({ item: state.item, value: value(state.value) })) + setValue(state => ({ item: state.item, value: state.value })) }, [setValue])} value={state.value} /> @@ -195,7 +195,7 @@ function ItemForm ({ fieldPositions={fieldPositions} onChange={useCallback( value => { - setValue(state => ({ item: state.item, value: value(state.value) })) + setValue(state => ({ item: state.item, value: state.value })) }, [setValue] )} diff --git a/packages/core/src/admin-ui/utils/useAdminMeta.tsx b/packages/core/src/admin-ui/utils/useAdminMeta.tsx index d3facba9358..f56bd482d09 100644 --- a/packages/core/src/admin-ui/utils/useAdminMeta.tsx +++ b/packages/core/src/admin-ui/utils/useAdminMeta.tsx @@ -1,45 +1,47 @@ -import { useEffect, useMemo, useState } from "react"; -import hashString from "@emotion/hash"; -import { type AdminMeta, type FieldViews } from "../../types"; -import { useLazyQuery } from "../apollo"; +import { + useEffect, + useMemo, + useState +} from "react" +import hashString from "@emotion/hash" +import type { AdminMeta, FieldViews } from "../../types" +import { useLazyQuery } from "../apollo" import { type StaticAdminMetaQuery, staticAdminMetaQuery, -} from "../admin-meta-graphql"; +} from "../admin-meta-graphql" -const expectedExports = new Set(["Field", "controller"]); -const adminMetaLocalStorageKey = "keystone.adminMeta"; +const expectedExports = new Set(["Field", "controller"]) +const adminMetaLocalStorageKey = "keystone.adminMeta" -let _mustRenderServerResult = true; +let _mustRenderServerResult = true function useMustRenderServerResult() { - const [, forceUpdate] = useState(0); + const [, forceUpdate] = useState(0) useEffect(() => { - _mustRenderServerResult = false; - forceUpdate(1); - }, []); + _mustRenderServerResult = false + forceUpdate(1) + }, []) - if (typeof window === "undefined") return true; + if (typeof window === "undefined") return true - return _mustRenderServerResult; + return _mustRenderServerResult } export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { const adminMetaFromLocalStorage = useMemo(() => { - if (typeof window === "undefined") return; + if (typeof window === "undefined") return - const item = localStorage.getItem(adminMetaLocalStorageKey); - if (item === null) return; + const item = localStorage.getItem(adminMetaLocalStorageKey) + if (item === null) return try { - const parsed = JSON.parse(item); - if (parsed.hash === adminMetaHash) { - return parsed.meta as StaticAdminMetaQuery["keystone"]["adminMeta"]; - } + const parsed = JSON.parse(item) + if (parsed.hash === adminMetaHash) return parsed.meta as StaticAdminMetaQuery["keystone"]["adminMeta"] } catch (err) { - return; + return } - }, [adminMetaHash]); + }, [adminMetaHash]) // it seems like Apollo doesn't skip the first fetch when using skip: true so we're using useLazyQuery instead const [fetchStaticAdminMeta, { data, error, called }] = useLazyQuery( @@ -47,64 +49,55 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { { fetchPolicy: "no-cache", // TODO: something is bugged } - ); + ) const shouldFetchAdminMeta = - adminMetaFromLocalStorage === undefined && !called; + adminMetaFromLocalStorage === undefined && !called useEffect(() => { - if (shouldFetchAdminMeta) { - fetchStaticAdminMeta(); - } - }, [shouldFetchAdminMeta, fetchStaticAdminMeta]); + if (shouldFetchAdminMeta) fetchStaticAdminMeta() + }, [shouldFetchAdminMeta, fetchStaticAdminMeta]) const runtimeAdminMeta = useMemo(() => { - if ((!data || error) && !adminMetaFromLocalStorage) return undefined; + if ((!data || error) && !adminMetaFromLocalStorage) return undefined const adminMeta: StaticAdminMetaQuery["keystone"]["adminMeta"] = adminMetaFromLocalStorage ? adminMetaFromLocalStorage - : data.keystone.adminMeta; + : data.keystone.adminMeta const runtimeAdminMeta: AdminMeta = { lists: {}, - }; + } for (const list of adminMeta.lists) { runtimeAdminMeta.lists[list.key] = { ...list, groups: [], fields: {}, - }; + } for (const field of list.fields) { for (const exportName of expectedExports) { if ((fieldViews[field.viewsIndex] as any)[exportName] === undefined) { throw new Error( `The view for the field at ${list.key}.${field.path} is missing the ${exportName} export` - ); + ) } } - const views = { ...fieldViews[field.viewsIndex] }; - const customViews: Record = {}; + const views = { ...fieldViews[field.viewsIndex] } + const customViews: Record = {} if (field.customViewsIndex !== null) { - const customViewsSource: FieldViews[number] & Record = - fieldViews[field.customViewsIndex]; - const allowedExportsOnCustomViews = new Set( - views.allowedExportsOnCustomViews - ); - Object.keys(customViewsSource).forEach((exportName) => { + const customViewsSource: FieldViews[number] & Record = fieldViews[field.customViewsIndex] + const allowedExportsOnCustomViews = new Set(views.allowedExportsOnCustomViews) + for (const exportName in customViewsSource) { if (allowedExportsOnCustomViews.has(exportName)) { - customViews[exportName] = customViewsSource[exportName]; + customViews[exportName] = customViewsSource[exportName] } else if (expectedExports.has(exportName)) { - (views as any)[exportName] = customViewsSource[exportName]; - } else { - throw new Error( - `Unexpected export named ${exportName} from the custom view from field at ${list.key}.${field.path}` - ); + (views as any)[exportName] = customViewsSource[exportName] } - }); + } } runtimeAdminMeta.lists[list.key].fields[field.path] = { @@ -121,7 +114,7 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { path: field.path, customViews, }), - }; + } } for (const group of list.groups) { @@ -131,7 +124,7 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { fields: group.fields.map( (field) => runtimeAdminMeta.lists[list.key].fields[field.path] ), - }); + }) } } @@ -142,28 +135,24 @@ export function useAdminMeta(adminMetaHash: string, fieldViews: FieldViews) { hash: hashString(JSON.stringify(adminMeta)), meta: adminMeta, }) - ); + ) } - return runtimeAdminMeta; - }, [data, error, adminMetaFromLocalStorage, fieldViews]); + return runtimeAdminMeta + }, [data, error, adminMetaFromLocalStorage, fieldViews]) - const mustRenderServerResult = useMustRenderServerResult(); + const mustRenderServerResult = useMustRenderServerResult() - if (mustRenderServerResult) { - return { state: "loading" as const }; - } - if (runtimeAdminMeta) { - return { state: "loaded" as const, value: runtimeAdminMeta }; - } + if (mustRenderServerResult) return { state: "loading" as const } + if (runtimeAdminMeta) return { state: "loaded" as const, value: runtimeAdminMeta } if (error) { return { state: "error" as const, error, refetch: async () => { - await fetchStaticAdminMeta(); + await fetchStaticAdminMeta() }, - }; + } } - return { state: "loading" as const }; + return { state: "loading" as const } } diff --git a/packages/core/src/fields/types/relationship/views/ComboboxMany.tsx b/packages/core/src/fields/types/relationship/views/ComboboxMany.tsx index 588683e259d..61ef4709db4 100644 --- a/packages/core/src/fields/types/relationship/views/ComboboxMany.tsx +++ b/packages/core/src/fields/types/relationship/views/ComboboxMany.tsx @@ -40,7 +40,6 @@ export function ComboboxMany ({ }) { const { data, loadingState, error, onLoadMore, search, setSearch } = useApolloQuery({ - extraSelection, labelField, list, searchFields, diff --git a/packages/core/src/fields/types/relationship/views/ComboboxSingle.tsx b/packages/core/src/fields/types/relationship/views/ComboboxSingle.tsx index 6c91651ada2..3871f4e37c2 100644 --- a/packages/core/src/fields/types/relationship/views/ComboboxSingle.tsx +++ b/packages/core/src/fields/types/relationship/views/ComboboxSingle.tsx @@ -19,7 +19,6 @@ export function ComboboxSingle ({ list, placeholder, state, - extraSelection = '', }: { autoFocus?: boolean description?: string @@ -36,11 +35,9 @@ export function ComboboxSingle ({ value: RelationshipValue | null onChange(value: RelationshipValue | null): void } - extraSelection?: string }) { const { data, loading, error, onLoadMore, search, setSearch } = useApolloQuery({ - extraSelection, labelField, list, searchFields, diff --git a/packages/core/src/fields/types/relationship/views/index.tsx b/packages/core/src/fields/types/relationship/views/index.tsx index 23f1e00f89b..4620a6ea2ab 100644 --- a/packages/core/src/fields/types/relationship/views/index.tsx +++ b/packages/core/src/fields/types/relationship/views/index.tsx @@ -29,13 +29,16 @@ export function Field (props: FieldProps) { } = props const foreignList = useList(field.refListKey) const [dialogIsOpen, setDialogOpen] = useState(false) + const description = field.description || undefined + const isReadOnly = onChange === undefined if (value.kind === 'count') { return ( @@ -48,11 +51,10 @@ export function Field (props: FieldProps) { setDialogOpen(true)} {...props}> {value.kind === 'many' ? ( ) { /> ) : ( ) { )} - {onChange !== undefined && ( + {!isReadOnly && ( setDialogOpen(false)}> {dialogIsOpen && ( { + deserialize: (data) => { if (config.fieldMeta.displayMode === 'count') { return { id: data.id, @@ -277,7 +278,7 @@ export function controller ( initialValue: value, } }, - serialize: state => { + serialize: (state) => { if (state.kind === 'many') { const newAllIds = new Set(state.value.map(x => x.id)) const initialIds = new Set(state.initialValue.map(x => x.id)) diff --git a/packages/core/src/fields/types/relationship/views/useApolloQuery.ts b/packages/core/src/fields/types/relationship/views/useApolloQuery.ts index b4342b1b9df..f920dbeb9a4 100644 --- a/packages/core/src/fields/types/relationship/views/useApolloQuery.ts +++ b/packages/core/src/fields/types/relationship/views/useApolloQuery.ts @@ -31,7 +31,6 @@ function useDebouncedValue (value: T, limitMs: number) { } export function useApolloQuery (args: { - extraSelection: string labelField: string list: ListMeta searchFields: string[] @@ -39,8 +38,8 @@ export function useApolloQuery (args: { | { kind: 'many', value: RelationshipValue[] } | { kind: 'one', value: RelationshipValue | null } }) { + const { labelField, list, searchFields, state } = args const keystone = useKeystone() - const { extraSelection, labelField, list, searchFields, state } = args const [search, setSearch] = useState(() => { if (state.kind === 'one' && state.value?.label) return state.value?.label return '' @@ -54,7 +53,6 @@ export function useApolloQuery (args: { items: ${list.graphql.names.listQueryName}(where: $where, take: $take, skip: $skip) { id: id label: ${labelField} - ${extraSelection} } count: ${list.graphql.names.listQueryCountName}(where: $where) } @@ -113,7 +111,6 @@ export function useApolloQuery (args: { // doesn't seem to become true when fetching more const [lastFetchMore, setLastFetchMore] = useState<{ where: Record - extraSelection: string list: ListMeta skip: number } | null>(null) @@ -125,24 +122,22 @@ export function useApolloQuery (args: { !loading && skip && data.items.length < count && - (lastFetchMore?.extraSelection !== extraSelection || - lastFetchMore?.where !== where || - lastFetchMore?.list !== list || - lastFetchMore?.skip !== skip) + (lastFetchMore?.where !== where || + lastFetchMore?.list !== list || + lastFetchMore?.skip !== skip) ) { const QUERY: TypedDocumentNode< - { items: { id: string; label: string | null }[] }, - { where: Record; take: number; skip: number } + { items: { id: string, label: string | null }[] }, + { where: Record, take: number, skip: number } > = gql` query RelationshipSelectMore($where: ${list.graphql.names.whereInputName}!, $take: Int!, $skip: Int!) { items: ${list.graphql.names.listQueryName}(where: $where, take: $take, skip: $skip) { label: ${labelField} id: id - ${extraSelection} } } ` - setLastFetchMore({ extraSelection, list, skip, where }) + setLastFetchMore({ list, skip, where }) fetchMore({ query: QUERY, variables: { @@ -151,12 +146,8 @@ export function useApolloQuery (args: { skip, }, }) - .then(() => { - setLastFetchMore(null) - }) - .catch(() => { - setLastFetchMore(null) - }) + .then(() => setLastFetchMore(null)) + .catch(() => setLastFetchMore(null)) } } diff --git a/packages/fields-document/src/DocumentEditor/component-blocks/api-shared.ts b/packages/fields-document/src/DocumentEditor/component-blocks/api-shared.ts index 452b8d82928..92b7cad9d2f 100644 --- a/packages/fields-document/src/DocumentEditor/component-blocks/api-shared.ts +++ b/packages/fields-document/src/DocumentEditor/component-blocks/api-shared.ts @@ -115,9 +115,9 @@ export type ArrayField = { export type RelationshipField = { kind: 'relationship' listKey: string - selection: string | undefined label: string many: Many + selection?: string } export interface ObjectField< @@ -297,15 +297,15 @@ type DiscriminantStringToDiscriminantValue< : DiscriminantString export type HydratedRelationshipData = { - id: string - label: string - data: Record + id: string | number + label: string | null + data?: Record } export type RelationshipData = { - id: string - label?: string - data?: Record + id: string | number + label: string | null + data?: Record } type ValueForRenderingFromComponentPropField = diff --git a/packages/fields-document/src/DocumentEditor/component-blocks/form-from-preview.tsx b/packages/fields-document/src/DocumentEditor/component-blocks/form-from-preview.tsx index 0f26447086c..8be52f81cff 100644 --- a/packages/fields-document/src/DocumentEditor/component-blocks/form-from-preview.tsx +++ b/packages/fields-document/src/DocumentEditor/component-blocks/form-from-preview.tsx @@ -1,7 +1,7 @@ /** @jsxRuntime classic */ /** @jsx jsx */ import { useList } from '@keystone-6/core/admin-ui/context' -import { RelationshipSelect } from '@keystone-6/core/fields/types/relationship/views/RelationshipSelect' +import { Field as RelationshipFieldView } from '@keystone-6/core/fields/types/relationship/views' import { jsx } from '@keystone-ui/core' import { GroupIndicatorLine } from '@keystone-6/core/admin-ui/utils' @@ -18,9 +18,9 @@ import { move } from '@keystar/ui/drag-and-drop' import { trash2Icon } from '@keystar/ui/icon/icons/trash2Icon' import { + type Key, type MemoExoticComponent, type ReactElement, - type Key, memo, useCallback, useMemo, @@ -36,7 +36,6 @@ import type { GenericPreviewProps, InitialOrUpdateValueFromComponentPropField, ObjectField, - RelationshipData, RelationshipField, ValueForComponentSchema, } from './api' @@ -249,52 +248,75 @@ function ArrayFieldPreview (props: DefaultFieldProps<'array'>) { ) } -function RelationshipFieldPreview ({ - schema, - autoFocus, - onChange, - value, -}: DefaultFieldProps<'relationship'>) { - const list = useList(schema.listKey) - - // TODO: FIXME - const searchFields = Object.keys(list.fields).filter(key => list.fields[key].search) +function RelationshipFieldPreview (props: DefaultFieldProps<'relationship'>) { + const { + autoFocus, + onChange, + schema, + value + } = props + const { + label, + listKey, + many + } = schema + const list = useList(listKey) + const formValue = (function () { + if (many) { + if (value !== null && !('length' in value)) throw TypeError('bad value') + const manyValue = value === null + ? [] + : value.map(x => ({ + id: x.id, + label: x.label || x.id.toString(), + data: x.data, + built: undefined + })) + return { + kind: 'many' as const, + id: '', // unused + initialValue: manyValue, + value: manyValue + } + } - return ( -
- {schema.label} - ({ - id: x.id, - label: x.label || x.id, - data: x.data, - })), - onChange: onChange, - } - : { - kind: 'one', - value: value - ? { - ...(value as RelationshipData), - label: (value as RelationshipData).label || (value as RelationshipData).id, - } - : null, - onChange: onChange, - } - } - /> -
- ) + if (value !== null && 'length' in value) throw TypeError('bad value') + const oneValue = value ? { + id: value.id, + label: value.label || value.id.toString(), + data: value.data, + built: undefined + } : null + return { + kind: 'one' as const, + id: '', // unused + initialValue: oneValue, + value: oneValue + } + })() + + return { + if (thing.kind === 'count') return // shouldnt happen + onChange(thing.value) + }} + value={formValue} + itemValue={{}} + /> } function FormFieldPreview ({ diff --git a/packages/fields-document/src/index.ts b/packages/fields-document/src/index.ts index 8450103337e..d5582b87792 100644 --- a/packages/fields-document/src/index.ts +++ b/packages/fields-document/src/index.ts @@ -8,14 +8,14 @@ import { jsonFieldTypePolyfilledForSQLite, } from '@keystone-6/core/types' import { graphql } from '@keystone-6/core' -import { type Relationships } from './DocumentEditor/relationship-shared' -import { type ComponentBlock } from './DocumentEditor/component-blocks/api-shared' +import type { Relationships } from './DocumentEditor/relationship-shared' +import type { ComponentBlock } from './DocumentEditor/component-blocks/api-shared' import { validateAndNormalizeDocument } from './validation' import { addRelationshipData } from './relationship-data' import { assertValidComponentSchema } from './DocumentEditor/component-blocks/field-assertions' -import { - type DocumentFeatures, - type controller, +import type { + DocumentFeatures, + controller, } from './views-shared' type RelationshipsConfig = Record ({ }, }), resolve ({ value }) { - if (value === null) { - return null - } + if (value === null) return null return { document: value } }, }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3caf546bfe..12c4091317d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -710,6 +710,25 @@ importers: specifier: ^5.5.0 version: 5.7.2 + examples/component-block-structures: + dependencies: + '@keystone-6/core': + specifier: ^6.3.1 + version: link:../../packages/core + '@keystone-6/fields-document': + specifier: ^9.1.1 + version: link:../../packages/fields-document + '@prisma/client': + specifier: 5.19.0 + version: 5.19.0(prisma@5.19.0) + devDependencies: + prisma: + specifier: 5.19.0 + version: 5.19.0 + typescript: + specifier: ^5.5.0 + version: 5.7.2 + examples/custom-admin-ui-logo: dependencies: '@keystone-6/core': diff --git a/tests/sandbox/structure-relationships.tsx b/tests/sandbox/structure-relationships.tsx index 6d280a3e67f..fd3a00f0840 100644 --- a/tests/sandbox/structure-relationships.tsx +++ b/tests/sandbox/structure-relationships.tsx @@ -1,10 +1,8 @@ import { - type ArrayField, - type ComponentSchemaForGraphQL, fields, } from '@keystone-6/fields-document/component-blocks' -export const schema: ArrayField = fields.array( +export const schema = fields.array( fields.relationship({ label: 'My things', listKey: 'Thing',