Cómo copiar macros en otras hojas de cálculo

Nivel de programación: Intermedio
Duración: 30 minutos
Tipo de proyecto: Complemento de Google Workspace

Objetivos

  • Comprender qué hace la solución
  • Comprender qué hacen los servicios de Apps Script dentro de la solución
  • Configura el entorno.
  • Configura la secuencia de comandos.
  • Ejecuta la secuencia de comandos.

Acerca de esta solución

Copiar macros de Hojas de cálculo de Google de forma manual de una hoja de cálculo a otra puede ser lento y propenso a errores. Este complemento de Google Workspace copia automáticamente un proyecto de secuencia de comandos y lo adjunta a una hoja de cálculo especificada por el usuario. Si bien esta solución se enfoca en las macros de Hojas de cálculo, puedes usarla para copiar y compartir cualquier secuencia de comandos vinculada a un contenedor.

Captura de pantalla del complemento de Google Workspace Share Macro

Cómo funciona

La secuencia de comandos copia el proyecto de Apps Script vinculado a la hoja de cálculo original y crea un proyecto de Apps Script duplicado vinculado a la hoja de cálculo especificada por el usuario.

Servicios de Apps Script

En esta solución, se usan los siguientes servicios:

Requisitos previos

Para usar esta muestra, debes cumplir con los siguientes requisitos previos:

Configura tu entorno

Abre tu proyecto de Cloud en la consola de Google Cloud

Si aún no está abierto, abre el proyecto de Cloud que deseas usar para este ejemplo:

  1. En la consola de Google Cloud, ve a la página Seleccionar un proyecto.

    Selecciona un proyecto de Cloud

  2. Selecciona el proyecto de Google Cloud que deseas usar. También puedes hacer clic en Create project y seguir las instrucciones en pantalla. Si creas un proyecto de Google Cloud, es posible que debas activar la facturación del proyecto.

Activa la API de Google Apps Script

En esta guía de inicio rápido, se usa la API de Google Apps Script.

Antes de usar las APIs de Google, debes activarlas en un proyecto de Google Cloud. Puedes activar una o más APIs en un solo proyecto de Google Cloud.
  • En tu proyecto de Cloud, activa la API de Google Apps Script.

    Activa la API

Los complementos de Google Workspace requieren la configuración de una pantalla de consentimiento. La configuración de la pantalla de consentimiento de OAuth de tu complemento define lo que Google muestra a los usuarios.

  1. En la consola de Google Cloud, ve a Menú > > Desarrollo de la marca.

    Ve a Desarrollo de la marca

  2. Si ya configuraste , puedes configurar la siguiente configuración de la pantalla de consentimiento de OAuth en Desarrollo de la marca, Público y Acceso a los datos. Si ves un mensaje que dice aún no se configuró, haz clic en Comenzar:
    1. En Información de la app, en Nombre de la app, ingresa un nombre para la app.
    2. En Correo electrónico de asistencia del usuario, elige una dirección de correo electrónico de asistencia para que los usuarios se comuniquen contigo si tienen preguntas sobre su consentimiento.
    3. Haz clic en Siguiente.
    4. En Público, selecciona Interno.
    5. Haz clic en Siguiente.
    6. En Información de contacto, ingresa una dirección de correo electrónico a la que se te puedan enviar notificaciones sobre cualquier cambio en tu proyecto.
    7. Haz clic en Siguiente.
    8. En Finalizar, revisa la Política de Datos del Usuario de los Servicios de las APIs de Google y, si estás de acuerdo, selecciona Acepto la Política de Datos del Usuario de los Servicios de las APIs de Google.
    9. Haz clic en Continuar.
    10. Haz clic en Crear.
  3. Por ahora, puedes omitir agregar permisos. En el futuro, cuando crees una app para usarla fuera de tu organización de Google Workspace, debes cambiar el Tipo de usuario a Externo. Luego, agrega los permisos de autorización que requiere tu app. Para obtener más información, consulta la guía completa Configura el consentimiento de OAuth.

Configura la secuencia de comandos

