Skip to content

Commit

Permalink
Examples/custom field view (#6063)
Browse files Browse the repository at this point in the history
  • Loading branch information
alvarezmauro committed Jul 8, 2021
1 parent 7d1f6fc commit fd342e2
Show file tree
Hide file tree
Showing 14 changed files with 872 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/popular-cups-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-next/example-custom-field-view': major
---

Initial version of the custom-field-view example
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ jobs:
'task-manager.test.ts',
'testing.test.ts',
'with-auth.test.ts',
'custom-field-view.test.ts',
'custom-field.test.ts',
]
fail-fast: false
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Each project below demonstrates a Keystone feature you can learn about and exper
- [Document field](./document-field): Adds document fields to the Blog base.
- [Testing](./testing): Adds tests with `@keystone-next/testing` to the `withAuth()` example.
- [Custom field](./custom-field): Adds a custom `stars` field to the Blog base.
- [Custom field view](./custom-field-view): Adds a custom Admin UI view to a `json` field to the Task Manager base.


## Running examples

Expand Down
1 change: 1 addition & 0 deletions examples/custom-field-view/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @keystone-next/example-custom-field-view
63 changes: 63 additions & 0 deletions examples/custom-field-view/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## Feature Example - Custom Field View

This project demonstrates how to create a custom field view for a JSON field. This custom field view allows users to add, edit and remove navigation items from a list

## Instructions

To run this project, clone the Keystone repository locally then navigate to this directory and run:

```shell
yarn dev
```

