Skip to content

Commit

Permalink
Merge pull request #2100 from graphcommerce-org/feature/GCOM-1108
Browse files Browse the repository at this point in the history
Feature/GCOM-1108 | Property picker for Dynamic Rows (V2)
  • Loading branch information
paales authored Nov 24, 2023
2 parents 63cc9b6 + 82067d7 commit 320364b
Show file tree
Hide file tree
Showing 20 changed files with 1,656 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/short-toys-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphcommerce/hygraph-dynamic-rows-ui': minor
---

Add Dynamic Row UI for property UI field through a custom Hygraph application
45 changes: 45 additions & 0 deletions docs/hygraph/property-picker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Hygraph Dynamic Rows

As you might have learned, Dynamic Rows enable the addition of rows across
multiple pages through rule-based relationships, rather than manually adding a
row to each page. These rules hinge on shared attributes among the pages, with a
category being a typical example of such an attribute. To enable the Dynamic Row
UI Extension, follow the installation instructions as below.

> Installation
>
> [Click here to install the Dynamic Row UI Extension](https://app.hygraph.com/apps/dynamic-row-property-picker/new)
<img width="1792" alt="image" src="https://github.com/graphcommerce-org/graphcommerce/assets/49681263/3226eedb-e58c-4d3f-9516-14ff6ed56f24">

## Enabling the Application

Once you click the link and authorize the application, you'll be taken to the
app's configuration page. On this page, you can switch the application on or off
as needed.

<img width="1792" alt="image" src="https://github.com/graphcommerce-org/graphcommerce/assets/49681263/ec9b55f6-14f6-466e-8a31-0b893ffe1297">

## Enabling the field

Now to enable the field, go to your Hygraph schema. Under components you should
have a `Text` and `Number` component. Each of these have a field with api ID
`property`. You will have to delete this field in both components. This will
result in current field data being lost, so in case you are migrating to the
extended UI, make sure to have a copy of those fields somewhere else.

> Note
>
> Make sure you migrated your schema to Graphcommerce 7.0 with
> [our Hygraph-CLI.](./cli.md)
Replace the existing fields with the new `Property picker` field in the right sidebar
(it should be under `Slug` and above `Rich text`). While adding the
`Property picker` field make sure that you make it `required`.

<img width="1792" alt="image" src="https://github.com/graphcommerce-org/graphcommerce/assets/49681263/9206f86d-477c-4eaf-bec6-1648874bee5e">

## Start building with your new Dynamic Rows UI!

If you have any questions about the feature, please reach out to us in our Slack
channel.
124 changes: 124 additions & 0 deletions packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useFieldExtension } from '@hygraph/app-sdk-react'
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { TextField } from '@mui/material'
import { useEffect, useMemo, useState } from 'react'
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { fetchGraphQLInterface } from '../lib/fetchGraphQLInterface'
import { createOptionsFromInterfaceObject, objectifyGraphQLInterface } from '../lib'

export function PropertyPicker() {
const { value, onChange, field, extension } = useFieldExtension()
const [localValue, setLocalValue] = useState<string | undefined | null>(
typeof value === 'string' ? value : undefined,
)
const [fields, setFields] = useState<any>(null)

useEffect(() => {
onChange(localValue).catch((err) => console.log(err))
}, [localValue, onChange])

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(),
})

const graphQLInterfaceQuery = useMemo(() => fetchGraphQLInterface(client), [client])

// 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],
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - outdated types from @hygraph/app-sdk-react
const fieldType = field.parent.apiId ?? 'ConditionText'
const options = fieldType === 'ConditionNumber' ? allOptions.number : allOptions.text

if (!fields) {
Promise.resolve(graphQLInterfaceQuery).then((res) => {
const fields = res?.data.__type?.fields

setFields(fields)
})
return <div>Loading fields...</div>
}
if (options.length < 1) return <div>No properties available</div>
if (options.length > 10000) return <div>Too many properties to display</div>