Crea el proyecto de Apps Script

  1. Haz clic en el siguiente botón para abrir el proyecto de Apps Script Compartir una macro.
    Abre el proyecto
  2. Haz clic en Resumen .
  3. En la página de descripción general, haz clic en Crear una copia El ícono para crear una copia.

Copia el número del proyecto de Cloud

  1. En la consola de Google Cloud, ve a Menú > IAM y administración > Configuración.

    Ir a Configuración de IAM y administrador

  2. En el campo Número del proyecto, copia el valor.

Configura el proyecto de Cloud del proyecto de Apps Script

  1. En el proyecto de Apps Script copiado, haz clic en Configuración del proyecto El ícono de configuración del proyecto.
  2. En Proyecto de Google Cloud Platform (GCP), haz clic en Cambiar proyecto.
  3. En Número de proyecto de GCP, pega el número de proyecto de Google Cloud.
  4. Haz clic en Establecer el proyecto.

Instala una implementación de prueba

  1. En el proyecto de Apps Script copiado, haz clic en Editor .
  2. Abre el archivo UI.gs y haz clic en Run. Cuando se te solicite, autoriza la secuencia de comandos.
  3. Haz clic en Implementar > Probar implementaciones.
  4. Haz clic en Instalar > Listo.

Obtén la información de la macro y la hoja de cálculo

  1. Abre una hoja de cálculo de Hojas de cálculo que tenga una macro y para la que tengas permiso para editar. Para usar una hoja de cálculo de muestra, crea una copia de la hoja de cálculo Macro de muestra.
  2. Haz clic en Extensiones > Apps Script.
  3. En el proyecto de Apps Script, haz clic en Configuración del proyecto El ícono de configuración del proyecto.
  4. Debajo del ID de la secuencia de comandos, haz clic en Copiar.
  5. Reserva el ID de la secuencia de comandos para usarlo en un paso posterior.
  6. Abre o crea una hoja de cálculo nueva en la que quieras agregar la macro. Debes tener permiso para editar la hoja de cálculo.
  7. Copia la URL de la hoja de cálculo y guárdala para usarla en un paso posterior.

Ejecuta la secuencia de comandos:

Asegúrate de que la API de Google Apps Script esté activada en la configuración del panel. Sigue los pasos de las siguientes secciones para ejecutar la secuencia de comandos.

Copia la macro

  1. En Hojas de cálculo, en la barra lateral derecha, abre el complemento Share Macro El ícono para crear una copia.
  2. En Macro de origen, pega el ID de la secuencia de comandos.
  3. En Hoja de cálculo de destino, pega la URL de la hoja de cálculo.
  4. Haz clic en Compartir macro.
  5. Haz clic en Autorizar acceso y autoriza el complemento.
  6. Repite los pasos del 2 al 4.

Abre la macro copiada.

  1. Si aún no está abierta, abre la hoja de cálculo a la que copiaste la macro.
  2. Haz clic en Extensiones > Apps Script.
  3. Si no ves el proyecto de Apps Script copiado, asegúrate de que la API de Google Apps Script esté activada en la configuración del panel y repite los pasos que se indican en Copia la macro.

Revisa el código

Para revisar el código de Apps Script de esta solución, haz clic en Ver código fuente a continuación:

Ver el código fuente

Code.gs

solutions/add-on/share-macro/Code.js
// To learn how to use this script, refer to the documentation:
// https://developers.devsite.corp.google.com/apps-script/add-ons/share-macro

/*
Copyright 2022 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/**
 * Uses Apps Script API to copy source Apps Script project 
 * to destination Google Spreadsheet container.
 * 
 * @param {string} sourceScriptId - Script ID of the source project.
 * @param {string} targetSpreadsheetUrl - URL if the target spreadsheet.
 */
function shareMacro_(sourceScriptId, targetSpreadsheetUrl) {

  // Gets the source project content using the Apps Script API.
  const sourceProject = APPS_SCRIPT_API.get(sourceScriptId);
  const sourceFiles = APPS_SCRIPT_API.getContent(sourceScriptId);

  // Opens the target spreadsheet and gets its ID.
  const parentSSId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId();

  // Creates an Apps Script project that's bound to the target spreadsheet.
  const targetProjectObj = APPS_SCRIPT_API.create(sourceProject.title, parentSSId);

  // Updates the Apps Script project with the source project content.
  APPS_SCRIPT_API.updateContent(targetProjectObj.scriptId, sourceFiles);

}

