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