From 2330f056c14959545694a93ffb77efe9e336255b Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Mon, 5 Aug 2024 15:44:10 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Resolve=20issue=20where=20the=20dynamic=20r?= =?UTF-8?q?ows=20UI=20wouldn=E2=80=99t=20load=20any=20definitions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/PropertyPicker.tsx | 125 +++++++----------- .../lib/createOptionsFromInterfaceObject.ts | 42 ------ .../lib/createRecursiveIntrospectionQuery.ts | 13 -- .../lib/fetchGraphQLInterface.ts | 14 -- .../lib/getFieldPaths.ts | 66 +++++++++ packages/hygraph-dynamic-rows-ui/lib/index.ts | 4 - .../lib/objectifyGraphQLInterface.ts | 62 --------- .../pages/property-picker.tsx | 7 +- .../hygraph-dynamic-rows-ui/pages/setup.tsx | 4 +- 9 files changed, 121 insertions(+), 216 deletions(-) delete mode 100644 packages/hygraph-dynamic-rows-ui/lib/createOptionsFromInterfaceObject.ts delete mode 100644 packages/hygraph-dynamic-rows-ui/lib/createRecursiveIntrospectionQuery.ts delete mode 100644 packages/hygraph-dynamic-rows-ui/lib/fetchGraphQLInterface.ts create mode 100644 packages/hygraph-dynamic-rows-ui/lib/getFieldPaths.ts delete mode 100644 packages/hygraph-dynamic-rows-ui/lib/index.ts delete mode 100644 packages/hygraph-dynamic-rows-ui/lib/objectifyGraphQLInterface.ts diff --git a/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx b/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx index 8419ae70f8..84174f83be 100644 --- a/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx +++ b/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx @@ -1,91 +1,59 @@ -import { ApolloClient, InMemoryCache } from '@apollo/client' -import { useFieldExtension } from '@hygraph/app-sdk-react' -// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { ApolloClient, gql, InMemoryCache, useQuery } from '@apollo/client' +import { FieldExtensionProps, useFieldExtension } from '@hygraph/app-sdk-react' import { TextField } from '@mui/material' +import { getIntrospectionQuery, IntrospectionQuery } from 'graphql' import { useEffect, useMemo, useState } from 'react' -import { createOptionsFromInterfaceObject, objectifyGraphQLInterface } from '../lib' -import { fetchGraphQLInterface } from '../lib/fetchGraphQLInterface' -import { __Field } from '../types' +import { getFieldPaths } from '../lib/getFieldPaths' + +function useClient(extension: FieldExtensionProps['extension']) { + return useMemo( + () => + new ApolloClient({ + uri: + typeof extension.config.backend === 'string' + ? extension.config.backend + : 'https://graphcommerce.vercel.app/api/graphql', // fallback on the standard GraphCommerce Schema + cache: new InMemoryCache(), + }), + [extension.config.backend], + ) +} export function PropertyPicker() { - const { value, onChange, extension } = useFieldExtension() + const fieldExtension = useFieldExtension() + + const { value, onChange, extension } = fieldExtension const [localValue, setLocalValue] = useState( typeof value === 'string' ? value : undefined, ) - const [fields, setFields] = useState<__Field[] | null>(null) + + const client = useClient(extension) + const { data, loading, error } = useQuery(gql(getIntrospectionQuery()), { + client, + }) + // eslint-disable-next-line no-underscore-dangle + const schema = data?.__schema useEffect(() => { onChange(localValue).catch((err) => err) }, [localValue, onChange]) - const graphQLInterfaceQuery = useMemo(() => { - const client = new ApolloClient({ - uri: - typeof extension.config.backend === 'string' - ? extension.config.backend - : 'https://graphcommerce.vercel.app/api/graphql', // fallback on the standard GraphCommerce Schema - cache: new InMemoryCache(), - }) - return fetchGraphQLInterface(client) - }, [extension.config.backend]) - - // Prepare options - const numberOptions = useMemo( - () => - createOptionsFromInterfaceObject( - objectifyGraphQLInterface(fields, 'number', ['ProductInterface']), - ), - [fields], - ) - const textOptions = useMemo( - () => - createOptionsFromInterfaceObject( - objectifyGraphQLInterface(fields, 'text', ['ProductInterface']), - ), - [fields], - ) - const allOptions = useMemo( - () => ({ - text: [...textOptions, { label: 'url', id: 'url' }].sort((a, b) => { - if (!a.label.includes('.') && !b.label.includes('.')) { - return a.label.localeCompare(b.label) - } - if (a.label.includes('.')) { - return 1 - } - return -1 - }), - number: [...numberOptions, { label: 'url', id: 'url' }], - }), - [numberOptions, textOptions], - ) - - // For now this we can not split number and text field options anymore as Hygraph made parent field apiId unreachable :/ - // const fieldType = field.parent.apiId ?? 'ConditionText' - // const options = fieldType === 'ConditionNumber' ? allOptions.number : allOptions.text - const options = [...allOptions.text, ...allOptions.number] - - if (!fields) { - Promise.resolve(graphQLInterfaceQuery) - .then((res) => { - const newFields: __Field[] = res?.data.__type?.fields - - setFields(newFields) - }) - .catch((err) => err) - - return
Loading fields...
- } - if (options.length < 1) return
No properties available
- if (options.length > 10000) return
Too many properties to display
+ const fieldPaths = useMemo(() => { + if (!schema) return [] + return getFieldPaths(schema, ['ProductInterface']) + .sort((a, b) => a.depth() - b.depth()) + .map((fp) => fp.stringify()) + .filter((v) => v !== undefined) + }, [schema]) return ( { @@ -95,6 +63,7 @@ export function PropertyPicker() { fullWidth sx={{ mt: '4px', + fontSize: '0.8em', '& .MuiInputBase-root': { borderRadius: { xs: '2px!important' }, }, @@ -119,11 +88,17 @@ export function PropertyPicker() { }, }} > - {options.map((o) => ( - - ))} + {fieldPaths.length > 0 ? ( + <> + {fieldPaths.map((fp) => ( + + ))} + + ) : ( + <>{loading ? 'Loading..' : error?.message} + )} ) } diff --git a/packages/hygraph-dynamic-rows-ui/lib/createOptionsFromInterfaceObject.ts b/packages/hygraph-dynamic-rows-ui/lib/createOptionsFromInterfaceObject.ts deleted file mode 100644 index c62b33fd88..0000000000 --- a/packages/hygraph-dynamic-rows-ui/lib/createOptionsFromInterfaceObject.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ProductProperty } from '../types' - -export const createOptionsFromInterfaceObject = ( - obj: object, - path = '', - inputs: ProductProperty[] = [], - parent = '', -): ProductProperty[] => { - for (const [key, value] of Object.entries(obj)) { - /** Keep count of the current path and parent */ - const currentPath: string = path ? `${path}.${key}` : key - const currentParent: string = parent ? `${parent}/` : '' - - /** - * If the value is a string, number or boolean, add it to the inputs array. If the value is an - * array, recurse on the first item. If the value is an object, recurse on all it's keys. - */ - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - inputs.push({ - label: currentPath, - id: currentPath, - }) - } else if (Array.isArray(value) && value.length > 0) { - createOptionsFromInterfaceObject( - value[0] as object, - `${currentPath}[0]`, - inputs, - `${currentParent}${key}`, - ) - } else if (typeof value === 'object' && value !== null) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - createOptionsFromInterfaceObject( - value as object, - currentPath, - inputs, - `${currentParent}${key}`, - ) - } - } - - return inputs -} diff --git a/packages/hygraph-dynamic-rows-ui/lib/createRecursiveIntrospectionQuery.ts b/packages/hygraph-dynamic-rows-ui/lib/createRecursiveIntrospectionQuery.ts deleted file mode 100644 index 4d5ce310df..0000000000 --- a/packages/hygraph-dynamic-rows-ui/lib/createRecursiveIntrospectionQuery.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const createRecursiveIntrospectionQuery = (type: string, depth: number) => { - let baseQuery = `__type(name: "${type}") { name fields { name ` - let endQuery = ' } }' - - for (let i = 0; i < depth; i++) { - baseQuery += 'type { name ofType { name fields { name isDeprecated ' - endQuery += ' } } }' - } - - const result = baseQuery + endQuery - - return result -} diff --git a/packages/hygraph-dynamic-rows-ui/lib/fetchGraphQLInterface.ts b/packages/hygraph-dynamic-rows-ui/lib/fetchGraphQLInterface.ts deleted file mode 100644 index c1d3047ffb..0000000000 --- a/packages/hygraph-dynamic-rows-ui/lib/fetchGraphQLInterface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client' -import { createRecursiveIntrospectionQuery } from './createRecursiveIntrospectionQuery' - -export const fetchGraphQLInterface = (client: ApolloClient) => { - const introspectionQuery = createRecursiveIntrospectionQuery('ProductInterface', 4) - - return client.query({ - query: gql` - query getSchema { - ${introspectionQuery} - } - `, - }) -} diff --git a/packages/hygraph-dynamic-rows-ui/lib/getFieldPaths.ts b/packages/hygraph-dynamic-rows-ui/lib/getFieldPaths.ts new file mode 100644 index 0000000000..5794b5e19e --- /dev/null +++ b/packages/hygraph-dynamic-rows-ui/lib/getFieldPaths.ts @@ -0,0 +1,66 @@ +import { IntrospectionField, IntrospectionOutputTypeRef, IntrospectionSchema } from 'graphql' + +function getType(type: IntrospectionOutputTypeRef) { + switch (type.kind) { + case 'NON_NULL': + case 'LIST': + return getType(type.ofType) + default: + return type + } +} + +class FieldPath { + constructor( + public field: IntrospectionField, + private prev: FieldPath | undefined, + ) {} + + stringify(filter?: string[]): string | undefined { + if (this.field.type.kind === 'SCALAR' && filter && !filter.includes(this.field.type.name)) { + return undefined + } + + const prevStr = this.prev?.stringify(filter) + return prevStr ? `${prevStr}.${this.field.name}` : this.field.name + } + + depth = () => (this?.prev?.depth() ?? 0) + 1 +} + +export function getFieldPaths( + schema: IntrospectionSchema, + types: string[], + prevPath: FieldPath | undefined = undefined, +): FieldPath[] { + const typeName = types[types.length - 1] + + const paths: FieldPath[] = [] + const type = schema.types.find((t) => t.name === typeName) + + if (!type) return paths + + if ((prevPath?.depth() ?? 0) > 3) return paths + + if (type.kind === 'OBJECT' || type.kind === 'INTERFACE') { + type.fields.forEach((field) => { + const t = getType(field.type) + + if (!types.includes(t.name) && !field.deprecationReason) { + const newTypes = [...types, t.name] + + const newPath = new FieldPath(field, prevPath) + + if (t.kind === 'OBJECT' || t.kind === 'INTERFACE') { + paths.push(...getFieldPaths(schema, newTypes, newPath)) + } else if (t.kind === 'SCALAR' || t.kind === 'ENUM') { + paths.push(newPath) + } else if (t.kind === 'UNION') { + // not supported currently + } + } + }) + } + + return paths +} diff --git a/packages/hygraph-dynamic-rows-ui/lib/index.ts b/packages/hygraph-dynamic-rows-ui/lib/index.ts deleted file mode 100644 index 576c4b767c..0000000000 --- a/packages/hygraph-dynamic-rows-ui/lib/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './createOptionsFromInterfaceObject' -export * from './createRecursiveIntrospectionQuery' -export * from './fetchGraphQLInterface' -export * from './objectifyGraphQLInterface' diff --git a/packages/hygraph-dynamic-rows-ui/lib/objectifyGraphQLInterface.ts b/packages/hygraph-dynamic-rows-ui/lib/objectifyGraphQLInterface.ts deleted file mode 100644 index 75f16bc683..0000000000 --- a/packages/hygraph-dynamic-rows-ui/lib/objectifyGraphQLInterface.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { __Field } from '../types' - -/** - * In this function we create an object from the GraphQL interface. - * We need this so we can map out the properties of an interface that is needed - * for the Dynamic Rows Autocomplete. - * @param fields - The GraphQL interface object that is read from the schema. - * @returns - */ -export const objectifyGraphQLInterface = ( - fields: __Field[] | null, - conditionType: 'text' | 'number' | 'all', - skip: string[], -): object => { - let objectifiedInterface: object = {} - - if (!fields) return objectifiedInterface - - for (const [, value] of Object.entries(fields)) { - const nestedFields = value?.type?.ofType?.fields - const { isDeprecated } = value - const typeOf = value?.type?.name - const typeName = value?.type?.ofType?.name ?? '' - - /** - * With typevalue we can know of which type a property is, so we for example can determine to to hide string values in ConditionNumbers. - */ - let typeValue: 'number' | 'text' | 'boolean' - switch (typeOf) { - case 'Float' || 'Int': - typeValue = 'number' - break - case 'Boolean': - typeValue = 'text' // Seperate booleans are not supported yet. - break - default: - typeValue = 'text' - break - } - - if (skip.includes(typeName) || isDeprecated || !value?.name) { - // do nothing - } else if (nestedFields) { - objectifiedInterface = { - ...objectifiedInterface, - [value.name]: objectifyGraphQLInterface(nestedFields, conditionType, [...skip, typeName]), - } - } else if (typeOf && conditionType === 'all') { - objectifiedInterface = { - ...objectifiedInterface, - [value.name]: typeValue, - } - } else if (conditionType === typeValue) { - objectifiedInterface = { - ...objectifiedInterface, - [value.name]: typeValue, - } - } - } - - return objectifiedInterface -} diff --git a/packages/hygraph-dynamic-rows-ui/pages/property-picker.tsx b/packages/hygraph-dynamic-rows-ui/pages/property-picker.tsx index 0f430193dc..d989badd72 100644 --- a/packages/hygraph-dynamic-rows-ui/pages/property-picker.tsx +++ b/packages/hygraph-dynamic-rows-ui/pages/property-picker.tsx @@ -1,15 +1,14 @@ import { Wrapper } from '@hygraph/app-sdk-react' -import React from 'react' -import { PropertyPicker } from '..' +import React, { useEffect } from 'react' +import { PropertyPicker } from '../components/PropertyPicker' export default function DRPropertyPicker() { const fieldContainer = React.useRef(null) - React.useEffect(() => { + useEffect(() => { /** * Some styling is being undone here to resolve conflicts between Hygraph App SDK and CssAndFramerMotionProvider. */ - const frameBox1 = fieldContainer?.current?.parentElement if (frameBox1) { frameBox1.style.position = 'static' diff --git a/packages/hygraph-dynamic-rows-ui/pages/setup.tsx b/packages/hygraph-dynamic-rows-ui/pages/setup.tsx index 7ef5e4fe93..84f2cf07b6 100644 --- a/packages/hygraph-dynamic-rows-ui/pages/setup.tsx +++ b/packages/hygraph-dynamic-rows-ui/pages/setup.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import { Page } from '..' export default function Setup() { @@ -8,7 +8,7 @@ export default function Setup() { * This is a hack to fix the height of the iframe, which was malfunctioning because of a conflict * with FramerNextPages */ - React.useEffect(() => { + useEffect(() => { const framerParent = appContainer?.current?.parentElement if (framerParent) { framerParent.style.position = 'static' From 3947b35b049133d5c1b6c816292d90ecc0af3f04 Mon Sep 17 00:00:00 2001 From: Paul Hachmang Date: Mon, 5 Aug 2024 15:47:35 +0200 Subject: [PATCH 2/3] Added missing URL option --- packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx b/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx index 84174f83be..814e3e6668 100644 --- a/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx +++ b/packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx @@ -90,6 +90,7 @@ export function PropertyPicker() { > {fieldPaths.length > 0 ? ( <> + {fieldPaths.map((fp) => (