/**
 * Function that encapsulates Apps Script API project manipulation. 
*/
const APPS_SCRIPT_API = {
  accessToken: ScriptApp.getOAuthToken(),

  /* APPS_SCRIPT_API.get
   * Gets Apps Script source project.
   * @param {string} scriptId - Script ID of the source project.
   * @return {Object} - JSON representation of source project.
   */
  get: function (scriptId) {
    const url = ('https://script.googleapis.com/v1/projects/' + scriptId);
    const options = {
      "method": 'get',
      "headers": {
        "Authorization": "Bearer " + this.accessToken
      },
      "muteHttpExceptions": true,
    };
    const res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      return JSON.parse(res);
    } else {
      console.log('An error occurred gettting the project details');
      console.log(res.getResponseCode());
      console.log(res.getContentText());
      console.log(res);
      return false;
    }
  },

  /* APPS_SCRIPT_API.create
   * Creates new Apps Script project in the target spreadsheet.
   * @param {string} title - Name of Apps Script project.
   * @param {string} parentId - Internal ID of target spreadsheet.
   * @return {Object} - JSON representation completed project creation.
   */
  create: function (title, parentId) {
    const url = 'https://script.googleapis.com/v1/projects';
    const options = {
      "headers": {
        "Authorization": "Bearer " + this.accessToken,
        "Content-Type": "application/json"
      },
      "muteHttpExceptions": true,
      "method": "POST",
      "payload": { "title": title }
    }
    if (parentId) {
      options.payload.parentId = parentId;
    }
    options.payload = JSON.stringify(options.payload);
    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      res = JSON.parse(res);
      return res;
    } else {
      console.log("An error occurred while creating the project");
      console.log(res.getResponseCode());
      console.log(res.getContentText());
      console.log(res);
      return false;
    }
  },
   /* APPS_SCRIPT_API.getContent
   * Gets the content of the source Apps Script project.
   * @param {string} scriptId - Script ID of the source project.
   * @return {Object} - JSON representation of Apps Script project content.
   */
   getContent: function (scriptId) {
    const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content";
    const options = {
      "method": 'get',
      "headers": {
        "Authorization": "Bearer " + this.accessToken
      },
      "muteHttpExceptions": true,
    };
    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      res = JSON.parse(res);
      return res['files'];
    } else {
      console.log('An error occurred obtaining the content from the source script');
      console.log(res.getResponseCode());
      console.log(res.getContentText());
      console.log(res);
      return false;
    }
  },

  /* APPS_SCRIPT_API.updateContent
   * Updates (copies) content from source to target Apps Script project.
   * @param {string} scriptId - Script ID of the source project.
   * @param {Object} files - JSON representation of Apps Script project content.
   * @return {boolean} - Result status of the function.
   */
  updateContent: function (scriptId, files) {
    const url = "https://script.googleapis.com/v1/projects/" + scriptId + "/content";
    const options = {
      "method": 'put',
      "headers": {
        "Authorization": "Bearer " + this.accessToken
      },
      "contentType": "application/json",
      "payload": JSON.stringify({ "files": files }),
      "muteHttpExceptions": true,
    };
    let res = UrlFetchApp.fetch(url, options);
    if (res.getResponseCode() == 200) {
      return true;
    } else {
      console.log(`An error occurred updating content of script ${scriptId}`);
      console.log(res.getResponseCode());
      console.log(res.getContentText());
      console.log(res);
      return false;
    }
  }
}

UI.gs

solutions/add-on/share-macro/UI.js
/**
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Change application logo here (and in manifest) as desired.
const ADDON_LOGO = 'https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png';

/**
 * Callback function for rendering the main card.
 * @return {CardService.Card} The card to show the user.
 */
function onHomepage(e) {
  return createSelectionCard(e);
}

