#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?: booleaninitialOrder?: DataGridOrderDirectionformat?: (value: string | null) => ReactNodeinitialFilter?: UuidFilterArtifacts}export type UuidFilterArtifacts = {mode: 'matchesExactly'query: stringnullCondition: boolean}export type GenericUuidCellFilterArtifacts = {mode: 'matchesExactly'query: string}export const GenericUuidCellFilter = <Filter extends GenericUuidCellFilterArtifacts>({ filter, setFilter }: {filter: FiltersetFilter: (filter: Filter) => void}) => {const [value, setValue] = React.useState(filter.query)return (<><Stack gap={'small'}><TextInputnotNullvalue={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).valuetarget.getField(marker.fieldName).updateValue(sourceValue)}}export interface CopyEntityArgs {copier: EntityCopiersource: EntityAccessortarget: EntityAccessor}export interface CopyHasOneRelationArgs {copier: EntityCopiersource: EntityAccessortarget: EntityAccessormarker: HasOneRelationMarker}export interface CopyHasManyRelationArgs {copier: EntityCopiersource: EntityAccessortarget: EntityAccessormarker: HasManyRelationMarker}export interface CopyColumnArgs {copier: EntityCopiersource: EntityAccessortarget: EntityAccessormarker: FieldMarker}export interface CopyHandler {copy?(args: CopyEntityArgs): booleancopyHasOneRelation?(args: CopyHasOneRelationArgs): booleancopyHasManyRelation?(args: CopyHasManyRelationArgs): booleancopyColumn?(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).valueif (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 = falseconst 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 = (<CreatePageentity="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: ReactNodefield: string | SugarableRelativeSingleFieldimmediatePersist?: boolean} & ButtonPropsexport 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: stringwhere: SugaredUniqueWhere | undefinedfield: stringchildren?: 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)'} />