-
Notifications
You must be signed in to change notification settings - Fork 73
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2100 from graphcommerce-org/feature/GCOM-1108
Feature/GCOM-1108 | Property picker for Dynamic Rows (V2)
- Loading branch information
Showing
20 changed files
with
1,656 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
124
packages/hygraph-dynamic-rows-ui/components/PropertyPicker.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './PropertyPicker' |
58 changes: 58 additions & 0 deletions
58
packages/hygraph-dynamic-rows-ui/components/setup.module.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './components/Setup' | ||
export * from './components' |
42 changes: 42 additions & 0 deletions
42
packages/hygraph-dynamic-rows-ui/lib/createOptionsFromInterfaceObject.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.