Node.js और Cloud Run की मदद से Google Workspace ऐड-ऑन बनाना

1. परिचय

Google Workspace ऐड-ऑन, पसंद के मुताबिक बनाए गए ऐप्लिकेशन होते हैं. ये Gmail, Docs, Sheets, और Slides जैसे Google Workspace ऐप्लिकेशन के साथ इंटिग्रेट हो जाते हैं. इनकी मदद से डेवलपर, Google Workspace में सीधे तौर पर इंटिग्रेट किए जा सकने वाले, पसंद के मुताबिक बनाए गए यूज़र इंटरफ़ेस बना सकते हैं. ऐड-ऑन की मदद से, उपयोगकर्ता अलग-अलग ऐप्लिकेशन के बीच स्विच किए बिना ज़्यादा बेहतर तरीके से काम कर सकते हैं.

इस कोडलैब में, Node.js, Cloud Run, और Datastore का इस्तेमाल करके, टास्क की सूची बनाने वाला एक सामान्य ऐड-ऑन बनाने और उसे डिप्लॉय करने का तरीका बताया गया है.

आपको क्या सीखने को मिलेगा

  • Cloud Shell का इस्तेमाल करना
  • Cloud Run पर डिप्लॉय करें
  • ऐड-ऑन डिप्लॉयमेंट डिस्क्रिप्टर बनाना और उसे डिप्लॉय करना
  • कार्ड फ़्रेमवर्क की मदद से ऐड-ऑन यूज़र इंटरफ़ेस (यूआई) बनाना
  • उपयोगकर्ता के इंटरैक्शन का जवाब देना
  • ऐड-ऑन में उपयोगकर्ता के कॉन्टेक्स्ट का इस्तेमाल करना

2. सेटअप और ज़रूरी शर्तें

Google Cloud प्रोजेक्ट बनाने के लिए, सेटअप से जुड़े निर्देशों का पालन करें. साथ ही, उन एपीआई और सेवाओं को चालू करें जिनका इस्तेमाल ऐड-ऑन करेगा.

अपने हिसाब से एनवायरमेंट सेट अप करना

  1. Cloud Console खोलें और एक नया प्रोजेक्ट बनाएं. (अगर आपके पास पहले से Gmail या Google Workspace खाता नहीं है, तो एक खाता बनाएं.)

'कोई प्रोजेक्ट चुनें' मेन्यू

'नया प्रोजेक्ट' बटन

प्रोजेक्ट आईडी

प्रोजेक्ट आईडी याद रखें. यह सभी Google Cloud प्रोजेक्ट के लिए एक यूनीक नाम होता है. ऊपर दिया गया नाम पहले ही इस्तेमाल किया जा चुका है. इसलिए, यह आपके लिए काम नहीं करेगा. माफ़ करें! इस कोड लैब में इसे बाद में PROJECT_ID के तौर पर दिखाया जाएगा.

  1. इसके बाद, Google Cloud संसाधनों का इस्तेमाल करने के लिए, Cloud Console में बिलिंग चालू करें..

इस कोडलैब को पूरा करने में ज़्यादा खर्च नहीं आएगा. कोडलैब के आखिर में दिए गए "क्लीन अप" सेक्शन में दिए गए निर्देशों का पालन करना न भूलें. इसमें आपको संसाधनों को बंद करने का तरीका बताया गया है, ताकि इस ट्यूटोरियल के बाद आपसे शुल्क न लिया जाए. Google Cloud के नए उपयोगकर्ता, मुफ़्त में आज़माने के लिए 300 डॉलर के प्रोग्राम में शामिल हो सकते हैं.

Google Cloud Shell

Google Cloud को अपने लैपटॉप से रिमोटली ऐक्सेस किया जा सकता है. हालांकि, इस कोडलैब में हम Google Cloud Shell का इस्तेमाल करेंगे. यह क्लाउड में चलने वाला कमांड लाइन एनवायरमेंट है.

Cloud Shell चालू करें

  1. Cloud Console में, Cloud Shell चालू करें Cloud Shell का आइकॉन पर क्लिक करें.

मेन्यू बार में Cloud Shell का आइकॉन

Cloud Shell को पहली बार खोलने पर, आपको एक वेलकम मैसेज दिखता है. इसमें Cloud Shell के बारे में जानकारी होती है. अगर आपको वेलकम मैसेज दिखता है, तो जारी रखें पर क्लिक करें. पहली बार मिलने वाला मैसेज दोबारा नहीं दिखता. वेलकम मैसेज यहां दिया गया है:

Cloud Shell का वेलकम मैसेज

Cloud Shell से कनेक्ट होने में कुछ ही सेकंड लगेंगे. कनेक्ट होने के बाद, आपको Cloud Shell टर्मिनल दिखेगा:

Cloud Shell टर्मिनल

इस वर्चुअल मशीन में, डेवलपमेंट के लिए ज़रूरी सभी टूल पहले से मौजूद होते हैं. यह 5 जीबी की होम डायरेक्ट्री उपलब्ध कराता है और Google Cloud में चलता है. इससे नेटवर्क की परफ़ॉर्मेंस और पुष्टि करने की प्रोसेस बेहतर होती है. इस कोडलैब में मौजूद सभी टास्क, ब्राउज़र या Chromebook की मदद से पूरे किए जा सकते हैं.

