マクロを他の Google スプレッドシートにコピーする

コーディング レベル: 中級
所要時間: 30 分
プロジェクト タイプ: Google Workspace アドオン

目標

  • ソリューションの機能を理解します。
  • ソリューション内で Apps Script サービスが何を行うかを理解します。
  • 環境を設定します。
  • スクリプトを設定します。
  • スクリプトを実行します。

このソリューションについて

Google スプレッドシートのマクロをあるスプレッドシートから別のスプレッドシートに手動でコピーすると、時間がかかり、エラーが発生しやすくなります。この Google Workspace アドオンは、スクリプト プロジェクトを自動的にコピーして、ユーザーが指定したスプレッドシートに添付します。このソリューションはスプレッドシートのマクロに重点を置いていますが、これを使用してコンテナ バインド スクリプトをコピーして共有することもできます。

Share Macro Google Workspace アドオンのスクリーンショット

仕組み

スクリプトは、元のスプレッドシートにバインドされている Apps Script プロジェクトをコピーし、ユーザーが指定したスプレッドシートにバインドされている Apps Script プロジェクトの複製を作成します。

Apps Script サービス

このソリューションでは、次のサービスを使用します。

  • URL 取得サービス - Apps Script API に接続して、ソース プロジェクトをコピーし、コピーを作成します。
  • Script サービス - 2 回目の承認プロンプトが表示されないように、Apps Script API を承認します。
  • スプレッドシート サービス - コピーした Apps Script プロジェクトを追加する対象のスプレッドシートが開きます。
  • カードサービス - アドオンのユーザー インターフェースを作成します。

前提条件

このサンプルを使用するには、次の前提条件を満たしている必要があります。

環境の設定

Google Cloud コンソールで Cloud プロジェクトを開く

まだ開いていない場合は、このサンプルで使用する Cloud プロジェクトを開きます。

  1. Google Cloud コンソールで、[プロジェクトを選択] ページに移動します。

    Cloud プロジェクトを選択する

  2. 使用する Google Cloud プロジェクトを選択します。または、[プロジェクトを作成] をクリックし、画面の指示に沿って操作します。Google Cloud プロジェクトを作成する場合は、プロジェクトの課金を有効にする必要がある場合があります。

Google Apps Script API を有効にする

このクイックスタートでは、Google Apps Script API を使用します。

Google API を使用する前に、Google Cloud プロジェクトで API を有効にする必要があります。1 つの Google Cloud プロジェクトで 1 つ以上の API を有効にできます。

Google Workspace アドオンには同意画面の構成が必要です。アドオンの OAuth 同意画面を構成すると、Google がユーザーに表示する内容が定義されます。

  1. Google Cloud コンソールで、メニュー > Google Auth platform > [ブランディング] に移動します。

    [ブランディング] に移動

  2. Google Auth platformをすでに構成している場合は、[ブランディング]、[対象ユーザー]、[データアクセス] で次の OAuth 同意画面の設定を構成できます。[Google Auth platform まだ設定されていません] というメッセージが表示された場合は、[使ってみる] をクリックします。
    1. [アプリ情報] の [アプリ名] に、アプリの名前を入力します。
    2. [ユーザー サポートメール] で、ユーザーが同意について問い合わせる際に使用するサポートのメールアドレスを選択します。
    3. [続行] をクリックします。
    4. [対象] で [内部] を選択します。
    5. [続行] をクリックします。
    6. [連絡先情報] で、プロジェクトに対する変更の通知を受け取るメールアドレスを入力します。
    7. [続行] をクリックします。
    8. [完了] で、Google API サービスのユーザーデータに関するポリシーを確認し、同意する場合は [Google API サービス: ユーザーデータに関するポリシーに同意します] を選択します。
    9. [続行] をクリックします。
    10. [作成] をクリックします。
  3. 現時点では、スコープの追加はスキップできます。今後、Google Workspace 組織外で使用するアプリを作成する場合は、[ユーザータイプ] を [外部] に変更する必要があります。次に、アプリに必要な認可スコープを追加します。詳細については、OAuth 同意画面を構成するの完全なガイドをご覧ください。

スクリプトを設定する

Apps Script プロジェクトを作成する

  1. 次のボタンをクリックして、マクロを共有する Apps Script プロジェクトを開きます。
    プロジェクトを開く
  2. [概要] をクリックします。
  3. 概要ページで [コピーを作成] コピーを作成するためのアイコン をクリックします。

Cloud プロジェクト番号をコピーする

  1. Google Cloud コンソールで、メニュー アイコン > [IAM と管理] > [設定] に移動します。

    [IAM と管理] の [設定] に移動

  2. [プロジェクト番号] フィールドで、値をコピーします。

Apps Script プロジェクトの Cloud プロジェクトを設定する

  1. コピーした Apps Script プロジェクトで、[プロジェクトの設定] プロジェクト設定のアイコン をクリックします。
  2. [Google Cloud Platform(GCP)プロジェクト] で、[プロジェクトを変更] をクリックします。
  3. [GCP プロジェクト番号] に、Google Cloud プロジェクト番号を貼り付けます。
  4. [プロジェクトを設定] をクリックします。

テスト デプロイをインストールする

  1. コピーした Apps Script プロジェクトで、[エディタ] をクリックします。
  2. UI.gs ファイルを開き、[実行] をクリックします。メッセージが表示されたら、スクリプトを承認します。
  3. [Deploy](デプロイ)> [Test deployments](テスト デプロイ)をクリックします。
  4. [インストール] > [完了] をクリックします。

マクロ スクリプトとスプレッドシートの情報を取得する

  1. マクロが含まれており、編集権限がある Google スプレッドシートを開きます。サンプル スプレッドシートを使用するには、サンプル マクロ スプレッドシートのコピーを作成します。
  2. [拡張機能] > [Apps Script] をクリックします。
  3. Apps Script プロジェクトで、[プロジェクトの設定] プロジェクト設定のアイコン をクリックします。
  4. スクリプト ID の下にある [コピー] をクリックします。
  5. スクリプト ID をメモしておきます。これは後の手順で使用します。
  6. マクロを追加するスプレッドシートを開くか、新規作成します。スプレッドシートを編集する権限が必要です。
  7. スプレッドシートの URL をコピーして、後のステップで使用するために保存します。

スクリプトを実行する

ダッシュボードの設定で Google Apps Script API がオンになっていることを確認します。以降のセクションの手順に沿ってスクリプトを実行します。

マクロをコピーする

  1. スプレッドシートの右側のサイドバーで、Share Macro アドオン コピーを作成するためのアイコン を開きます。
  2. [ソース マクロ] にスクリプト ID を貼り付けます。
  3. [ターゲット スプレッドシート] に、スプレッドシートの URL を貼り付けます。
  4. [共有マクロ] をクリックします。
  5. [アクセスを承認] をクリックして、アドオンを承認します。
  6. ステップ 2 ~ 4 を繰り返します。

コピーしたマクロを開く

  1. まだ開いていない場合は、マクロをコピーしたスプレッドシートを開きます。
  2. [拡張機能] > [Apps Script] をクリックします。
  3. コピーした Apps Script プロジェクトが表示されない場合は、ダッシュボードの設定で Google Apps Script API がオンになっていることを確認し、マクロをコピーするの手順を繰り返します。

コードを確認する

このソリューションの Apps Script コードを確認するには、下の [ソースコードを表示] をクリックします。

ソースコードを表示

コード.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": {}
  }
}

寄稿者

このサンプルは、Google デベロッパー エキスパートの協力を得て Google が管理しています。

次のステップ