#forms snippets

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>
...