Cloud Shell से कनेक्ट होने के बाद, आपको दिखेगा कि आपकी पुष्टि पहले ही हो चुकी है और प्रोजेक्ट को आपके प्रोजेक्ट आईडी पर पहले ही सेट कर दिया गया है.

  1. पुष्टि करें कि आपने Cloud Shell में पुष्टि कर ली है. इसके लिए, यह कमांड चलाएं:
gcloud auth list

अगर आपको GCP API कॉल करने के लिए, Cloud Shell को अनुमति देने के लिए कहा जाता है, तो अनुमति दें पर क्लिक करें.

कमांड आउटपुट

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

चालू खाता सेट करने के लिए, यह कमांड चलाएं:

gcloud config set account <ACCOUNT>

पुष्टि करें कि आपने सही प्रोजेक्ट चुना है. इसके लिए, Cloud Shell में यह कमांड चलाएं:

gcloud config list project

कमांड आउटपुट

[core]
project = <PROJECT_ID>

अगर सही प्रोजेक्ट नहीं मिलता है, तो इस निर्देश का इस्तेमाल करके उसे सेट किया जा सकता है:

gcloud config set project <PROJECT_ID>

कमांड आउटपुट

Updated property [core/project].

इस कोडलैब में, कमांड लाइन के साथ-साथ फ़ाइल में बदलाव करने के तरीके भी बताए गए हैं. फ़ाइल में बदलाव करने के लिए, Cloud Shell में पहले से मौजूद कोड एडिटर का इस्तेमाल किया जा सकता है. इसके लिए, Cloud Shell टूलबार के दाईं ओर मौजूद, एडिटर खोलें बटन पर क्लिक करें. Cloud Shell में, आपको vim और emacs जैसे लोकप्रिय एडिटर भी मिलेंगे.

3. Cloud Run, Datastore, और ऐड-ऑन एपीआई चालू करें

Cloud API चालू करना

Cloud Shell से, उन कॉम्पोनेंट के लिए Cloud API चालू करें जिनका इस्तेमाल किया जाएगा:

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  cloudresourcemanager.googleapis.com \
  datastore.googleapis.com \
  gsuiteaddons.googleapis.com

इस प्रोसेस को पूरा होने में कुछ समय लग सकता है.

टास्क पूरा होने पर, आपको इस तरह का मैसेज दिखेगा:

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

डेटास्टोर इंस्टेंस बनाना

इसके बाद, App Engine चालू करें और Datastore डेटाबेस बनाएं. Datastore का इस्तेमाल करने के लिए, App Engine को चालू करना ज़रूरी है. हालांकि, हम App Engine का इस्तेमाल किसी अन्य काम के लिए नहीं करेंगे.

gcloud app create --region=us-central
gcloud firestore databases create --type=datastore-mode --region=us-central

ऐड-ऑन को चलाने और उपयोगकर्ता के डेटा पर कार्रवाई करने के लिए, उपयोगकर्ता की अनुमति ज़रूरी होती है. इसे चालू करने के लिए, प्रोजेक्ट की सहमति वाली स्क्रीन को कॉन्फ़िगर करें. कोड लैब के लिए, सहमति स्क्रीन को इंटरनल ऐप्लिकेशन के तौर पर कॉन्फ़िगर किया जाएगा. इसका मतलब है कि इसे सार्वजनिक तौर पर डिस्ट्रिब्यूट नहीं किया जाएगा.

  1. Google Cloud Console को नए टैब या विंडो में खोलें.
  2. "Google Cloud Console" के बगल में मौजूद, डाउन ऐरो ड्रॉप-डाउन ऐरो पर क्लिक करें और अपना प्रोजेक्ट चुनें.
  3. सबसे ऊपर बाएं कोने में, मेन्यू मेन्यू आइकॉन पर क्लिक करें.
  4. एपीआई और सेवाएं > क्रेडेंशियल पर क्लिक करें. आपके प्रोजेक्ट के लिए क्रेडेंशियल पेज दिखता है.
  5. OAuth के लिए सहमति वाली स्क्रीन पर क्लिक करें. इसके बाद, "OAuth के लिए सहमति देने वाली स्क्रीन" दिखेगी.
  6. "उपयोगकर्ता का टाइप" में जाकर, इंटरनल चुनें. अगर @gmail.com खाते का इस्तेमाल किया जा रहा है, तो बाहरी को चुनें.
  7. बनाएं पर क्लिक करें. "ऐप्लिकेशन रजिस्ट्रेशन में बदलाव करें" पेज दिखेगा.
  8. फ़ॉर्म भरें:
    • ऐप्लिकेशन का नाम में, "Todo Add-on" डालें.
    • उपयोगकर्ता सहायता के लिए ईमेल पता में, अपना निजी ईमेल पता डालें.
    • डेवलपर की संपर्क जानकारी में जाकर, अपना निजी ईमेल पता डालें.
  9. सेव करें और जारी रखें पर क्लिक करें. आपको स्कोप का फ़ॉर्म दिखेगा.
  10. स्कोप फ़ॉर्म में, सेव करें और जारी रखें पर क्लिक करें. आपको खास जानकारी दिखेगी.
  11. डैशबोर्ड पर वापस जाएं पर क्लिक करें.

