@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 +6 -0
- package/components/PropertyPicker.tsx +51 -75
- package/lib/getFieldPaths.ts +66 -0
- package/package.json +5 -5
- package/pages/property-picker.tsx +3 -4
- package/pages/setup.tsx +2 -2
- package/lib/createOptionsFromInterfaceObject.ts +0 -42
- package/lib/createRecursiveIntrospectionQuery.ts +0 -13
- package/lib/fetchGraphQLInterface.ts +0 -14
- package/lib/index.ts +0 -4
- package/lib/objectifyGraphQLInterface.ts +0 -62
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 {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
36
|
-
"@graphcommerce/prettier-config-pwa": "9.0.0-canary.
|
|
37
|
-
"@graphcommerce/typescript-config-pwa": "9.0.0-canary.
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
-
}
|