/**
 * Builds the primary card interface used to collect user inputs.
 * 
 * @param {Object} e - Add-on event object.
 * @param {string} sourceScriptId - Script ID of the source project.
 * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet.
 * @param {string[]} errors - Array of error messages. 
 * 
 * @return {CardService.Card} The card to show to the user for inputs.
 */
function createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors) {

  // Configures card header.
  let cardHeader = CardService.newCardHeader()
    .setTitle('Share macros with other spreadheets!')
    .setImageUrl(ADDON_LOGO)
    .setImageStyle(CardService.ImageStyle.SQUARE);

  // If form errors exist, configures section with error messages.
  let showErrors = false;

  if (errors && errors.length) {
    showErrors = true;
    let msg = errors.reduce((str, err) => `${str}${err}<br>`, '');
    msg = `<b>Form submission errors:</b><br><font color="#ba0000">${msg}</font>`;

    // Builds error message section.
    sectionErrors = CardService.newCardSection()
      .addWidget(CardService.newDecoratedText()
        .setText(msg)
        .setWrapText(true));
  }

  // Configures source project section.
  let sectionSource = CardService.newCardSection()
    .addWidget(CardService.newDecoratedText()
      .setText('<b>Source macro</b><br>The Apps Script project to copy'))

    .addWidget(CardService.newTextInput()
      .setFieldName('sourceScriptId')
      .setValue(sourceScriptId || '')
      .setTitle('Script ID of the source macro')
      .setHint('You must have at least edit permission for the source spreadsheet to access its script project'))

    .addWidget(CardService.newTextButton()
      .setText('Find the script ID')
      .setOpenLink(CardService.newOpenLink()
        .setUrl('https://developers.google.com/apps-script/api/samples/execute')
        .setOpenAs(CardService.OpenAs.FULL_SIZE)
        .setOnClose(CardService.OnClose.NOTHING)));

  // Configures target spreadsheet section.
  let sectionTarget = CardService.newCardSection()
    .addWidget(CardService.newDecoratedText()
      .setText('<b>Target spreadsheet</b>'))

    .addWidget(CardService.newTextInput()
      .setFieldName('targetSpreadsheetUrl')
      .setValue(targetSpreadsheetUrl || '')
      .setHint('You must have at least edit permission for the target spreadsheet')
      .setTitle('Target spreadsheet URL'));

  // Configures help section.
  let sectionHelp = CardService.newCardSection()
    .addWidget(CardService.newDecoratedText()
      .setText('<b><font color=#c80000>NOTE: </font></b>' +
        'The Apps Script API must be turned on.')
      .setWrapText(true))

    .addWidget(CardService.newTextButton()
      .setText('Turn on Apps Script API')
      .setOpenLink(CardService.newOpenLink()
        .setUrl('https://script.google.com/home/usersettings')
        .setOpenAs(CardService.OpenAs.FULL_SIZE)
        .setOnClose(CardService.OnClose.NOTHING)));

  // Configures card footer with action to copy the macro.
  var cardFooter = CardService.newFixedFooter()
    .setPrimaryButton(CardService.newTextButton()
      .setText('Share macro')
      .setOnClickAction(CardService.newAction()
        .setFunctionName('onClickFunction_')));

  // Begins building the card.
  let builder = CardService.newCardBuilder()
    .setHeader(cardHeader);

  // Adds error section if applicable.
  if (showErrors) {
    builder.addSection(sectionErrors)
  }

  // Adds final sections & footer.
  builder
    .addSection(sectionSource)
    .addSection(sectionTarget)
    .addSection(sectionHelp)
    .setFixedFooter(cardFooter);

  return builder.build();
}

/**
 * Action handler that validates user inputs and calls shareMacro_
 * function to copy Apps Script project to target spreadsheet.
 * 
 * @param {Object} e - Add-on event object.
 * 
 * @return {CardService.Card} Responds with either a success or error card.
 */