4. शुरुआती ऐड-ऑन बनाना

प्रोजेक्ट को शुरू करना

शुरू करने के लिए, आपको एक आसान "Hello world" ऐड-ऑन बनाना होगा और उसे डिप्लॉय करना होगा. ऐड-ऑन, वेब सेवाएं होती हैं. ये https अनुरोधों का जवाब देती हैं. साथ ही, ये JSON पेलोड के साथ जवाब देती हैं. इस पेलोड में यूज़र इंटरफ़ेस (यूआई) और की जाने वाली कार्रवाइयों के बारे में जानकारी होती है. इस ऐड-ऑन में, Node.js और Express फ़्रेमवर्क का इस्तेमाल किया जाएगा.

इस टेंप्लेट प्रोजेक्ट को बनाने के लिए, Cloud Shell का इस्तेमाल करके todo-add-on नाम की नई डायरेक्ट्री बनाएं और उस पर जाएं:

mkdir ~/todo-add-on
cd ~/todo-add-on

इस डायरेक्ट्री में ही इस कोडलैब का सारा काम किया जाएगा.

Node.js प्रोजेक्ट शुरू करें:

npm init

NPM, प्रोजेक्ट कॉन्फ़िगरेशन के बारे में कई सवाल पूछता है. जैसे, नाम और वर्शन. डिफ़ॉल्ट वैल्यू स्वीकार करने के लिए, हर सवाल के लिए ENTER दबाएं. डिफ़ॉल्ट एंट्री पॉइंट, index.js नाम की फ़ाइल होती है. इसे हम अगले चरण में बनाएंगे.

इसके बाद, Express वेब फ़्रेमवर्क इंस्टॉल करें:

npm install --save express express-async-handler

ऐड-ऑन का बैकएंड बनाना

अब ऐप्लिकेशन बनाना शुरू करें.

index.js नाम की फ़ाइल बनाएं. फ़ाइलें बनाने के लिए, Cloud Shell Editor का इस्तेमाल किया जा सकता है. इसके लिए, Cloud Shell विंडो के टूलबार पर मौजूद Open Editor बटन पर क्लिक करें. इसके अलावा, Cloud Shell में फ़ाइलों में बदलाव करने और उन्हें मैनेज करने के लिए, vim या emacs का इस्तेमाल किया जा सकता है.

index.js फ़ाइल बनाने के बाद, यह कॉन्टेंट जोड़ें:

