نسخ وحدات ماكرو إلى جداول بيانات أخرى

مستوى الترميز: متوسط
المدة: 30 دقيقة
نوع المشروع: إضافة Google Workspace

الأهداف

  • فهم دور الحلّ
  • فهم ما تفعله خدمات Apps Script ضمن الحلّ
  • إعداد البيئة
  • إعداد النص البرمجي
  • شغِّل النص البرمجي.

لمحة عن هذا الحل

إنّ نسخ وحدات الماكرو في "جداول بيانات Google" يدويًا من جدول بيانات إلى آخر قد يكون مستهلكًا للوقت وعرضةً للأخطاء. تؤدي إضافة Google Workspace هذه تلقائيًا إلى نسخ مشروع نص برمجي وإرفاقه بجدول بيانات يحدّده المستخدم. على الرغم من أنّ هذا الحلّ يركّز على وحدات الماكرو في "جداول بيانات Google"، يمكنك استخدامه لنسخ ومشاركة أي نص برمجي مرتبط بالحاويات.

لقطة شاشة لإضافة Share Macro في Google Workspace

آلية العمل

ينسخ البرنامج النصي مشروع "برمجة التطبيقات" المرتبط بالورقة الحاسبة الأصلية وينشئ مشروعًا مكرّرًا من "برمجة التطبيقات" مرتبطًا بالورقة الحاسبة التي حدّدها المستخدم.

خدمات "برمجة تطبيقات Google"

يستخدم هذا الحلّ الخدمات التالية:

المتطلبات الأساسية

لاستخدام هذا العيّنة، يجب استيفاء المتطلبات الأساسية التالية:

إعداد البيئة

فتح مشروعك على Cloud في Google Cloud Console

افتح مشروع Cloud الذي تريد استخدامه لهذه العيّنة إذا لم يكن مفتوحًا من قبل:

  1. في وحدة تحكّم Google Cloud، انتقِل إلى صفحة اختيار مشروع.

    اختيار مشروع على السحابة الإلكترونية

  2. اختَر مشروع Google Cloud الذي تريد استخدامه. أو انقر على إنشاء مشروع واتّبِع التعليمات الظاهرة على الشاشة. في حال إنشاء مشروع على Google Cloud، قد تحتاج إلى تفعيل الفوترة للمشروع.

تفعيل Google Apps Script API

تستخدِم ميزة "البدء السريع" هذه Google Apps Script API.

قبل استخدام واجهات برمجة تطبيقات Google، عليك تفعيلها في مشروع على Google Cloud. يمكنك تفعيل واجهة برمجة تطبيقات واحدة أو أكثر في مشروع واحد على Google Cloud.

تتطلّب إضافات Google Workspace ضبط شاشة الموافقة. من خلال ضبط شاشة طلب الموافقة المتعلّقة ببروتوكول OAuth في إضافتك، يمكنك تحديد ما يلي: المعلومات التي تعرِضها Google للمستخدمين.

  1. في وحدة تحكّم Google Cloud، انتقِل إلى رمز القائمة > > العلامة التجارية.

    الانتقال إلى "الهوية البصرية للعلامة التجارية"

  2. إذا سبق لك ضبط ، يمكنك ضبط إعدادات شاشة طلب الموافقة المتعلّقة ببروتوكول OAuth التالية في العلامة التجارية وشريحة الجمهور والوصول إلى البيانات. إذا ظهرت لك رسالة تفيد بأنّ لم يتم ضبطه بعد، انقر على البدء:
    1. ضمن معلومات التطبيق، أدخِل اسمًا للتطبيق في اسم التطبيق.
    2. في عنوان البريد الإلكتروني لدعم المستخدمين، اختَر عنوان بريد إلكتروني لفريق الدعم يمكن للمستخدمين التواصل معك من خلاله إذا كانت لديهم أسئلة حول موافقتهم.
    3. انقر على التالي.
    4. ضمن الجمهور، اختَر داخلي.
    5. انقر على التالي.
    6. ضمن معلومات الاتصال، أدخِل عنوان بريد إلكتروني يمكننا من خلاله إعلامك بأي تغييرات تطرأ على مشروعك.
    7. انقر على التالي.
    8. ضمن إنهاء، راجِع سياسة بيانات المستخدمين في خدمات Google API، وإذا كنت توافق عليها، انقر على أوافق على "سياسة بيانات المستخدمين في خدمات Google API".
    9. انقر على متابعة.
    10. انقر على إنشاء.
  3. يمكنك حاليًا تخطّي إضافة النطاقات. في المستقبل، عند إنشاء تطبيق لاستخدامه خارج مؤسستك على Google Workspace، عليك تغيير نوع المستخدم إلى خارجي. بعد ذلك، أضِف نطاقات التفويض التي يتطلبها تطبيقك. لمزيد من المعلومات، يُرجى الاطّلاع على الدليل الكامل ضبط موافقة OAuth.