return (
<TextField
id='property-selector'
select
SelectProps={{
native: true,
variant: 'outlined',
}}
value={localValue}
onChange={(v) => {
const val = v.target.value
setLocalValue(val)
}}
fullWidth
sx={{
mt: '4px',
'& .MuiInputBase-root': {
borderRadius: { xs: '2px!important' },
},
'& .MuiOutlinedInput-root': {
'& fieldset.MuiOutlinedInput-notchedOutline': {
borderColor: { xs: 'rgb(208, 213, 231)' },
transition: 'border-color 0.25s ease 0s',
},
'&:hover': {
'& fieldset.MuiOutlinedInput-notchedOutline': {
borderColor: { xs: 'rgb(208, 213, 231)' },
},
},
'&.Mui-focused': {
'& fieldset.MuiOutlinedInput-notchedOutline': {
borderColor: { xs: 'rgb(90, 92, 236)' },
},
},
},
'& .MuiInputLabel-root.Mui-focused': {
color: { xs: 'rgb(90, 92, 236)' },
},
}}
>
{options.map((o) => (
<option key={o.id} value={o.id}>
{o.label}
</option>
))}
</TextField>
)
}
103 changes: 103 additions & 0 deletions packages/hygraph-dynamic-rows-ui/components/Setup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useApp, Wrapper } from '@hygraph/app-sdk-react'
import styles from './setup.module.css'
import { useState } from 'react'

function Install() {
// @ts-ignore - outdated types from @hygraph/app-sdk-react
const { updateInstallation, installation, showToast, extension } = useApp()
const installed = installation.status === 'COMPLETED'
const [gqlUri, setGqlUri] = useState('')

const saveOnClick = () => {
updateInstallation({
config: { backend: gqlUri },
status: 'COMPLETED',
}).then(() =>
showToast({
title: 'New GraphQL URI saved',
description: `${gqlUri} is now the GraphQL URI for this application.}`,
duration: 5000,
isClosable: true,
position: 'top-left',
variantColor: 'success',
}).catch((err) => console.log(err)),
)
}

const changedUri = extension.config.backend !== gqlUri

const installOnClick = () =>
updateInstallation({
config: { backend: gqlUri },
status: 'COMPLETED',
}).then(() =>
showToast({
title: 'Application enabled',
description: 'You can now use the Dynamic Row Property Selector field in your schema.',
duration: 5000,
isClosable: true,
position: 'top-left',
variantColor: 'success',
}).catch((err) => console.log(err)),
)

const uninstallOnClick = async () => {
updateInstallation({
config: {},
status: 'DISABLED',
})
.then(() => {
showToast({
title: 'Application disabled',
description: 'You can re-enable the application from the application configuration page.',
duration: 5000,
isClosable: true,
position: 'top-left',
variantColor: 'success',
})
})
.catch((error) => {
console.error('Error updating installation', error)
})

return 0
}

return (
<>
<>
<span>GraphQL API URI</span>
<input
name='gql-uri'
defaultValue={extension.config.backend}
onChange={(e) => setGqlUri(e.target.value)}
/>
</>

<button
type='button'
className={styles.button}
onClick={changedUri ? saveOnClick : installed ? uninstallOnClick : installOnClick}
>
{changedUri ? 'Save' : installed ? 'Disable app' : 'Enable app'}
</button>
</>
)
}

export function Page() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Dynamic Rows Property Selector</h1>
<p className={styles.description}>
Enhance your content management experience with Dynamic Rows, specifically designed to
integrate seamlessly with our Dynamic Row module. It features an intuitive property picker
field, allowing for effortless selection and organization of properties to customize your
content layout. Press install to get started!
</p>
<Wrapper>
<Install />
</Wrapper>
</div>
)
}
1 change: 1 addition & 0 deletions packages/hygraph-dynamic-rows-ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PropertyPicker'
58 changes: 58 additions & 0 deletions packages/hygraph-dynamic-rows-ui/components/setup.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
max-width: 1200px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.title {
font-size: 24px;
font-weight: 600;
line-height: 32px;
margin-bottom: 16px;
}

.desciption {
font-size: 14px;
font-weight: 300;
line-height: 20px;
margin-bottom: 16px;
}

.input {
display: inline;
}

.button {
user-select: none;
box-sizing: border-box;
appearance: none;
position: relative;
display: inline-flex;
-webkit-box-align: center;
align-items: center;
text-align: center;
vertical-align: middle;
align-self: center;
text-decoration: none;
font-weight: 500;
border: 0px;
margin: 16px 0px 0px;
border-radius: 4px;
font-size: 12px;
line-height: 16px;
height: 24px;
min-width: 24px;
padding-left: 8px;
padding-right: 8px;
color: rgb(255, 255, 255);
background-color: rgb(90, 92, 236);
}

.button:hover {
cursor: pointer;
background-color: rgb(58, 48, 166);
}
2 changes: 2 additions & 0 deletions packages/hygraph-dynamic-rows-ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './components/Setup'
export * from './components'
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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
}
Loading

0 comments on commit 320364b

Please sign in to comment.