const express = require('express');
const asyncHandler = require('express-async-handler');

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post("/", asyncHandler(async (req, res) => {
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello world!`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

सर्वर, ‘Hello world' मैसेज दिखाने के अलावा और कुछ नहीं करता. बाद में, ज़्यादा सुविधाएं जोड़ी जाएंगी.

Cloud Run पर डिप्लॉय करें

Cloud Run पर डिप्लॉय करने के लिए, ऐप्लिकेशन को कंटेनर में रखना ज़रूरी है.

कंटेनर बनाना

Dockerfile नाम की Dockerfile बनाएं. इसमें यह कॉन्टेंट शामिल होना चाहिए:

FROM node:12-slim

# Create and change to the app directory.
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production

# Copy local code to the container image.
COPY . ./

# Run the web service on container startup.
CMD [ "node", "index.js" ]

कंटेनर में अनचाही फ़ाइलों को शामिल होने से रोकना

कंटेनर को हल्का रखने के लिए, .dockerignore फ़ाइल बनाएं. इसमें ये शामिल होने चाहिए:

Dockerfile
.dockerignore
node_modules
npm-debug.log

Cloud Build चालू करना

इस कोडलैब में, नई सुविधाएं जोड़े जाने पर ऐड-ऑन को कई बार बनाया और डिप्लॉय किया जाएगा. कंटेनर बनाने, उसे कंटेनर रजिस्ट्री में पुश करने, और उसे Cloud Build में डिप्लॉय करने के लिए अलग-अलग कमांड चलाने के बजाय, Cloud Build का इस्तेमाल करके पूरी प्रोसेस को मैनेज करें. ऐप्लिकेशन को बनाने और डिप्लॉय करने के तरीके के बारे में निर्देश देने वाली cloudbuild.yaml फ़ाइल बनाएं:

steps:
 # Build the container image
 - name: 'gcr.io/cloud-builders/docker'
   args: ['build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME', '.']
 # Push the container image to Container Registry
 - name: 'gcr.io/cloud-builders/docker'
   args: ['push', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME']
 # Deploy container image to Cloud Run
 - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
   entrypoint: gcloud
   args:
   - 'run'
   - 'deploy'
   - '$_SERVICE_NAME'
   - '--image'
   - 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
   - '--region'
   - '$_REGION'
   - '--platform'
   - 'managed'
images:
 - 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
substitutions:
   _SERVICE_NAME: todo-add-on
   _REGION: us-central1

ऐप्लिकेशन को डिप्लॉय करने के लिए, Cloud Build को अनुमति देने के लिए ये कमांड चलाएं:

PROJECT_ID=$(gcloud config list --format='value(core.project)')
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/run.admin
gcloud iam service-accounts add-iam-policy-binding \
    $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/iam.serviceAccountUser

ऐड-ऑन का बैकएंड बनाना और उसे डिप्लॉय करना

बिल्ड शुरू करने के लिए, Cloud Shell में यह कमांड चलाएँ:

gcloud builds submit

पूरी तरह से बिल्ड और डिप्लॉय होने में कुछ मिनट लग सकते हैं. खास तौर पर, पहली बार ऐसा करने पर.

बिल्ड पूरा होने के बाद, पुष्टि करें कि सेवा डिप्लॉय हो गई है और यूआरएल ढूंढें. यह कमांड चलाएं:

gcloud run services list --platform managed

इस यूआरएल को कॉपी करें. आपको इसकी ज़रूरत अगले चरण में पड़ेगी. इस चरण में, Google Workspace को यह बताया जाता है कि ऐड-ऑन को कैसे चालू किया जाए.

ऐड-ऑन रजिस्टर करना

सर्वर चालू होने के बाद, ऐड-ऑन के बारे में जानकारी दें, ताकि Google Workspace को पता चल सके कि इसे कैसे दिखाया जाए और कैसे चालू किया जाए.

डिप्लॉयमेंट डिस्क्रिप्टर बनाना

यहां दिए गए कॉन्टेंट के साथ deployment.json फ़ाइल बनाएं. पक्का करें कि आपने URL प्लेसहोल्डर की जगह, डिप्लॉय किए गए ऐप्लिकेशन के यूआरएल का इस्तेमाल किया हो.

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/calendar.addons.execute"
  ],
  "addOns": {
    "common": {
      "name": "Todo Codelab",
      "logoUrl": "https://raw.githubusercontent.com/webdog/octicons-png/main/black/check.png",
      "homepageTrigger": {
        "runFunction": "URL"
      }
    },
    "gmail": {},
    "drive": {},
    "calendar": {},
    "docs": {},
    "sheets": {},
    "slides": {}
  }
}

यह निर्देश चलाकर, डिप्लॉयमेंट डिस्क्रिप्टर अपलोड करें:

gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json

ऐड-ऑन के बैकएंड को ऐक्सेस करने की अनुमति देना

ऐड-ऑन फ़्रेमवर्क को भी सेवा को कॉल करने की अनुमति चाहिए. Cloud Run के लिए IAM नीति को अपडेट करने के लिए, यहां दी गई कमांड चलाएं. इससे Google Workspace को ऐड-ऑन शुरू करने की अनुमति मिल जाएगी:

SERVICE_ACCOUNT_EMAIL=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)")
gcloud run services add-iam-policy-binding todo-add-on --platform managed --region us-central1 --role roles/run.invoker --member "serviceAccount:$SERVICE_ACCOUNT_EMAIL"

टेस्टिंग के लिए ऐड-ऑन इंस्टॉल करना

अपने खाते के लिए, डेवलपमेंट मोड में ऐड-ऑन इंस्टॉल करने के लिए, Cloud Shell में यह कमांड चलाएं:

gcloud workspace-add-ons deployments install todo-add-on

(Gmail)[https://mail.google.com/] को नए टैब या विंडो में खोलें. दाईं ओर, चेक मार्क वाले आइकॉन के साथ ऐड-ऑन ढूंढें.

इंस्टॉल किए गए ऐड-ऑन का आइकॉन

ऐड-ऑन खोलने के लिए, सही का निशान वाले आइकॉन पर क्लिक करें. ऐड-ऑन को अनुमति देने के लिए एक प्रॉम्प्ट दिखता है.

पुष्टि करने का प्रॉम्प्ट

ऐक्सेस करने की अनुमति दें पर क्लिक करें. इसके बाद, पॉप-अप में अनुमति देने से जुड़े निर्देशों का पालन करें. यह प्रोसेस पूरी होने के बाद, ऐड-ऑन अपने-आप फिर से लोड हो जाता है और ‘Hello world!' मैसेज दिखाता है.

बधाई हो! अब आपके पास एक सामान्य ऐड-ऑन डिप्लॉय और इंस्टॉल किया गया है. अब इसे टास्क सूची वाले ऐप्लिकेशन में बदलें!

5. उपयोगकर्ता की पहचान को ऐक्सेस करना

आम तौर पर, कई उपयोगकर्ता ऐड-ऑन का इस्तेमाल ऐसी जानकारी के साथ काम करने के लिए करते हैं जो उनके या उनके संगठनों के लिए निजी होती है. इस कोडलैब में, ऐड-ऑन को सिर्फ़ मौजूदा उपयोगकर्ता के टास्क दिखाने चाहिए. उपयोगकर्ता की पहचान, ऐड-ऑन को आइडेंटिटी टोकन के ज़रिए भेजी जाती है. इसे डिकोड करना ज़रूरी होता है.

डिप्लॉयमेंट डिस्क्रिप्टर में स्कोप जोड़ना

उपयोगकर्ता की पहचान को डिफ़ॉल्ट रूप से नहीं भेजा जाता है. यह उपयोगकर्ता का डेटा है और ऐड-ऑन को इसे ऐक्सेस करने की अनुमति चाहिए. यह अनुमति पाने के लिए, deployment.json को अपडेट करें. साथ ही, ऐड-ऑन के लिए ज़रूरी दायरों की सूची में openid और email OAuth स्कोप जोड़ें. OAuth स्कोप जोड़ने के बाद, ऐड-ऑन उपयोगकर्ताओं को अगली बार ऐड-ऑन इस्तेमाल करते समय, ऐक्सेस देने के लिए कहता है.

"oauthScopes": [
      "https://www.googleapis.com/auth/gmail.addons.execute",
      "https://www.googleapis.com/auth/calendar.addons.execute",
      "openid",
      "email"
],

इसके बाद, डिप्लॉयमेंट डिस्क्रिप्टर को अपडेट करने के लिए, Cloud Shell में यह निर्देश चलाएं:

gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json

ऐड-ऑन सर्वर को अपडेट करना

ऐड-ऑन को उपयोगकर्ता की पहचान का अनुरोध करने के लिए कॉन्फ़िगर किया गया है. हालांकि, इसे लागू करने के तरीके को अब भी अपडेट करना होगा.

पहचान वाले टोकन को पार्स करना

प्रोजेक्ट में Google auth लाइब्रेरी जोड़कर शुरू करें:

npm install --save google-auth-library

इसके बाद, index.js में बदलाव करके OAuth2Client को ज़रूरी बनाएं:

const { OAuth2Client } = require('google-auth-library');

इसके बाद, आईडी टोकन को पार्स करने के लिए हेल्पर मेथड जोड़ें:

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

उपयोगकर्ता की पहचान दिखाना

टास्क की सूची से जुड़ी सभी सुविधाएं जोड़ने से पहले, यह एक अच्छा समय है. ऐप्लिकेशन के रूट को अपडेट करें, ताकि ‘Hello world.' के बजाय उपयोगकर्ता का ईमेल पता और यूनीक आईडी प्रिंट किया जा सके.

app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello ${user.email} ${user.sub}`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

इन बदलावों के बाद, index.js फ़ाइल ऐसी दिखनी चाहिए:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello ${user.email} ${user.sub}`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

फिर से डिप्लॉय करें और टेस्ट करें

ऐड-ऑन को फिर से बनाएं और फिर से डिप्लॉय करें. Cloud Shell में यह कमांड चलाएं:

gcloud builds submit

सर्वर को फिर से डिप्लॉय करने के बाद, Gmail खोलें या फिर से लोड करें. इसके बाद, ऐड-ऑन को फिर से खोलें. स्कोप बदल जाने की वजह से, ऐड-ऑन आपसे फिर से अनुमति मांगेगा. ऐड-ऑन को फिर से अनुमति दें. इसके बाद, ऐड-ऑन आपका ईमेल पता और उपयोगकर्ता आईडी दिखाएगा.

अब ऐड-ऑन को पता चल गया है कि उपयोगकर्ता कौन है. इसलिए, अब टास्क की सूची बनाने की सुविधा जोड़ी जा सकती है.

6. टास्क सूची लागू करना

कोड लैब के लिए शुरुआती डेटा मॉडल आसान है: Task इकाइयों की सूची, जिनमें से हर इकाई में टास्क के बारे में जानकारी देने वाले टेक्स्ट और टाइमस्टैंप के लिए प्रॉपर्टी होती हैं.

डेटास्टोर इंडेक्स बनाना

इस कोडलैब में, प्रोजेक्ट के लिए Datastore को पहले ही चालू कर दिया गया था. इसके लिए स्कीमा की ज़रूरत नहीं होती. हालांकि, इसमें कंपाउंड क्वेरी के लिए इंडेक्स बनाने पड़ते हैं. इंडेक्स बनाने में कुछ मिनट लग सकते हैं. इसलिए, सबसे पहले इंडेक्स बनाएं.

index.yaml नाम की फ़ाइल बनाएं और उसमें यह कॉन्टेंट जोड़ें:

indexes:
- kind: Task
  ancestor: yes
  properties:
  - name: created

इसके बाद, Datastore इंडेक्स अपडेट करें:

gcloud datastore indexes create index.yaml

जब जारी रखने के लिए कहा जाए, तब अपने कीबोर्ड पर ENTER दबाएं. इंडेक्स बनाने की प्रोसेस बैकग्राउंड में होती है. जब तक यह प्रोसेस पूरी हो रही है, तब तक ऐड-ऑन कोड को अपडेट करना शुरू करें, ताकि "todos" को लागू किया जा सके.

ऐड-ऑन के बैकएंड को अपडेट करना

प्रोजेक्ट में Datastore लाइब्रेरी इंस्टॉल करें:

npm install --save @google-cloud/datastore

Datastore में डेटा पढ़ने और लिखने की अनुमति

डेटास्टोर लाइब्रेरी इंपोर्ट करके और क्लाइंट बनाकर, "todos" को लागू करने के लिए index.js को अपडेट करें:

const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

Datastore से टास्क पढ़ने और लिखने के लिए, ये तरीके जोड़ें:

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

यूज़र इंटरफ़ेस (यूआई) को रेंडर करने की सुविधा लागू करना

ज़्यादातर बदलाव, ऐड-ऑन के यूज़र इंटरफ़ेस (यूआई) में किए गए हैं. पहले, यूज़र इंटरफ़ेस (यूआई) से मिले सभी कार्ड स्टैटिक होते थे. उपलब्ध डेटा के हिसाब से उनमें बदलाव नहीं होता था. यहां, कार्ड को उपयोगकर्ता की मौजूदा टास्क लिस्ट के आधार पर डाइनैमिक तरीके से बनाया जाना चाहिए.

कोड लैब के यूज़र इंटरफ़ेस (यूआई) में, टेक्स्ट इनपुट के साथ-साथ टास्क की सूची होती है. इसमें चेक बॉक्स होते हैं, ताकि टास्क को पूरा होने के तौर पर मार्क किया जा सके. इनमें से हर एक में onChangeAction प्रॉपर्टी भी होती है. जब उपयोगकर्ता कोई टास्क जोड़ता है या मिटाता है, तो इससे ऐड-ऑन सर्वर में कॉलबैक होता है. इन सभी मामलों में, अपडेट की गई टास्क लिस्ट के साथ यूज़र इंटरफ़ेस (यूआई) को फिर से रेंडर करना होगा. इसे मैनेज करने के लिए, हम कार्ड यूज़र इंटरफ़ेस (यूआई) बनाने का नया तरीका पेश करते हैं.

index.js में बदलाव करना जारी रखें और यह तरीका जोड़ें:

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    // Input for adding a new task
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        // Create text & checkbox for each task
        tasks.forEach(task => taskListSection.widgets.push({
            decoratedText: {
                text: task.text,
                wrapText: true,
                switchControl: {
                    controlType: 'CHECKBOX',
                    name: 'completedTasks',
                    value: task[datastore.KEY].id,
                    selected: false,
                    onChangeAction: {
                        function: `${baseUrl}/complete`,
                    }
                }
            }
        }));
    } else {
        // Placeholder for empty task list
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    }
    return card;
}

रास्तों की जानकारी अपडेट करना

अब Datastore से डेटा पढ़ने और उसमें डेटा लिखने के लिए हेल्पर तरीके मौजूद हैं. साथ ही, यूज़र इंटरफ़ेस (यूआई) बनाने के लिए भी हेल्पर तरीके मौजूद हैं. इसलिए, आइए अब इन्हें ऐप्लिकेशन के रास्तों में एक साथ जोड़ते हैं. मौजूदा रूट को बदलें और दो और रूट जोड़ें: एक टास्क जोड़ने के लिए और दूसरा उन्हें मिटाने के लिए.

app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date()
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

यहां पूरी तरह से काम करने वाली index.js फ़ाइल दी गई है:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date()
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    // Input for adding a new task
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        // Create text & checkbox for each task
        tasks.forEach(task => taskListSection.widgets.push({
            decoratedText: {
                text: task.text,
                wrapText: true,
                switchControl: {
                    controlType: 'CHECKBOX',
                    name: 'completedTasks',
                    value: task[datastore.KEY].id,
                    selected: false,
                    onChangeAction: {
                        function: `${baseUrl}/complete`,
                    }
                }
            }
        }));
    } else {
        // Placeholder for empty task list
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    }
    return card;
}

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

फिर से डिप्लॉय करें और टेस्ट करें

ऐड-ऑन को फिर से बनाने और फिर से डिप्लॉय करने के लिए, बिल्ड शुरू करें. Cloud Shell में यह कमांड चलाएं:

gcloud builds submit

Gmail में, ऐड-ऑन को फिर से लोड करें. इसके बाद, नया यूज़र इंटरफ़ेस (यूआई) दिखेगा. कुछ समय निकालकर ऐड-ऑन के बारे में जानें. कुछ टास्क जोड़ने के लिए, इनपुट में कुछ टेक्स्ट डालें और अपने कीबोर्ड पर ENTER दबाएं. इसके बाद, उन्हें मिटाने के लिए चेकबॉक्स पर क्लिक करें.

टास्क वाला ऐड-ऑन

अगर आपको यह चरण छोड़ना है, तो इस कोडलैब के आखिरी चरण पर जाएँ और अपने प्रोजेक्ट को क्लीन अप करें. इसके अलावा, अगर आपको ऐड-ऑन के बारे में ज़्यादा जानना है, तो एक और चरण पूरा किया जा सकता है.

7. (ज़रूरी नहीं) कॉन्टेक्स्ट जोड़ना

ऐड-ऑन की सबसे अहम सुविधाओं में से एक है कॉन्टेक्स्ट अवेयरनेस. उपयोगकर्ता की अनुमति से ऐड-ऑन, Google Workspace के कॉन्टेक्स्ट को ऐक्सेस कर सकते हैं. जैसे, उपयोगकर्ता जिस ईमेल को देख रहा है, कोई कैलेंडर इवेंट, और कोई दस्तावेज़. ऐड-ऑन, कॉन्टेंट डालने जैसी कार्रवाइयां भी कर सकते हैं. इस कोडलैब में, Workspace के एडिटर्स (Docs, Sheets, और Slides) के लिए कॉन्टेक्स्ट सपोर्ट जोड़ा जाएगा. इससे एडिटर्स में काम करते समय बनाए गए किसी भी टास्क में मौजूदा दस्तावेज़ को अटैच किया जा सकेगा. जब टास्क दिखता है, तो उस पर क्लिक करने से दस्तावेज़ एक नए टैब में खुल जाता है. इससे उपयोगकर्ता को दस्तावेज़ पर वापस जाने और अपना टास्क पूरा करने में मदद मिलती है.

ऐड-ऑन के बैकएंड को अपडेट करना

newTask का रूट अपडेट करना

सबसे पहले, /newTask रूट को अपडेट करें, ताकि अगर दस्तावेज़ का आईडी उपलब्ध हो, तो उसे टास्क में शामिल किया जा सके:

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    // Get the current document if it is present
    const editorInfo = event.docs || event.sheets || event.slides;
    let document = null;
    if (editorInfo && editorInfo.id) {
        document = {
            id: editorInfo.id,
        }
    }
    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date(),
        document,
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

अब नए बनाए गए टास्क में, मौजूदा दस्तावेज़ का आईडी शामिल होता है. हालांकि, एडिटर में कॉन्टेक्स्ट को डिफ़ॉल्ट रूप से शेयर नहीं किया जाता है. उपयोगकर्ता के अन्य डेटा की तरह ही, इस डेटा को ऐक्सेस करने के लिए भी उपयोगकर्ता को ऐड-ऑन को अनुमति देनी होगी. ज़्यादा जानकारी शेयर करने से बचने के लिए, हर फ़ाइल के हिसाब से अनुमति का अनुरोध करना और अनुमति देना बेहतर तरीका है.

यूज़र इंटरफ़ेस (यूआई) अपडेट करना

index.js में, दो बदलाव करने के लिए buildCard को अपडेट करें. पहला बदलाव, टास्क को रेंडर करने के तरीके में किया गया है. अगर कोई दस्तावेज़ मौजूद है, तो उसमें उसका लिंक शामिल किया जाएगा. दूसरा, अगर ऐड-ऑन को एडिटर में रेंडर किया जाता है और फ़ाइल का ऐक्सेस अब तक नहीं दिया गया है, तो अनुमति लेने के लिए एक प्रॉम्प्ट दिखाना.

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        tasks.forEach(task => {
            const widget = {
                decoratedText: {
                    text: task.text,
                    wrapText: true,
                    switchControl: {
                        controlType: 'CHECKBOX',
                        name: 'completedTasks',
                        value: task[datastore.KEY].id,
                        selected: false,
                        onChangeAction: {
                            function: `${baseUrl}/complete`,
                        }
                    }
                }
            };
            // Make item clickable and open attached doc if present
            if (task.document) {
                widget.decoratedText.bottomLabel = 'Click to open  document.';
                const id = task.document.id;
                const url = `https://drive.google.com/open?id=${id}`
                widget.decoratedText.onClick = {
                    openLink: {
                        openAs: 'FULL_SIZE',
                        onClose: 'NOTHING',
                        url: url,
                    }
                }
            }
            taskListSection.widgets.push(widget)
        });
    } else {
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    };

    // Display file authorization prompt if the host is an editor
    // and no doc ID present
    const event = req.body;
    const editorInfo = event.docs || event.sheets || event.slides;
    const showFileAuth = editorInfo && editorInfo.id === undefined;
    if (showFileAuth) {
        card.fixedFooter = {
            primaryButton: {
                text: 'Authorize file access',
                onClick: {
                    action: {
                        function: `${baseUrl}/authorizeFile`,
                    }
                }
            }
        }
    }
    return card;
}

