How to duplicate/clone a record in Strapi?

Viewed 4458

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?

2 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

this feature doesn't exist in the admin dashboard.

If you want to implement it by your self, it's possible. You will have to customize the admin panel.

You will have to use the customization concept - https://strapi.io/documentation/3.0.0-beta.x/concepts/customization.html#plugin-extensions

You will have to update the Edit View of the Content Manager plugin - https://github.com/strapi/strapi/tree/master/packages/strapi-plugin-content-manager/admin/src/containers/EditView

Learn how works this container to add a button near from the cancel button for example.

You will also have to create a new route and controller function to find the entry you want to create and create a new one based on the info you get.

Related