#binding snippets

UUID cell

UuidCell.tsx

import { FunctionComponent, ReactNode, useCallback } from 'react'
import {
Component,
DataGridColumn,
DataGridColumnPublicProps,
DataGridOrderDirection,
Field,
FieldFallbackView,
FieldFallbackViewPublicProps,
QueryLanguage,
Stack,
SugaredRelativeSingleField,
Text,
wrapFilterInHasOnes,
} from '@contember/admin'
import { TextInput } from '@contember/ui'
import { Input } from '@contember/client'
function isUuid(value: string) {
return !!value.match(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/)
}
export type UuidCellProps =
& DataGridColumnPublicProps
& FieldFallbackViewPublicProps
& SugaredRelativeSingleField
& {
disableOrder?: boolean
initialOrder?: DataGridOrderDirection
format?: (value: string | null) => ReactNode
initialFilter?: UuidFilterArtifacts
}
export type UuidFilterArtifacts = {
mode: 'matchesExactly'
query: string
nullCondition: boolean
}
export type GenericUuidCellFilterArtifacts = {
mode: 'matchesExactly'
query: string
}
export const GenericUuidCellFilter = <Filter extends GenericUuidCellFilterArtifacts>({ filter, setFilter }: {
filter: Filter
setFilter: (filter: Filter) => void
}) => {
const [value, setValue] = React.useState(filter.query)
return (
<>
<Stack gap={'small'}>
<TextInput
notNull
value={filter.query}
style={{ minWidth: '350px' }}
validationState={value && !isUuid(value) ? 'invalid' : 'valid'}
placeholder={'UUID'}
onChange={useCallback((currentValue?: string | null) => {
if (currentValue === null || currentValue === undefined) {
throw new Error('should not happen')
}
if (currentValue && isUuid(currentValue)) {
setFilter({
...filter,
query: currentValue,
})
}
setValue(currentValue)
}, [filter, setFilter, setValue])}
/>
{(value && !isUuid(value)) && (
<Text>This is not valid UUID</Text>
)}
</Stack>
</>
)
}
export const createGenericUserCellFilterCondition = (filter: GenericUuidCellFilterArtifacts) => {
const baseOperators = {
matchesExactly: 'eq',
}
let condition: Input.Condition<string> = {
[baseOperators[filter.mode]]: filter.query,
}
return condition
}
export const UuidCell: FunctionComponent<UuidCellProps> = Component(props => {
return (
<DataGridColumn<UuidFilterArtifacts>
{...props}
enableOrdering={!props.disableOrder as true}
getNewOrderBy={(newDirection, { environment }) =>
newDirection ? QueryLanguage.desugarOrderBy(`${props.field as string} ${newDirection}`, environment) : undefined
}
getNewFilter={(filter, { environment }) => {
if (filter.query === '' && filter.nullCondition === false) {
return undefined
}
let condition = filter.query !== '' ? createGenericUserCellFilterCondition(filter) : {}
if (filter.nullCondition) {
condition = {
or: [condition, { isNull: true }],
}
}
const desugared = QueryLanguage.desugarRelativeSingleField(props, environment)
return wrapFilterInHasOnes(desugared.hasOneRelationPath, {
[desugared.field]: condition,
})
}}
emptyFilter={{
mode: 'matchesExactly',
query: '',
nullCondition: false,
}}
filterRenderer={({ filter, setFilter, ...props }) => {
return (
<Stack horizontal align="center">
<GenericUuidCellFilter {...props} filter={filter} setFilter={setFilter} />
</Stack>
)
}}
>
<Field<string>
{...props}
format={value => {
if (value === null) {
return <FieldFallbackView fallback={props.fallback} fallbackStyle={props.fallbackStyle} />
}
if (props.format) {
return props.format(value as any)
}
return value
}}
/>
</DataGridColumn>
)
}, 'UuidCell')

Copying content of entities

Often you want to copy some data from another entity. In this guide we will create a copier that copies the content of Page entities.

utils/EntityCopier.ts