फ़ाइल ऐक्सेस करने की अनुमति देने का तरीका लागू करना

अनुमति देने वाले बटन से ऐप्लिकेशन में एक नया रूट जुड़ जाता है. इसलिए, इसे लागू करते हैं. इस रूट में, होस्ट ऐप्लिकेशन की कार्रवाइयों के बारे में बताया गया है. ये ऐड-ऑन के होस्ट ऐप्लिकेशन के साथ इंटरैक्ट करने के लिए खास निर्देश हैं. इस मामले में, मौजूदा एडिटर फ़ाइल का ऐक्सेस पाने का अनुरोध करने के लिए.

index.js में, /authorizeFile रूट जोड़ें:

app.post('/authorizeFile', asyncHandler(async (req, res) => {
    const responsePayload = {
        renderActions: {
            hostAppAction: {
                editorAction: {
                    requestFileScopeForActiveDocument: {}
                }
            },
        }
    };
    res.json(responsePayload);
}));

यहां पूरी तरह से काम करने वाली index.js फ़ाइल दी गई है:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    // Get the current document if it is present
    const editorInfo = event.docs || event.sheets || event.slides;
    let document = null;
    if (editorInfo && editorInfo.id) {
        document = {
            id: editorInfo.id,
        }
    }
    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date(),
        document,
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/authorizeFile', asyncHandler(async (req, res) => {
    const responsePayload = {
        renderActions: {
            hostAppAction: {
                editorAction: {
                    requestFileScopeForActiveDocument: {}
                }
            },
        }
    };
    res.json(responsePayload);
}));

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        tasks.forEach(task => {
            const widget = {
                decoratedText: {
                    text: task.text,
                    wrapText: true,
                    switchControl: {
                        controlType: 'CHECKBOX',
                        name: 'completedTasks',
                        value: task[datastore.KEY].id,
                        selected: false,
                        onChangeAction: {
                            function: `${baseUrl}/complete`,
                        }
                    }
                }
            };
            // Make item clickable and open attached doc if present
            if (task.document) {
                widget.decoratedText.bottomLabel = 'Click to open  document.';
                const id = task.document.id;
                const url = `https://drive.google.com/open?id=${id}`
                widget.decoratedText.onClick = {
                    openLink: {
                        openAs: 'FULL_SIZE',
                        onClose: 'NOTHING',
                        url: url,
                    }
                }
            }
            taskListSection.widgets.push(widget)
        });
    } else {
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    };

    // Display file authorization prompt if the host is an editor
    // and no doc ID present
    const event = req.body;
    const editorInfo = event.docs || event.sheets || event.slides;
    const showFileAuth = editorInfo && editorInfo.id === undefined;
    if (showFileAuth) {
        card.fixedFooter = {
            primaryButton: {
                text: 'Authorize file access',
                onClick: {
                    action: {
                        function: `${baseUrl}/authorizeFile`,
                    }
                }
            }
        }
    }
    return card;
}

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

