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 के नए उपयोगकर्ता, 300USD डॉलर के मुफ़्त में आज़माने वाले प्रोग्राम में हिस्सा ले सकते हैं.

Google Cloud शेल

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

Cloud Shell चालू करें

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

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

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

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

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

द क्लाउड शेल टर्मिनल

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

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

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

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

कमांड आउटपुट

 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 ऐड-ऑन" डालें.
    • उपयोगकर्ता सहायता ईमेल में, अपना निजी ईमेल पता डालें.
    • डेवलपर की संपर्क जानकारी में जाकर, अपना निजी ईमेल पता डालें.
  9. सेव करें और जारी रखें पर क्लिक करें. एक स्कोप फ़ॉर्म दिखेगा.
  10. दायरा फ़ॉर्म में, सेव करें और जारी रखें पर क्लिक करें. आपको खास जानकारी दिखेगी.
  11. डैशबोर्ड पर वापस जाएं पर क्लिक करें.

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

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

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

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

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

इस डायरेक्ट्री में कोडलैब का पूरा काम आपको करना होगा.

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

npm init

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

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

npm install --save express express-async-handler

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

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

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

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}`)
});

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

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 का इस्तेमाल करके, प्रोसेस को व्यवस्थित करें. ऐप्लिकेशन बनाने और डिप्लॉय करने के तरीके से जुड़े निर्देशों के साथ 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

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

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

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

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

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

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

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

अनुमति देने का अनुरोध

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

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

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 की पुष्टि करने वाली लाइब्रेरी जोड़कर शुरू करें:

npm install --save google-auth-library

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

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();
}

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

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

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 दबाएं. इंडेक्स बनाने की प्रोसेस बैकग्राउंड में होती है. इस प्रोसेस के दौरान, "काम की सूची" लागू करने के लिए, ऐड-ऑन कोड को अपडेट करना शुरू करें.

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

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

npm install --save @google-cloud/datastore

Datastore में पढ़ें और लिखें

डेटास्टोर लाइब्रेरी इंपोर्ट करने और क्लाइंट बनाने से शुरू होने वाली "काम की सूची" लागू करने के लिए, 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 के कॉन्टेक्स्ट को ऐक्सेस कर सकते हैं. जैसे- उपयोगकर्ता की ओर से देखा जा रहा ईमेल, कैलेंडर इवेंट, और दस्तावेज़. ऐड-ऑन, कॉन्टेंट डालने जैसी कार्रवाइयां भी कर सकते हैं. इस कोडलैब में, Docs, Sheets, और Slides जैसे Workspace एडिटर्स के लिए कॉन्टेक्स्ट सपोर्ट जोड़ा जा सकता है. इससे एडिटर के ज़रिए बनाए गए किसी भी टास्क के साथ मौजूदा दस्तावेज़ अटैच किया जा सकता है. टास्क के दिखने पर, उस पर क्लिक करने से दस्तावेज़ एक नए टैब में खुल जाएगा. इससे उपयोगकर्ता को टास्क पूरा करने के लिए, दस्तावेज़ पर वापस ले जाया जाएगा.

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

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. प्रोजेक्ट की सूची में, अपना प्रोजेक्ट चुनें. इसके बाद, Delete पर क्लिक करें.
  2. डायलॉग बॉक्स में, प्रोजेक्ट आईडी टाइप करें. इसके बाद, प्रोजेक्ट मिटाने के लिए शट डाउन करें पर क्लिक करें.

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