function onClickFunction_(e) {

  const sourceScriptId = e.formInput.sourceScriptId;
  const targetSpreadsheetUrl = e.formInput.targetSpreadsheetUrl;

  // Validates inputs for errors.
  const errors = [];

  // Pushes an error message if the Script ID parameter is missing.
  if (!sourceScriptId) {
    errors.push('Missing script ID');
  } else {

    // Gets the Apps Script project if the Script ID parameter is valid.
    const sourceProject = APPS_SCRIPT_API.get(sourceScriptId);
    if (!sourceProject) {
      // Pushes an error message if the Script ID parameter isn't valid.
      errors.push('Invalid script ID');
    }
  }

  // Pushes an error message if the spreadsheet URL is missing.
  if (!targetSpreadsheetUrl) {
    errors.push('Missing Spreadsheet URL');
  } else
    try {
      // Tests for valid spreadsheet URL to get the spreadsheet ID.
      const ssId = SpreadsheetApp.openByUrl(targetSpreadsheetUrl).getId();
    } catch (err) {
      // Pushes an error message if the spreadsheet URL parameter isn't valid.
      errors.push('Invalid spreadsheet URL');
    }

  if (errors && errors.length) {
    // Redisplays form if inputs are missing or invalid.
    return createSelectionCard(e, sourceScriptId, targetSpreadsheetUrl, errors);

  } else {
    // Calls shareMacro function to copy the project.
    shareMacro_(sourceScriptId, targetSpreadsheetUrl);

    // Creates a success card to display to users.
    return buildSuccessCard(e, targetSpreadsheetUrl);
  }
}

/**
 * Builds success card to inform user & let them open the spreadsheet.
 * 
 * @param {Object} e - Add-on event object.
 * @param {string} targetSpreadsheetUrl - URL of the target spreadsheet.
 * 
 * @return {CardService.Card} Returns success card.
 */function buildSuccessCard(e, targetSpreadsheetUrl) {

  // Configures card header.
  let cardHeader = CardService.newCardHeader()
    .setTitle('Share macros with other spreadsheets!')
    .setImageUrl(ADDON_LOGO)
    .setImageStyle(CardService.ImageStyle.SQUARE);

  // Configures card body section with success message and open button.
  let sectionBody1 = CardService.newCardSection()
    .addWidget(CardService.newTextParagraph()
      .setText('Sharing process is complete!'))
    .addWidget(CardService.newTextButton()
      .setText('Open spreadsheet')
      .setOpenLink(CardService.newOpenLink()
        .setUrl(targetSpreadsheetUrl)
        .setOpenAs(CardService.OpenAs.FULL_SIZE)
        .setOnClose(CardService.OnClose.RELOAD_ADD_ON)));
  let sectionBody2 = CardService.newCardSection()
    .addWidget(CardService.newTextParagraph()
      .setText('If you don\'t see the copied project in your target spreadsheet,' +
       ' make sure you turned on the Apps Script API in the Apps Script dashboard.'))
    .addWidget(CardService.newTextButton()
      .setText("Check API")
      .setOpenLink(CardService.newOpenLink()
        .setUrl('https://script.google.com/home/usersettings')
        .setOpenAs(CardService.OpenAs.FULL_SIZE)
        .setOnClose(CardService.OnClose.RELOAD_ADD_ON)));

  // Configures the card footer with action to start new process.
  let cardFooter = CardService.newFixedFooter()
    .setPrimaryButton(CardService.newTextButton()
      .setText('Share another')
      .setOnClickAction(CardService.newAction()
        .setFunctionName('onHomepage')));

  return builder = CardService.newCardBuilder()
    .setHeader(cardHeader)
    .addSection(sectionBody1)
    .addSection(sectionBody2)
    .setFixedFooter(cardFooter)
    .build();
 }

appsscript.json

solutions/add-on/share-macro/appsscript.json
{
  "timeZone": "America/Los_Angeles",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/script.projects"
  ],
    "urlFetchWhitelist": [
    "https://script.googleapis.com/"
  ],
  "addOns": {
    "common": {
      "name": "Share Macro",
      "logoUrl": "https://www.gstatic.com/images/branding/product/2x/apps_script_48dp.png",
      "layoutProperties": {
        "primaryColor": "#188038",
        "secondaryColor": "#34a853"
      },
      "homepageTrigger": {
        "runFunction": "onHomepage"
      }
    },
    "sheets": {}
  }
}

Colaboradores

Google mantiene esta muestra con la ayuda de expertos en desarrollo de Google.

Próximos pasos