डिप्लॉयमेंट डिस्क्रिप्टर में स्कोप जोड़ना

सर्वर को फिर से बनाने से पहले, ऐड-ऑन के डिप्लॉयमेंट डिस्क्रिप्टर को अपडेट करें, ताकि उसमें https://www.googleapis.com/auth/drive.file OAuth स्कोप शामिल हो. OAuth के स्कोप की सूची में https://www.googleapis.com/auth/drive.file जोड़ने के लिए, deployment.json को अपडेट करें:

"oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/calendar.addons.execute",
    "https://www.googleapis.com/auth/drive.file",
    "openid",
    "email"
]

Cloud Shell में यह निर्देश चलाकर, नया वर्शन अपलोड करें:

gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json

फिर से डिप्लॉय करें और टेस्ट करें

आखिर में, सर्वर को फिर से बनाया जाता है. Cloud Shell में यह कमांड चलाएं:

gcloud builds submit

प्रोसेस पूरी होने के बाद, Gmail खोलने के बजाय, कोई मौजूदा Google दस्तावेज़ खोलें या doc.new खोलकर नया दस्तावेज़ बनाएं. अगर कोई नया दस्तावेज़ बनाया जा रहा है, तो उसमें कुछ टेक्स्ट ज़रूर डालें या फ़ाइल का नाम दें.

