Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to duplicate/clone a record in Strapi?

Tags:

strapi

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?

like image 711
Nick Avatar asked Oct 12 '25 09:10

Nick


1 Answers

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
like image 72
Grenny Bown Avatar answered Oct 16 '25 06:10

Grenny Bown



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!