This will start the Admin UI at [localhost:3000](http://localhost:3000).

You can use the Admin UI to create items in your database.

You can also access a GraphQL Playground at [localhost:3000/api/graphql](http://localhost:3000/api/graphql), which allows you to directly run GraphQL queries and mutations.

## Features

In this project we add a new JSON field to the `Task` list:

```typescript
relatedLinks: json({
ui: {
views: require.resolve('./fields/related-links/components.tsx'),
createView: { fieldMode: 'edit' },
listView: { fieldMode: 'hidden' },
itemView: { fieldMode: 'edit' },
},
}),
```

This field defines `ui.views` and provides a custom editor component which allows users to view and edit this field in a more intuitive way than just using raw JSON.

The stored JSON data:

```json
{
"data": {
"allTasks": [
{
"relatedLinks": [
{
"label": "Keystone website",
"href": "keystonejs.com"
},
{
"label": "Prisma website",
"href": "prisma.io"
}
]
}
]
}
}
```

will be rendered as.

<div align="center">
<img src="./custom-field-ui.png" width="445">
</div>
Binary file added examples/custom-field-view/custom-field-ui.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
193 changes: 193 additions & 0 deletions examples/custom-field-view/fields/related-links/components.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import React from 'react';
import { FieldProps } from '@keystone-next/types';
import { css } from '@emotion/css';
import { Button } from '@keystone-ui/button';
import { FieldContainer, FieldLabel, TextInput } from '@keystone-ui/fields';
import { MinusCircleIcon, EditIcon } from '@keystone-ui/icons';
import { controller } from '@keystone-next/fields/types/json/views';
import { Fragment, useState } from 'react';

interface RelatedLink {
label: string;
href: string;
}

const styles = {
form: {
field: css`
display: flex;
flex-wrap: nowrap;
align-items: center;
width: 100%;
margin: 1rem 0 0 0;
`,
label: css`
width: 10%;
`,
input: css`
width: 90%;
`,
button: css`
margin: 1rem 0.5rem 0 0;
`,
},
list: {
ul: css`
list-style: none;
margin: 1rem 0 0 0;
padding: 0;
`,
li: css`
display: flex;
align-items: center;
flex-wrap: nowrap;
width: 100%;
&:nth-of-type(2n) > div:nth-of-type(1) {
background-color: white;
}
`,
data: css`
background-color: #eff3f6;
padding: 0.5rem;
flex: auto;
display: flex;
align-items: flex-start;
flex-wrap: nowrap;
`,
dataLabel: css`
width: 40%;
`,
dataHref: css`
width: 60%;
`,
optionButton: css`
margin: 0 0 0 0.5rem;
`,
},
};

export const Field = ({ field, value, onChange, autoFocus }: FieldProps<typeof controller>) => {
const [labelValue, setLabelValue] = useState('');
const [hrefValue, setHrefValue] = useState('');
const [index, setIndex] = useState<number | null>(null);

const relatedLinks: RelatedLink[] = value ? JSON.parse(value) : [];

const onSubmitNewRelatedLink = () => {
if (onChange) {
const relatedLinksCopy = [...relatedLinks, { label: labelValue, href: hrefValue }];
onChange(JSON.stringify(relatedLinksCopy));
onCancelRelatedLink();
}
};

const onDeleteRelatedLink = (index: number) => {
if (onChange) {
const relatedLinksCopy = [...relatedLinks];
relatedLinksCopy.splice(index, 1);
onChange(JSON.stringify(relatedLinksCopy));
onCancelRelatedLink();
}
};

const onEditRelatedLink = (index: number) => {
if (onChange) {
setIndex(index);
setLabelValue(relatedLinks[index].label);
setHrefValue(relatedLinks[index].href);
}
};

const onUpdateRelatedLink = () => {
if (onChange && index !== null) {
const relatedLinksCopy = [...relatedLinks];
relatedLinksCopy[index] = { label: labelValue, href: hrefValue };
onChange(JSON.stringify(relatedLinksCopy));
onCancelRelatedLink();
}
};

const onCancelRelatedLink = () => {
setIndex(null);
setLabelValue('');
setHrefValue('');
};

return (
<FieldContainer>
<FieldLabel>{field.label}</FieldLabel>
{onChange && (
<Fragment>
<div className={styles.form.field}>
<FieldLabel className={styles.form.label}>Label</FieldLabel>
<TextInput
autoFocus={autoFocus}
onChange={event => setLabelValue(event.target.value)}
value={labelValue}
className={styles.form.input}
/>
</div>
<div className={styles.form.field}>
<FieldLabel className={styles.form.label}>Href</FieldLabel>
<TextInput
autoFocus={autoFocus}
onChange={event => setHrefValue(event.target.value)}
value={hrefValue}
className={styles.form.input}
/>
</div>

{index !== null ? (
<Fragment>
<Button onClick={onUpdateRelatedLink} className={styles.form.button}>
Update
</Button>
<Button onClick={onCancelRelatedLink} className={styles.form.button}>
Cancel
</Button>
</Fragment>
) : (
<Button onClick={onSubmitNewRelatedLink} className={styles.form.button}>
Add
</Button>
)}
</Fragment>
)}
<ul className={styles.list.ul}>
{relatedLinks.map((relatedLink: RelatedLink, i: number) => {
return (
<li key={`related-link-${i}`} className={styles.list.li}>
<div className={styles.list.data}>
<div className={styles.list.dataLabel}>{relatedLink.label}</div>
<div className={styles.list.dataHref}>
<a href={relatedLink.href} target="_blank">
{relatedLink.href}
</a>
</div>
</div>
{onChange && (
<div>
<Button
size="small"
onClick={() => onEditRelatedLink(i)}
className={styles.list.optionButton}
>
<EditIcon size="small" color="blue" />
</Button>
<Button size="small" className={styles.list.optionButton}>
<MinusCircleIcon
size="small"
color="red"
onClick={() => onDeleteRelatedLink(i)}
/>
</Button>
</div>
)}
</li>
);
})}
</ul>
</FieldContainer>
);
};
10 changes: 10 additions & 0 deletions examples/custom-field-view/keystone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { config } from '@keystone-next/keystone/schema';
import { lists } from './schema';

export default config({
db: {
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
},
lists,
});
29 changes: 29 additions & 0 deletions examples/custom-field-view/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@keystone-next/example-custom-field-view",
"version": "0.0.0",
"private": true,
"license": "MIT",
"scripts": {
"dev": "keystone-next dev",
"start": "keystone-next start",
"build": "keystone-next build"
},
"dependencies": {
"@emotion/css": "^11.1.3",
"@keystone-next/fields": "^11.0.2",
"@keystone-next/keystone": "^21.0.0",
"@keystone-next/types": "^21.0.1",
"@keystone-ui/button": "^5.0.0",
"@keystone-ui/core": "^3.1.0",
"@keystone-ui/fields": "^4.1.2",
"@keystone-ui/icons": "^4.0.0",
"react": "^17.0.2"
},
"devDependencies": {
"typescript": "^4.3.5"
},
"engines": {
"node": "^12.20 || >= 14.13"
},
"repository": "https://github.com/keystonejs/keystone/tree/master/examples/custom-field-view"
}

1 comment on commit fd342e2

@vercel
Copy link

@vercel vercel bot commented on fd342e2 Jul 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.