ऐड-ऑन खोलें. ऐड-ऑन में सबसे नीचे, फ़ाइल ऐक्सेस करने की अनुमति दें बटन दिखता है. बटन पर क्लिक करें. इसके बाद, फ़ाइल का ऐक्सेस दें.

अनुमति देने के बाद, एडिटर में रहते हुए कोई टास्क जोड़ें. टास्क में एक लेबल होता है. इससे पता चलता है कि दस्तावेज़ अटैच किया गया है. लिंक पर क्लिक करने से, दस्तावेज़ नए टैब में खुल जाता है. ज़ाहिर है, पहले से खुले हुए दस्तावेज़ को दोबारा खोलना थोड़ा अजीब है. अगर आपको मौजूदा दस्तावेज़ के लिंक को फ़िल्टर करने के लिए यूज़र इंटरफ़ेस (यूआई) को ऑप्टिमाइज़ करना है, तो यह काम करके आपको अतिरिक्त क्रेडिट मिलेगा!

8. बधाई हो

बधाई हो! आपने Cloud Run का इस्तेमाल करके, Google Workspace ऐड-ऑन को बना लिया है और उसे डिप्लॉय कर दिया है. इस कोडलैब में, ऐड-ऑन बनाने के लिए कई मुख्य कॉन्सेप्ट शामिल किए गए हैं. हालांकि, एक्सप्लोर करने के लिए और भी बहुत कुछ है. यहां दिए गए संसाधन देखें. साथ ही, अतिरिक्त शुल्क से बचने के लिए अपने प्रोजेक्ट को बंद करना न भूलें.

व्यवस्थित करें

अपने खाते से ऐड-ऑन को अनइंस्टॉल करने के लिए, Cloud Shell में यह कमांड चलाएं:

gcloud workspace-add-ons deployments uninstall todo-add-on

इस ट्यूटोरियल में इस्तेमाल किए गए संसाधनों के लिए, अपने Google Cloud Platform खाते से शुल्क न लिए जाने के लिए:

  • Cloud Console में, संसाधन मैनेज करें पेज पर जाएं. सबसे ऊपर बाएं कोने में मौजूद, मेन्यू मेन्यू आइकॉन > IAM और एडमिन > संसाधन मैनेज करें पर क्लिक करें.
  1. प्रोजेक्ट की सूची में, अपना प्रोजेक्ट चुनें. इसके बाद, मिटाएं पर क्लिक करें.
  2. डायलॉग बॉक्स में, प्रोजेक्ट आईडी टाइप करें. इसके बाद, प्रोजेक्ट मिटाने के लिए बंद करें पर क्लिक करें.

ज़्यादा जानें

  • Google Workspace Add-ons के बारे में खास जानकारी
  • Marketplace में जाकर, मौजूदा ऐप्लिकेशन और ऐड-ऑन ढूंढना