The main class for copying entities.
import { EntityAccessor, FieldMarker, HasManyRelationMarker, HasOneRelationMarker, PRIMARY_KEY_NAME } from '@contember/admin'
export class EntityCopier {
constructor(private handlers: CopyHandler[] = []) {
}
copy(source: EntityAccessor, target: EntityAccessor) {
for (const handler of this.handlers) {
if (handler.copy?.({ copier: this, source, target })) {
return
}
}
for (const [, marker] of source.getMarker().fields.markers) {
if (marker instanceof FieldMarker) {
this.copyColumn(source, target, marker)
} else if (marker instanceof HasOneRelationMarker) {
this.copyHasOneRelation(source, target, marker)
} else if (marker instanceof HasManyRelationMarker) {
this.copyHasManyRelation(source, target, marker)
}
}
}
public copyHasOneRelation(source: EntityAccessor, target: EntityAccessor, marker: HasOneRelationMarker) {
for (const handler of this.handlers) {
if (handler.copyHasOneRelation?.({ copier: this, source, target, marker })) {
return
}
}
const subEntity = source.getEntity({ field: marker.parameters })
if (marker.parameters.expectedMutation === 'connectOrDisconnect') {
if (!subEntity.existsOnServer && subEntity.hasUnpersistedChanges) {
throw 'cannot copy'
}
target.connectEntityAtField(marker.parameters.field, subEntity)
} else if (
marker.parameters.expectedMutation === 'anyMutation' ||
marker.parameters.expectedMutation === 'createOrDelete'
) {
const subTarget = target.getEntity({ field: marker.parameters })
this.copy(subEntity, subTarget)
}
}
public copyHasManyRelation(source: EntityAccessor, target: EntityAccessor, marker: HasManyRelationMarker) {
for (const handler of this.handlers) {
if (handler.copyHasManyRelation?.({ copier: this, source, target, marker })) {
return
}
}
const list = source.getEntityList(marker.parameters)
const targetList = target.getEntityList(marker.parameters)
if (marker.parameters.expectedMutation === 'connectOrDisconnect') {
for (const subEntity of list) {
if (!subEntity.existsOnServer && subEntity.hasUnpersistedChanges) {
throw 'cannot copy'
}
targetList.connectEntity(subEntity)
}
} else if (
marker.parameters.expectedMutation === 'anyMutation' ||
marker.parameters.expectedMutation === 'createOrDelete'
) {
targetList.disconnectAll()
for (const subEntity of list) {
targetList.createNewEntity(target => {
this.copy(subEntity, target())
})
}
}
}
public copyColumn(source: EntityAccessor, target: EntityAccessor, marker: FieldMarker) {
if (marker.fieldName === PRIMARY_KEY_NAME) {
return
}
for (const handler of this.handlers) {
if (handler.copyColumn?.({ copier: this, source, target, marker })) {
return
}
}
const sourceValue = source.getField(marker.fieldName).value
target.getField(marker.fieldName).updateValue(sourceValue)
}
}
export interface CopyEntityArgs {
copier: EntityCopier
source: EntityAccessor
target: EntityAccessor
}
export interface CopyHasOneRelationArgs {
copier: EntityCopier
source: EntityAccessor
target: EntityAccessor
marker: HasOneRelationMarker
}
export interface CopyHasManyRelationArgs {
copier: EntityCopier
source: EntityAccessor
target: EntityAccessor
marker: HasManyRelationMarker
}
export interface CopyColumnArgs {
copier: EntityCopier
source: EntityAccessor
target: EntityAccessor
marker: FieldMarker
}
export interface CopyHandler {
copy?(args: CopyEntityArgs): boolean
copyHasOneRelation?(args: CopyHasOneRelationArgs): boolean
copyHasManyRelation?(args: CopyHasManyRelationArgs): boolean
copyColumn?(args: CopyColumnArgs): boolean
}

utils/ContentCopyHandler.ts

This "plugin" regenerates IDs of references.
import { CopyColumnArgs, CopyHandler, CopyHasManyRelationArgs } from './EntityCopier'
import { generateUuid, PRIMARY_KEY_NAME } from '@contember/admin'
export class ContentCopyHandler implements CopyHandler {
constructor(private blockEntity: string, private contentField: string, private referencesField: string) {
}
copyColumn({ source, marker }: CopyColumnArgs): boolean {
return source.name === this.blockEntity && marker.fieldName === this.contentField
}
copyHasManyRelation({ source, target, copier, marker }: CopyHasManyRelationArgs): boolean {
if (source.name !== this.blockEntity || marker.parameters.field !== this.referencesField) {
return false
}
const list = source.getEntityList(marker.parameters)
const targetList = target.getEntityList(marker.parameters)
targetList.disconnectAll()
const referenceMapping = new Map<string, string>()
for (const subEntity of list) {
const newId = generateUuid()
referenceMapping.set(subEntity.id as string, newId)
targetList.createNewEntity(target => {
copier.copy(subEntity, target())
target().getField(PRIMARY_KEY_NAME).updateValue(newId)
})
}
const jsonRaw = source.getField(this.contentField).value
if (typeof jsonRaw !== 'string') {
return true
}
const jsonValue = JSON.parse(jsonRaw, (key, value) => {
if (key === 'referenceId') {
return referenceMapping.get(value)
}
return value
})
target.getField(this.contentField).updateValue(JSON.stringify(jsonValue))
return true
}
}

components/CopyHandler.ts

