It’s really common that you’ll want to extend the Admin UI with custom components. Common use cases for this functionality are:
- You want to show a built-in datatype in a unique way, for example this string input might actually be a tag array that’s separated by commas.
- You want to show icons / otherwise visually design what users are seeing in the Admin area.
- You have a complex JSON field in your database and want to present a full-featured UI to the users to edit it instead of a text box, for example it might represent the schedule for repeating on a meeting invite or similar, so you want to show that UI to users.
All of this is possible (and easy!) in Graphweaver. In fact if you want to inject whole pages into the admin area, you can learn more about that in .
By default, Graphweaver looks for custom fields in [your project]/admin-ui/custom-fields
. If it finds an export called customFields
after importing that file / folder, then it’ll build your custom fields into the admin UI and start using them.
Example
Here’s an example of a custom field from our REST example project:
import { CustomField } from '@exogee/graphweaver-admin-ui-components';
import { Link } from './link';
export const customFields = new Map<string, CustomField[]>();
customFields.set('Task', [
{
name: 'search',
type: 'custom',
index: 3,
component: Link,
hideOnDetailForm: true,
},
]);
This tells Graphweaver that:
- There’s a custom field we want to add to the
Task
entity. - The field is called
'search'
and should be displayed at index 3 - By default this field is shown both in the table and on the detail form, but we only want this to show up on the table.
- When rendering this, we should render the
Link
component, which looks like this:
import { MouseEventHandler } from 'react';
import { ReactComponent as OpenIcon } from '../assets/16-open-external.svg';
import { CustomFieldArgs } from '@exogee/graphweaver-admin-ui-components';
interface Task {
user: {
label: string;
value: string;
};
}
export const Link = ({ entity }: CustomFieldArgs<Task>) => {
const handleClick = (e: MouseEventHandler<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
window.open(`https://google.com/search?q=${entity.user.label}`, '_blank', 'noreferrer');
};
return (
<div style={{ cursor: 'pointer' }} onClick={handleClick}>
<OpenIcon />
</div>
);
};
This will render a link icon that when clicked, searches Google for the user that owns the task in a new window.
GQL in Custom Fields
It’s possible to include a GQL query in a custom component, but you may only see the first result if the query returns multiple results. This is an effect of Apollo cache configuration, but the cache configuration can be changed to avoid this or the id can be added to the fields returned.
This example uses Apollo client to execute a GQL query, and includes the id field in the response.
const UPLOADS_QUERY = gql`
query uploads($id: ID!) {
uploads(filter: { submission: { id: $id } }) {
id
url
}
}
`;
export const DownloadAttachments = (args: CustomFieldArgs<Submission>) => {
const id = args.entity.id;
const { loading, data, error } = Apollo.useQuery(UPLOADS_QUERY, { variables: { id } });
if (error) console.error(error);
if (loading) return <span>Loading...</span>;
const uploadList = data
? data?.uploads?.map((upload) => (
<a href={upload.url}> download</a>
))
: [];
return (
<>
<label htmlFor="attachments">attachments</label>
<div id="attachments" className={panelStyle}>
{uploadList}
</div>
</>
);
Data Passed to Custom Fields
Custom fields receive a single CustomFieldArgs<T>
argument with the following values:
Key | Type | Value |
entity | T | All data retrieved from the backend for the ‘row’ in the grid. If edits are made to the form, this is intentionally not updated, use Formik hooks to trace changes on the detail form |
context | 'detail-form' | 'table' | The context that this field is currently rendering in. You can conditionally render different things in each context as you see fit, or you can create separate fields, one that shows only on the table and the other that shows only on the detail form. |
Example usage:
export const MyCustomFieldComponent = ({ entity }: CustomFieldArgs<Task>) => {
// At this point `entity` will contain the row data of `Task` type
}
Overriding Default Fields
You can override default fields for entities by adding a custom field that has the same name as an existing field. Graphweaver will render your custom field instead of the in-built UI for that field in this case, and you can select whatever index you want for the field.
Labels
We don’t render labels for custom fields on the detail panel because we aren’t sure if you want one there or not. To render your own as part of the custom field, simply use the DetailPanelFieldLabel
component from the @exogee/graphweaver-admin-ui-components
package like so:
import { DetailPanelFieldLabel } from '@exogee/graphweaver-admin-ui-components';
// ...
<DetailPanelFieldLabel fieldName="myField" />
Manipulating Detail Panel Form Data
We use Formik in the Admin UI to show and manage the detail form. This means when your custom fields are rendered in the Detail Panel you can utilise the useFormikContext()
hook to read initial values vs current values, implement validation, and change the value of any other field on the form from your custom field as users edit the values.
Whatever Formik believes the final form values are when submitting is what’s sent to the backend, so you can manipulate this as much as you need.
Configuring Where Graphweaver Looks for Custom Fields
By default custom fields are located in [your project]/admin-ui/custom-fields
, but if you want to change the import path you can control this with the adminUI.customFieldsPath
configuration option in the graphweaver-config
file.