@graphcommerce/hygraph-dynamic-rows-ui 9.0.0-canary.69 → 9.0.0-canary.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @graphcommerce/hygraph-dynamic-rows-ui
2
2
 
3
+ ## 9.0.0-canary.70
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2347](https://github.com/graphcommerce-org/graphcommerce/pull/2347) [`7fa50a2`](https://github.com/graphcommerce-org/graphcommerce/commit/7fa50a2f21ee9edbc67d06d7694316f101f9415f) - Resolve issue where the dynamic rows UI wouldn’t load any definitions ([@paales](https://github.com/paales))
8
+
3
9
  ## 9.0.0-canary.69
4
10
 
5
11
  ## 9.0.0-canary.68
@@ -1,91 +1,59 @@
1
- import { ApolloClient, InMemoryCache } from '@apollo/client'
2
- import { useFieldExtension } from '@hygraph/app-sdk-react'
3
- // eslint-disable-next-line @typescript-eslint/no-restricted-imports
1
+ import { ApolloClient, gql, InMemoryCache, useQuery } from '@apollo/client'
2
+ import { FieldExtensionProps, useFieldExtension } from '@hygraph/app-sdk-react'
4
3
  import { TextField } from '@mui/material'
4
+ import { getIntrospectionQuery, IntrospectionQuery } from 'graphql'
5
5
  import { useEffect, useMemo, useState } from 'react'
6
- import { createOptionsFromInterfaceObject, objectifyGraphQLInterface } from '../lib'
7
- import { fetchGraphQLInterface } from '../lib/fetchGraphQLInterface'
8
- import { __Field } from '../types'
6
+ import { getFieldPaths } from '../lib/getFieldPaths'
7
+
8
+ function useClient(extension: FieldExtensionProps['extension']) {
9
+ return useMemo(
10
+ () =>
11
+ new ApolloClient({
12
+ uri:
13
+ typeof extension.config.backend === 'string'
14
+ ? extension.config.backend
15
+ : 'https://graphcommerce.vercel.app/api/graphql', // fallback on the standard GraphCommerce Schema
16
+ cache: new InMemoryCache(),
17
+ }),
18
+ [extension.config.backend],
19
+ )
20
+ }
9
21
 
10
22
  export function PropertyPicker() {
11
- const { value, onChange, extension } = useFieldExtension()
23
+ const fieldExtension = useFieldExtension()
24
+
25
+ const { value, onChange, extension } = fieldExtension
12
26
  const [localValue, setLocalValue] = useState<string | undefined | null>(
13
27
  typeof value === 'string' ? value : undefined,
14
28
  )
15
- const [fields, setFields] = useState<__Field[] | null>(null)
29
+
30
+ const client = useClient(extension)
31
+ const { data, loading, error } = useQuery<IntrospectionQuery>(gql(getIntrospectionQuery()), {
32
+ client,
33
+ })
34
+ // eslint-disable-next-line no-underscore-dangle
35
+ const schema = data?.__schema
16
36
 
17
37
  useEffect(() => {
18
38
  onChange(localValue).catch((err) => err)
19
39
  }, [localValue, onChange])
20
40
 
21
- const graphQLInterfaceQuery = useMemo(() => {
22
- const client = new ApolloClient({
23
- uri:
24
- typeof extension.config.backend === 'string'
25
- ? extension.config.backend
26
- : 'https://graphcommerce.vercel.app/api/graphql', // fallback on the standard GraphCommerce Schema
27
- cache: new InMemoryCache(),
28
- })
29
- return fetchGraphQLInterface(client)
30
- }, [extension.config.backend])
31
-
32
- // Prepare options
33
- const numberOptions = useMemo(
34
- () =>
35
- createOptionsFromInterfaceObject(
36
- objectifyGraphQLInterface(fields, 'number', ['ProductInterface']),
37
- ),
38
- [fields],
39
- )
40
- const textOptions = useMemo(
41
- () =>
42
- createOptionsFromInterfaceObject(
43
- objectifyGraphQLInterface(fields, 'text', ['ProductInterface']),
44
- ),
45
- [fields],
46
- )
47
- const allOptions = useMemo(
48
- () => ({
49
- text: [...textOptions, { label: 'url', id: 'url' }].sort((a, b) => {
50
- if (!a.label.includes('.') && !b.label.includes('.')) {
51
- return a.label.localeCompare(b.label)
52
- }
53
- if (a.label.includes('.')) {
54
- return 1
55
- }
56
- return -1
57
- }),
58
- number: [...numberOptions, { label: 'url', id: 'url' }],
59
- }),
60
- [numberOptions, textOptions],
61
- )
62
-
63
- // For now this we can not split number and text field options anymore as Hygraph made parent field apiId unreachable :/
64
- // const fieldType = field.parent.apiId ?? 'ConditionText'
65
- // const options = fieldType === 'ConditionNumber' ? allOptions.number : allOptions.text
66
- const options = [...allOptions.text, ...allOptions.number]
67
-
68
- if (!fields) {
69
- Promise.resolve(graphQLInterfaceQuery)
70
- .then((res) => {
71
- const newFields: __Field[] = res?.data.__type?.fields
72
-
73
- setFields(newFields)
74
- })
75
- .catch((err) => err)
76
-
77
- return <div>Loading fields...</div>
78
- }
79
- if (options.length < 1) return <div>No properties available</div>
80
- if (options.length > 10000) return <div>Too many properties to display</div>
41
+ const fieldPaths = useMemo(() => {
42
+ if (!schema) return []
43
+ return getFieldPaths(schema, ['ProductInterface'])
44
+ .sort((a, b) => a.depth() - b.depth())
45
+ .map((fp) => fp.stringify())
46
+ .filter<string>((v) => v !== undefined)
47
+ }, [schema])
81
48
 
82
49
  return (
83
50
  <TextField
84
51
  id='property-selector'
85
- select
52
+ select={!!fieldPaths.length}
53
+ variant='outlined'
54
+ size='small'
86
55
  SelectProps={{
87
56
  native: true,
88
- variant: 'outlined',
89
57
  }}
90
58
  value={localValue}
91
59
  onChange={(v) => {
@@ -95,6 +63,7 @@ export function PropertyPicker() {
95
63
  fullWidth
96
64
  sx={{
97
65
  mt: '4px',
66
+ fontSize: '0.8em',
98
67
  '& .MuiInputBase-root': {
99
68
  borderRadius: { xs: '2px!important' },
100
69
  },
@@ -119,11 +88,18 @@ export function PropertyPicker() {
119
88
  },
120
89
  }}
121
90
  >
122
- {options.map((o) => (
123
- <option key={o.id} value={o.id}>
124
- {o.label}
125
- </option>
126
- ))}
91
+ {fieldPaths.length > 0 ? (
92
+ <>
93
+ <option value='url'>url</option>
94
+ {fieldPaths.map((fp) => (
95
+ <option key={fp} value={fp}>
96
+ {fp}
97
+ </option>
98
+ ))}
99
+ </>
100
+ ) : (
101
+ <>{loading ? 'Loading..' : error?.message}</>
102
+ )}
127
103
  </TextField>
128
104
  )
129
105
  }
@@ -0,0 +1,66 @@
1
+ import { IntrospectionField, IntrospectionOutputTypeRef, IntrospectionSchema } from 'graphql'
2
+
3
+ function getType(type: IntrospectionOutputTypeRef) {
4
+ switch (type.kind) {
5
+ case 'NON_NULL':
6
+ case 'LIST':
7
+ return getType(type.ofType)
8
+ default:
9
+ return type
10
+ }
11
+ }
12
+
13
+ class FieldPath {
14
+ constructor(
15
+ public field: IntrospectionField,
16
+ private prev: FieldPath | undefined,
17
+ ) {}
18
+
19
+ stringify(filter?: string[]): string | undefined {
20
+ if (this.field.type.kind === 'SCALAR' && filter && !filter.includes(this.field.type.name)) {
21
+ return undefined
22
+ }
23
+
24
+ const prevStr = this.prev?.stringify(filter)
25
+ return prevStr ? `${prevStr}.${this.field.name}` : this.field.name
26
+ }
27
+
28
+ depth = () => (this?.prev?.depth() ?? 0) + 1
29
+ }
30
+
31
+ export function getFieldPaths(
32
+ schema: IntrospectionSchema,
33
+ types: string[],
34
+ prevPath: FieldPath | undefined = undefined,
35
+ ): FieldPath[] {
36
+ const typeName = types[types.length - 1]
37
+
38
+ const paths: FieldPath[] = []
39
+ const type = schema.types.find((t) => t.name === typeName)
40
+
41
+ if (!type) return paths
42
+
43
+ if ((prevPath?.depth() ?? 0) > 3) return paths
44
+
45
+ if (type.kind === 'OBJECT' || type.kind === 'INTERFACE') {
46
+ type.fields.forEach((field) => {
47
+ const t = getType(field.type)
48
+
49
+ if (!types.includes(t.name) && !field.deprecationReason) {
50
+ const newTypes = [...types, t.name]
51
+
52
+ const newPath = new FieldPath(field, prevPath)
53
+
54
+ if (t.kind === 'OBJECT' || t.kind === 'INTERFACE') {
55
+ paths.push(...getFieldPaths(schema, newTypes, newPath))
56
+ } else if (t.kind === 'SCALAR' || t.kind === 'ENUM') {
57
+ paths.push(newPath)
58
+ } else if (t.kind === 'UNION') {
59
+ // not supported currently
60
+ }
61
+ }
62
+ })
63
+ }
64
+
65
+ return paths
66
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/hygraph-dynamic-rows-ui",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "9.0.0-canary.69",
5
+ "version": "9.0.0-canary.70",
6
6
  "sideEffects": false,
7
7
  "type": "commonjs",
8
8
  "prettier": "@graphcommerce/prettier-config-pwa",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@apollo/client": "~3.10.8",
20
- "@graphcommerce/next-config": "9.0.0-canary.69",
20
+ "@graphcommerce/next-config": "9.0.0-canary.70",
21
21
  "@hygraph/app-sdk-react": "^0.0.4",
22
22
  "@lingui/core": "^4.11.2",
23
23
  "@lingui/macro": "^4.11.2",
@@ -32,9 +32,9 @@
32
32
  "webpack": "~5.93.0"
33
33
  },
34
34
  "devDependencies": {
35
- "@graphcommerce/eslint-config-pwa": "9.0.0-canary.69",
36
- "@graphcommerce/prettier-config-pwa": "9.0.0-canary.69",
37
- "@graphcommerce/typescript-config-pwa": "9.0.0-canary.69",
35
+ "@graphcommerce/eslint-config-pwa": "9.0.0-canary.70",
36
+ "@graphcommerce/prettier-config-pwa": "9.0.0-canary.70",
37
+ "@graphcommerce/typescript-config-pwa": "9.0.0-canary.70",
38
38
  "@types/react-is": "^18.3.0",
39
39
  "babel-plugin-macros": "^3.1.0",
40
40
  "eslint": "^8",
@@ -1,15 +1,14 @@
1
1
  import { Wrapper } from '@hygraph/app-sdk-react'
2
- import React from 'react'
3
- import { PropertyPicker } from '..'
2
+ import React, { useEffect } from 'react'
3
+ import { PropertyPicker } from '../components/PropertyPicker'
4
4
 
5
5
  export default function DRPropertyPicker() {
6
6
  const fieldContainer = React.useRef<HTMLDivElement | null>(null)
7
7
 
8
- React.useEffect(() => {
8
+ useEffect(() => {
9
9
  /**
10
10
  * Some styling is being undone here to resolve conflicts between Hygraph App SDK and CssAndFramerMotionProvider.
11
11
  */
12
-
13
12
  const frameBox1 = fieldContainer?.current?.parentElement
14
13
  if (frameBox1) {
15
14
  frameBox1.style.position = 'static'
package/pages/setup.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { useEffect } from 'react'
2
2
  import { Page } from '..'
3
3
 
4
4
  export default function Setup() {
@@ -8,7 +8,7 @@ export default function Setup() {
8
8
  * This is a hack to fix the height of the iframe, which was malfunctioning because of a conflict
9
9
  * with FramerNextPages
10
10
  */
11
- React.useEffect(() => {
11
+ useEffect(() => {
12
12
  const framerParent = appContainer?.current?.parentElement
13
13
  if (framerParent) {
14
14
  framerParent.style.position = 'static'
@@ -1,42 +0,0 @@
1
- import { ProductProperty } from '../types'
2
-
3
- export const createOptionsFromInterfaceObject = (
4
- obj: object,
5
- path = '',
6
- inputs: ProductProperty[] = [],
7
- parent = '',
8
- ): ProductProperty[] => {
9
- for (const [key, value] of Object.entries(obj)) {
10
- /** Keep count of the current path and parent */
11
- const currentPath: string = path ? `${path}.${key}` : key
12
- const currentParent: string = parent ? `${parent}/` : ''
13
-
14
- /**
15
- * If the value is a string, number or boolean, add it to the inputs array. If the value is an
16
- * array, recurse on the first item. If the value is an object, recurse on all it's keys.
17
- */
18
- if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
19
- inputs.push({
20
- label: currentPath,
21
- id: currentPath,
22
- })
23
- } else if (Array.isArray(value) && value.length > 0) {
24
- createOptionsFromInterfaceObject(
25
- value[0] as object,
26
- `${currentPath}[0]`,
27
- inputs,
28
- `${currentParent}${key}`,
29
- )
30
- } else if (typeof value === 'object' && value !== null) {
31
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
32
- createOptionsFromInterfaceObject(
33
- value as object,
34
- currentPath,
35
- inputs,
36
- `${currentParent}${key}`,
37
- )
38
- }
39
- }
40
-
41
- return inputs
42
- }
@@ -1,13 +0,0 @@
1
- export const createRecursiveIntrospectionQuery = (type: string, depth: number) => {
2
- let baseQuery = `__type(name: "${type}") { name fields { name `
3
- let endQuery = ' } }'
4
-
5
- for (let i = 0; i < depth; i++) {
6
- baseQuery += 'type { name ofType { name fields { name isDeprecated '
7
- endQuery += ' } } }'
8
- }
9
-
10
- const result = baseQuery + endQuery
11
-
12
- return result
13
- }
@@ -1,14 +0,0 @@
1
- import { ApolloClient, NormalizedCacheObject, gql } from '@apollo/client'
2
- import { createRecursiveIntrospectionQuery } from './createRecursiveIntrospectionQuery'
3
-
4
- export const fetchGraphQLInterface = (client: ApolloClient<NormalizedCacheObject>) => {
5
- const introspectionQuery = createRecursiveIntrospectionQuery('ProductInterface', 4)
6
-
7
- return client.query({
8
- query: gql`
9
- query getSchema {
10
- ${introspectionQuery}
11
- }
12
- `,
13
- })
14
- }
package/lib/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './createOptionsFromInterfaceObject'
2
- export * from './createRecursiveIntrospectionQuery'
3
- export * from './fetchGraphQLInterface'
4
- export * from './objectifyGraphQLInterface'
@@ -1,62 +0,0 @@
1
- import { __Field } from '../types'
2
-
3
- /**
4
- * In this function we create an object from the GraphQL interface.
5
- * We need this so we can map out the properties of an interface that is needed
6
- * for the Dynamic Rows Autocomplete.
7
- * @param fields - The GraphQL interface object that is read from the schema.
8
- * @returns
9
- */
10
- export const objectifyGraphQLInterface = (
11
- fields: __Field[] | null,
12
- conditionType: 'text' | 'number' | 'all',
13
- skip: string[],
14
- ): object => {
15
- let objectifiedInterface: object = {}
16
-
17
- if (!fields) return objectifiedInterface
18
-
19
- for (const [, value] of Object.entries(fields)) {
20
- const nestedFields = value?.type?.ofType?.fields
21
- const { isDeprecated } = value
22
- const typeOf = value?.type?.name
23
- const typeName = value?.type?.ofType?.name ?? ''
24
-
25
- /**
26
- * With typevalue we can know of which type a property is, so we for example can determine to to hide string values in ConditionNumbers.
27
- */
28
- let typeValue: 'number' | 'text' | 'boolean'
29
- switch (typeOf) {
30
- case 'Float' || 'Int':
31
- typeValue = 'number'
32
- break
33
- case 'Boolean':
34
- typeValue = 'text' // Seperate booleans are not supported yet.
35
- break
36
- default:
37
- typeValue = 'text'
38
- break
39
- }
40
-
41
- if (skip.includes(typeName) || isDeprecated || !value?.name) {
42
- // do nothing
43
- } else if (nestedFields) {
44
- objectifiedInterface = {
45
- ...objectifiedInterface,
46
- [value.name]: objectifyGraphQLInterface(nestedFields, conditionType, [...skip, typeName]),
47
- }
48
- } else if (typeOf && conditionType === 'all') {
49
- objectifiedInterface = {
50
- ...objectifiedInterface,
51
- [value.name]: typeValue,
52
- }
53
- } else if (conditionType === typeValue) {
54
- objectifiedInterface = {
55
- ...objectifiedInterface,
56
- [value.name]: typeValue,
57
- }
58
- }
59
- }
60
-
61
- return objectifiedInterface
62
- }