إعداد النص البرمجي

إنشاء مشروع "برمجة تطبيقات Google"

  1. انقر على الزر التالي لفتح مشروع مشاركة وحدة ماكرو Apps Script.
    فتح المشروع
  2. انقر على نظرة عامة .
  3. في صفحة النظرة العامة، انقر على رمز إنشاء نسخة رمز لإنشاء نسخة.

نسخ رقم مشروع Cloud

  1. في Google Cloud Console، انتقِل إلى القائمة > المشرف وإدارة الهوية وإمكانية الوصول > الإعدادات.

    الانتقال إلى "إدارة الهوية وإمكانية الوصول" و"إعدادات المشرف"

  2. في حقل رقم المشروع، انسخ القيمة.

ضبط مشروع Cloud لمشروع "برمجة التطبيقات"

  1. في مشروع "برمجة تطبيقات Google" المنسوخ، انقر على إعدادات المشروع رمز إعدادات المشروع.
  2. ضمن مشروع Google Cloud Platform (GCP)، انقر على تغيير المشروع.
  3. في حقل رقم مشروع Google Cloud Platform، الصِق رقم مشروع Google Cloud.
  4. انقر على ضبط المشروع.

تثبيت عملية نشر تجريبية

  1. في مشروع "برمجة تطبيقات Google" المنسوخ، انقر على المحرِّر .
  2. افتح ملف UI.gs وانقر على تشغيل. امنح الإذن للملف البرمجي عند مطالبتك بذلك.
  3. انقر على نشر > اختبار عمليات النشر.
  4. انقر على تثبيت > تم.

الحصول على نص الماكرو ومعلومات جدول البيانات

  1. افتح جدول بيانات في "جداول بيانات Google" يحتوي على وحدة ماكرو ويكون لديك إذن بتعديله. لاستخدام نموذج جدول بيانات، أنشئ نسخة من نموذج جدول بيانات الوحدات النمطية.
  2. انقر على الإضافات > Apps Script.
  3. في مشروع "برمجة التطبيقات"، انقر على إعدادات المشروع رمز إعدادات المشروع.
  4. ضمن معرّف النص البرمجي، انقر على نسخ.
  5. احفظ رقم تعريف النص البرمجي لاستخدامه في خطوة لاحقة.
  6. افتح جدول بيانات جديدًا أو أنشئ جدول بيانات جديدًا تريد إضافة الماكرو إليه. يجب أن يكون لديك إذن بتعديل جدول البيانات.
  7. انسخ عنوان URL لجدول البيانات واحفظه لاستخدامه في خطوة لاحقة.

تشغيل النص البرمجي

تأكَّد من تفعيل Google Apps Script API في إعدادات لوحة البيانات. اتّبِع الخطوات الواردة في الأقسام التالية لتشغيل النص البرمجي.

نسخ وحدة الماكرو

  1. في "جداول بيانات Google"، افتح إضافة "مشاركة وحدة الماكرو" في الشريط الجانبي الأيمن رمز لإنشاء نسخة.
  2. ضمن الرمز الموسّع للمصدر، الصِق معرّف النص البرمجي.
  3. ضمن جدول البيانات المستهدَف، الصِق عنوان URL لجدول البيانات.
  4. انقر على مشاركة وحدات الماكرو.
  5. انقر على تفويض الوصول وافوض الإضافة.
  6. كرِّر الخطوات من 2 إلى 4.

افتح الماكرو المنسوخ.

  1. افتح جدول البيانات الذي نسخت إليه وحدة الماكرو إذا لم يكن مفتوحًا من قبل.
  2. انقر على الإضافات > Apps Script.
  3. إذا لم يظهر لك مشروع "برمجة تطبيقات Google" المنسوخ، تأكَّد من أنّه تم تفعيل واجهة برمجة التطبيقات Google Apps Script API في إعدادات لوحة البيانات وكرر الخطوات الواردة ضمن نسخ الماكرو.

مراجعة الرمز

لمراجعة رمز Apps Script لهذا الحلّ، انقر على عرض رمز المصدر أدناه:

عرض رمز المصدر

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": {}
  }
}

المساهمون

تُعدّ هذه العينة من قِبل Google بمساعدة "خبراء المطوّرين" في Google.

الخطوات التالية