Helper component, which loads the entity, we are copying from, and loads it into a current entity.
import { Component, EntitySubTree, useEntity, useEntitySubTree } from '@contember/admin'
import React, { ReactNode, useEffect, useRef } from 'react'
import { EntityCopier } from './EntityCopier'
import { ContentCopyHandler } from './ContentCopyHandler'
type CopyHandlerProps = { entityName: string; children: ReactNode; parameterName: string }
export const CopyHandler = Component<CopyHandlerProps>((props, environment) => {
const id = environment.getParameterOrElse(props.parameterName, null)
if (!id) {
return null
}
return <CopyHandlerInner {...props} id={id} />
})
export const CopyHandlerInner = Component<CopyHandlerProps & { id: string | number }>(() => {
const entity = useEntity()
const sourceEntity = useEntitySubTree('source')
const firstRender = useRef(true)
useEffect(() => {
if (!firstRender.current) {
return
}
firstRender.current = false
const copier = new EntityCopier([
new ContentCopyHandler('ContentBlock', 'json', 'references'), // configure the content to match your schema
])
copier.copy(sourceEntity, entity)
}, [entity, sourceEntity])
return null
}, (props) => {
return (
<EntitySubTree entity={{
entityName: props.entityName,
where: { id: props.id },
}} alias={'source'}>
{props.children}
</EntitySubTree>
)
})

pages/article.ts

Usage on some page.
import { Component, CreatePage, EditPage, LinkButton } from '@contember/admin'
import React from 'react'
import { CopyHandler } from './CopyHandler'
const ArticleForm = Component(() => {
return <>
{/*form fields*/}
</>
})
export const create = (
<CreatePage
entity="Article"
rendererProps={{
title: 'Create new article',
}}
redirectOnSuccess={'article/edit(id: $entity.id)'}
>
<ArticleForm />
<CopyHandler entityName={'Article'} parameterName={'copyFrom'}>
<ArticleForm />
</CopyHandler>
</CreatePage>
)
export const edit = (
<EditPage entity="Article(id=$id)" rendererProps={{ title: 'Edit article' }}>
<ArticleForm />
<LinkButton to={'article/create(copyFrom: $entity.id)'}>Create copy</LinkButton>
</EditPage>
)

Button that sets some field null

For example you have a datetime field when you want to contact someone. And want to mark it as done. Easiest way is to just set this field to null. So here's a code to do just that.

SetNullButton.tsx

Put into /components
import { ReactNode } from 'react'
import { Button, ButtonProps, SugarableRelativeSingleField, useField, usePersistWithFeedback } from '@contember/admin'
export type SetNullButtonProps = {
children: ReactNode
field: string | SugarableRelativeSingleField
immediatePersist?: boolean
} & ButtonProps
export const SetNullButton = ({ children, field, immediatePersist, ...buttonProps }: SetNullButtonProps) => {
const fieldAccessor = useField(field)
const triggerPersist = usePersistWithFeedback()
if (fieldAccessor.value === null) {
return null
}
return <>
<Button {...buttonProps} onClick={e => {
e.preventDefault()
e.stopPropagation()
fieldAccessor.getAccessor().updateValue(null)
if (immediatePersist) {
triggerPersist().catch(() => { })
}
}}>
{children}
</Button>
</>
}

AnyFile.tsx

Usage of the component above
...
<SetNullButton field="nextContactDate" immediatePersist>
Mark done
</SetNullButton>
...

Connect entity

This component allows you to connect an entity by a using a unique identifier, such as from a URL. It can be used in conjunction with SelectField, as SelectField does not have the capability to select default values.

ConnectEntity.tsx

During the static render, the component retrieves the entity you wish to connect. Subsequently, in the useEffect, the said entity gets linked to the specified field.
export interface ConnectEntityProps {
entity: string
where: SugaredUniqueWhere | undefined
field: string
children?: ReactNode
}
export const ConnectEntity = Component<ConnectEntityProps>(({ entity, where, field }) => {
const currentEntity = useEntity()
const getSubtree = useGetEntitySubTree()
useEffect(() => {
if (!where || currentEntity.getEntity(field).existsOnServer) {
return
}
const entityToConnect = getSubtree({
entity: {
entityName: entity,
where,
},
})
if (entityToConnect) {
currentEntity.connectEntityAtField(field, entityToConnect)
}
}, [entity, field, currentEntity, where, getSubtree])
return null
}, ({ entity, where, children, field }) => {
if (!where) {
return null
}
return <>
<HasOne field={field} />
<EntitySubTree entity={{ entityName: entity, where }} children={children} />
</>
})

Usage.tsx

The usage is straightforward. You simply need to specify the field where you want to connect the entity and provide the unique identifier of the entity.
<ConnectEntity field={'site'} entity={'Site'} where={'(code = $site)'} />