I would like to start from a clone of another record in my database, without having to fill in all of those fields again. Currently this is very tedious, having to open two tabs (one with the existing record, and another with new record) and copy-pasting data.
How can I clone/duplicate a record in Strapi?
You need to edit this files following editing extensions guide
├───admin
│ └───src
│ ├───containers
│ │ ├───EditView
│ │ │ Header.js
│ │ └───EditViewDataManagerProvider
│ │ │ index.js
│ │ └───utils
│ │ cleanData.js
│ │ index.js
│ └───translations
│ en.json
├───config
│ routes.json
├───controllers
│ ContentManager.js
└───services
ContentManager.js
Header.js
if (!isCreatingEntry) {
headerActions.unshift(
{
label: formatMessage({
id: `${pluginId}.containers.Edit.clone`,
}),
color: 'primary',
onClick: (e) => {
handleClone(e);
},
type: 'button',
style: {
paddingLeft: 15,
paddingRight: 15,
fontWeight: 600,
},
},
{
label: formatMessage({
id: 'app.utils.delete',
}),
color: 'delete',
onClick: () => {
toggleWarningDelete();
},
type: 'button',
style: {
paddingLeft: 15,
paddingRight: 15,
fontWeight: 600,
},
},
);
}
and pass handleClone
here
const {
deleteSuccess,
initialData,
layout,
redirectToPreviousPage,
resetData,
handleClone,
setIsSubmitting,
slug,
clearData,
} = useDataManager();
EditViewDataManagerProvider/index.js
const handleClone = async (event) => {
event.preventDefault();
// Create yup schema
const schema = createYupSchema(
currentContentTypeLayout,
{
components: get(allLayoutData, 'components', {}),
},
true
);
try {
// Validate the form using yup
await schema.validate(modifiedData, { abortEarly: false });
// Set the loading state in the plugin header
const filesToUpload = getFilesToUpload(modifiedData);
// Remove keys that are not needed
// Clean relations
const cleanedData = cleanData(
cloneDeep(modifiedData),
currentContentTypeLayout,
allLayoutData.components,
true
);
const formData = new FormData();
formData.append('data', JSON.stringify(cleanedData));
Object.keys(filesToUpload).forEach((key) => {
const files = filesToUpload[key];
files.forEach((file) => {
formData.append(`files.${key}`, file);
});
});
// Change the request helper default headers so we can pass a FormData
const headers = {};
const method = 'POST';
const endPoint = `${slug}/clone/${modifiedData.id}`;
emitEvent(isCreatingEntry ? 'willCloneEntry' : 'willCloneEntry');
try {
// Time to actually send the data
await request(
getRequestUrl(endPoint),
{
method,
headers,
body: formData,
signal,
},
false,
false
);
emitEvent(isCreatingEntry ? 'didCloneEntry' : 'didCloneEntry');
dispatch({
type: 'CLONE_SUCCESS',
});
strapi.notification.success(`${pluginId}.success.record.clone`);
// strapi.notification.success('Entry cloned!');
redirectToPreviousPage();
} catch (err) {
console.error({ err });
const error = get(
err,
['response', 'payload', 'message', '0', 'messages', '0', 'id'],
'SERVER ERROR'
);
setIsSubmitting(false);
emitEvent(isCreatingEntry ? 'didNotCloneEntry' : 'didNotCloneEntry', {
error: err,
});
strapi.notification.error(error);
}
} catch (err) {
const errors = getYupInnerErrors(err);
console.error({ err, errors });
dispatch({
type: 'CLONE_ERRORS',
errors,
});
}
};
and provide handleClone
to EditViewDataManagerContext.Provider.value
EditViewDataManagerProvider/utils/cleanData.js
const cleanData = (retrievedData, currentSchema, componentsSchema, clone = false) => {
const getType = (schema, attrName) =>
get(schema, ['attributes', attrName, 'type'], '');
const getSchema = (schema, attrName) =>
get(schema, ['attributes', attrName], '');
const getOtherInfos = (schema, arr) =>
get(schema, ['attributes', ...arr], '');
const recursiveCleanData = (data, schema) => Object.keys(data).reduce((acc, current) => {
const attrType = getType(schema.schema, current);
const valueSchema = getSchema(schema.schema, current);
const value = get(data, current);
const component = getOtherInfos(schema.schema, [current, 'component']);
const isRepeatable = getOtherInfos(schema.schema, [
current,
'repeatable',
]);
let cleanedData;
switch (attrType) {
case 'string': {
if (clone && valueSchema.unique) {
cleanedData = `${value}_clone`;
} else {
cleanedData = value;
}
break;
}
case 'json':
try {
cleanedData = JSON.parse(value);
} catch (err) {
cleanedData = value;
}
break;
case 'date':
case 'datetime':
cleanedData =
value && value._isAMomentObject === true
? value.toISOString()
: value;
break;
case 'media':
if (getOtherInfos(schema.schema, [current, 'multiple']) === true) {
cleanedData = value
? helperCleanData(
value.filter(file => !(file instanceof File)),
'id'
)
: null;
} else {
cleanedData =
get(value, 0) instanceof File ? null : get(value, 'id', null);
}
break;
case 'component':
if (isRepeatable) {
cleanedData = value
? value.map((data) => {
const subCleanedData = recursiveCleanData(
data,
componentsSchema[component]
);
return subCleanedData;
})
: value;
} else {
cleanedData = value
? recursiveCleanData(value, componentsSchema[component])
: value;
}
break;
case 'dynamiczone':
cleanedData = value.map((componentData) => {
const subCleanedData = recursiveCleanData(
componentData,
componentsSchema[componentData.__component]
);
return subCleanedData;
});
break;
default:
cleanedData = helperCleanData(value, 'id');
}
acc[current] = cleanedData;
if (clone && (current === '_id' || current === 'id')) {
acc[current] = undefined;
}
return acc;
}, {});
return recursiveCleanData(retrievedData, currentSchema);
};
just copy EditViewDataManagerProvider/utils/index.js
add in config/routes.json
{
"method": "POST",
"path": "/explorer/:model/clone/:id",
"handler": "ContentManager.clone",
"config": {
"policies": ["routing"]
}
}
add similar to create clone method in controllers/ContentManager.js
async clone(ctx) {
const contentManagerService = strapi.plugins['content-manager'].services.contentmanager;
const { model } = ctx.params;
try {
if (ctx.is('multipart')) {
const { data, files } = parseMultipartBody(ctx);
ctx.body = await contentManagerService.clone(data, { files, model });
} else {
// Create an entry using `queries` system
ctx.body = await contentManagerService.clone(ctx.request.body, { model });
}
// await strapi.telemetry.send('didCreateFirstContentTypeEntry', { model });
} catch (error) {
strapi.log.error(error);
ctx.badRequest(null, [
{
messages: [{ id: error.message, message: error.message, field: error.field }],
errors: _.get(error, 'data.errors'),
},
]);
}
},
and finally services/ContentManager.js
clone(data, { files, model } = {}) {
return strapi.entityService.create({ data, files }, { model });
},
Don't forget to update translations in translations/:lang.json
"containers.Edit.clone": "clone button",
"success.record.clone": "notification"
Don't forget to rebuild your admin UI
npm run build
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With