1. Giriş
Google Workspace eklentileri, Gmail, Dokümanlar, E-Tablolar ve Slaytlar gibi Google Workspace uygulamalarıyla entegre olan özelleştirilmiş uygulamalardır. Bu API'ler, geliştiricilerin doğrudan Google Workspace'e entegre edilmiş özelleştirilmiş kullanıcı arayüzleri oluşturmasına olanak tanır. Eklentiler, kullanıcıların bağlamlar arasında daha az geçiş yaparak daha verimli çalışmasına yardımcı olur.
Bu codelab'de Node.js, Cloud Run ve Datastore kullanarak basit bir görev listesi eklentisi oluşturmayı ve dağıtmayı öğreneceksiniz.
Neler öğreneceksiniz?
- Cloud Shell'i kullanma
- Cloud Run'a dağıt
- Eklenti dağıtım tanımlayıcısı oluşturma ve dağıtma
- Kart çerçevesiyle eklenti kullanıcı arayüzleri oluşturma
- Kullanıcı etkileşimlerine yanıt verme
- Eklentide kullanıcı bağlamından yararlanma
2. Kurulum ve şartlar
Google Cloud projesi oluşturmak ve eklentinin kullanacağı API'leri ve hizmetleri etkinleştirmek için kurulum talimatlarını uygulayın.
Yönlendirmesiz ortam kurulumu
- Cloud Console'u açın ve yeni bir proje oluşturun. (Gmail veya Google Workspace hesabınız yoksa oluşturun.)
Proje kimliğini unutmayın. Bu kimlik, tüm Google Cloud projelerinde benzersiz bir addır (Yukarıdaki ad zaten alınmış olduğundan sizin için çalışmayacaktır). Bu codelab'in ilerleyen kısımlarında PROJECT_ID olarak adlandırılacaktır.
- Ardından, Google Cloud kaynaklarını kullanmak için Cloud Console'da faturalandırmayı etkinleştirin.
Bu codelab'i tamamlamak neredeyse hiç maliyetli değildir. Bu eğitimin ötesinde faturalandırma ücreti alınmaması için kaynakları nasıl kapatacağınız konusunda size tavsiyelerde bulunan codelab'in sonundaki"Temizleme" bölümündeki talimatları uyguladığınızdan emin olun. Google Cloud'un yeni kullanıcıları 300 ABD doları değerinde ücretsiz deneme programından yararlanabilir.
Google Cloud Shell
Google Cloud, dizüstü bilgisayarınızdan uzaktan çalıştırılabilir. Ancak bu codelab'de, bulutta çalışan bir komut satırı ortamı olan Google Cloud Shell'i kullanacağız.
Cloud Shell'i etkinleştirme
- Cloud Console'da Cloud Shell'i etkinleştir 'i
tıklayın.
Cloud Shell'i ilk kez açtığınızda açıklayıcı bir karşılama mesajı gösterilir. Karşılama mesajını görürseniz Devam'ı tıklayın. Karşılama mesajı tekrar görünmez. Karşılama mesajı:
Cloud Shell'in temel hazırlığı ve bağlanması yalnızca birkaç dakikanızı alır. Bağlantı kurulduktan sonra Cloud Shell Terminali'ni görürsünüz:
Bu sanal makine, ihtiyaç duyduğunuz tüm geliştirme araçlarını içerir. 5 GB boyutunda kalıcı bir ana dizin bulunur ve Google Cloud'da çalışır. Bu sayede ağ performansı ve kimlik doğrulama önemli ölçüde güçlenir. Bu codelab'deki tüm çalışmalarınızı tarayıcı veya Chromebook'unuzla yapabilirsiniz.
Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin, proje kimliğinize ayarlandığını görürsünüz.
- Kimliğinizin doğrulandığını onaylamak için Cloud Shell'de şu komutu çalıştırın:
gcloud auth list
Cloud Shell'in GCP API çağrısı yapması için yetki vermeniz istenirse Yetkilendir'i tıklayın.
Komut çıkışı
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
Etkin hesabı ayarlamak için şu komutu çalıştırın:
gcloud config set account <ACCOUNT>
Doğru projeyi seçtiğinizi onaylamak için Cloud Shell'de şu komutu çalıştırın:
gcloud config list project
Komut çıkışı
[core] project = <PROJECT_ID>
Doğru proje döndürülmüyorsa şu komutla ayarlayabilirsiniz:
gcloud config set project <PROJECT_ID>
Komut çıkışı
Updated property [core/project].
Bu codelab'de hem komut satırı işlemleri hem de dosya düzenleme kullanılır. Dosya düzenleme için Cloud Shell araç çubuğunun sağ tarafındaki Open Editor (Düzenleyiciyi Aç) düğmesini tıklayarak Cloud Shell'deki yerleşik kod düzenleyiciyi kullanabilirsiniz. Ayrıca, Cloud Shell'de vim ve emacs gibi popüler düzenleyicileri de bulabilirsiniz.
3. Cloud Run, Datastore ve eklenti API'lerini etkinleştirme
Cloud API'lerini etkinleştirme
Cloud Shell'den, kullanılacak bileşenler için Cloud API'lerini etkinleştirin:
gcloud services enable \ run.googleapis.com \ cloudbuild.googleapis.com \ cloudresourcemanager.googleapis.com \ datastore.googleapis.com \ gsuiteaddons.googleapis.com
Bu işlemin tamamlanması birkaç dakika sürebilir.
İşlem tamamlandığında şuna benzer bir başarı mesajı gösterilir:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
Veri deposu örneği oluşturma
Ardından App Engine'i etkinleştirin ve Datastore veritabanı oluşturun. Datastore'u kullanmak için App Engine'in etkinleştirilmesi gerekir ancak App Engine başka amaçlarla kullanılmaz.
gcloud app create --region=us-central gcloud firestore databases create --type=datastore-mode --region=us-central
OAuth kullanıcı rızası ekranı oluşturma
Eklentinin çalışması ve veriler üzerinde işlem yapması için kullanıcı izni gerekir. Bunu etkinleştirmek için projenin kullanıcı rızası ekranını yapılandırın. Codelab'de, başlamak için kullanıcı rızası ekranını herkese açık dağıtım için olmayan bir dahili uygulama olarak yapılandıracaksınız.
- Google Cloud Console'u yeni bir sekmede veya pencerede açın.
- "Google Cloud Console"un yanındaki aşağı oku
tıklayın ve projenizi seçin. - Sol üst köşedeki Menü simgesini
tıklayın. - API'ler ve Hizmetler > Kimlik bilgileri'ni tıklayın. Projenizin kimlik bilgisi sayfası görünür.
- OAuth izin ekranı'nı tıklayın. "OAuth kullanıcı rızası ekranı" gösterilir.
- "Kullanıcı Türü" bölümünde Dahili'yi seçin. @gmail.com hesabı kullanıyorsanız Harici'yi seçin.
- Oluştur'u tıklayın. "Uygulama kaydını düzenle" sayfası görünür.
- Formu doldurun:
- Uygulama adı bölümüne "Todo Add-on" yazın.
- Kullanıcı destek e-postası bölümüne kişisel e-posta adresinizi girin.
- Geliştirici iletişim bilgileri bölümünde kişisel e-posta adresinizi girin.
- Kaydet ve Devam Et'i tıklayın. Bir Kapsamlar formu gösterilir.
- Kapsamlar formunda Kaydet ve Devam Et'i tıklayın. Bir özet gösterilir.
- Kontrol paneline dön'ü tıklayın.
4. İlk eklentiyi oluşturma
Projeyi başlatma
Başlamak için basit bir "Hello world" eklentisi oluşturup dağıtacaksınız. Eklentiler, https isteklerine yanıt veren ve kullanıcı arayüzünü ve yapılacak işlemleri açıklayan bir JSON yüküyle yanıt veren web hizmetleridir. Bu eklentide Node.js ve Express çerçevesini kullanacaksınız.
Bu şablon projesini oluşturmak için Cloud Shell'i kullanarak todo-add-on adlı yeni bir dizin oluşturun ve bu dizine gidin:
mkdir ~/todo-add-on cd ~/todo-add-on
Bu codelab'deki tüm işlemleri bu dizinde yapacaksınız.
Node.js projesini başlatın:
npm init
NPM, proje yapılandırmasıyla ilgili ad ve sürüm gibi çeşitli sorular sorar. Her soru için ENTER tuşuna basarak varsayılan değerleri kabul edin. Varsayılan giriş noktası, bir sonraki adımda oluşturacağımız index.js adlı dosyadır.
Ardından, Express web çerçevesini yükleyin:
npm install --save express express-async-handler
Eklenti arka ucunu oluşturma
Uygulamayı oluşturmaya başlama zamanı.
index.js adlı bir dosya oluşturun. Dosya oluşturmak için Cloud Shell penceresinin araç çubuğundaki Open Editor (Düzenleyiciyi Aç) düğmesini tıklayarak Cloud Shell Düzenleyici'yi kullanabilirsiniz. Alternatif olarak, vim veya emacs kullanarak Cloud Shell'deki dosyaları düzenleyip yönetebilirsiniz.
index.js dosyasını oluşturduktan sonra aşağıdaki içeriği ekleyin:
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}`)
});
Sunucu, "Merhaba dünya" mesajını göstermek dışında pek bir şey yapmaz ve bu da sorun değildir. Daha sonra başka işlevler ekleyebilirsiniz.
Cloud Run'a dağıt
Cloud Run'a dağıtmak için uygulamanın container mimarisine alınması gerekir.
Kapsayıcıyı oluşturma
Dockerfile adlı bir Dockerfile oluşturun. Bu dosya şunları içermelidir:
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" ]
İstenmeyen dosyaları kapsayıcının dışında tutma
Kapsayıcının hafif kalmasına yardımcı olmak için aşağıdakileri içeren bir .dockerignore dosyası oluşturun:
Dockerfile
.dockerignore
node_modules
npm-debug.log
Cloud Build'i etkinleştirme
Bu codelab'de, yeni işlevler eklendikçe eklentiyi birkaç kez oluşturup dağıtacaksınız. Container'ı oluşturmak, container kayıt defterine aktarmak ve Cloud Build'e dağıtmak için ayrı komutlar çalıştırmak yerine, prosedürü düzenlemek için Cloud Build'i kullanın. Uygulamanın nasıl oluşturulup dağıtılacağıyla ilgili talimatların yer aldığı bir cloudbuild.yaml dosyası oluşturun:
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'e uygulamayı dağıtma izni vermek için aşağıdaki komutları çalıştırın:
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
Eklenti arka ucunu oluşturma ve dağıtma
Derlemeyi başlatmak için Cloud Shell'de şu komutu çalıştırın:
gcloud builds submit
Tam derleme ve dağıtım işleminin tamamlanması birkaç dakika sürebilir. Bu durum özellikle ilk kez yapıldığında geçerlidir.
Derleme tamamlandıktan sonra hizmetin dağıtıldığını doğrulayın ve URL'yi bulun. Komutu çalıştırın:
gcloud run services list --platform managed
Bu URL'yi kopyalayın. Google Workspace'e eklentinin nasıl çağrılacağını bildirmek için bu URL'ye ihtiyacınız olacak.
Eklentiyi kaydetme
Sunucu çalışır duruma geldiğine göre, Google Workspace'in eklentiyi nasıl görüntüleyeceğini ve çağıracağını bilmesi için eklentiyi açıklayın.
Dağıtım tanımlayıcısı oluşturma
Aşağıdaki içeriğe sahip deployment.json dosyasını oluşturun. URL yer tutucusu yerine dağıtılan uygulamanın URL'sini kullandığınızdan emin olun.
{
"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": {}
}
}
Şu komutu çalıştırarak dağıtım açıklayıcısını yükleyin:
gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json
Eklenti arka ucuna erişimi yetkilendirme
Eklenti çerçevesinin de hizmeti çağırmak için izne ihtiyacı vardır. Google Workspace'in eklentiyi çağırmasına izin vermek için Cloud Run'ın IAM politikasını güncellemek üzere aşağıdaki komutları çalıştırın:
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"
Test için eklentiyi yükleme
Eklentiyi hesabınız için geliştirme modunda yüklemek üzere Cloud Shell'de şu komutu çalıştırın:
gcloud workspace-add-ons deployments install todo-add-on
Yeni bir sekmede veya pencerede (Gmail)[https://mail.google.com/] adresini açın. Sağ tarafta, onay işareti simgesi olan eklentiyi bulun.

Eklentiyi açmak için onay işareti simgesini tıklayın. Eklentiyi yetkilendirme istemi gösterilir.

Erişimi Yetkilendir'i tıklayın ve açılır penceredeki yetkilendirme akışı talimatlarını uygulayın. Tamamlandığında eklenti otomatik olarak yeniden yüklenir ve "Merhaba dünya!" mesajını gösterir.
Tebrikler! Artık basit bir eklentiniz dağıtılmış ve yüklenmiş durumda. Artık onu bir görev listesi uygulamasına dönüştürme zamanı!
5. Kullanıcı kimliğine erişme
Eklentiler genellikle birçok kullanıcı tarafından kendilerine veya kuruluşlarına ait özel bilgilerle çalışmak için kullanılır. Bu codelab'de, eklenti yalnızca mevcut kullanıcının görevlerini göstermelidir. Kullanıcı kimliği, çözülmesi gereken bir kimlik jetonu aracılığıyla eklentiye gönderilir.
Dağıtım tanımlayıcısına kapsam ekleme
Kullanıcı kimliği varsayılan olarak gönderilmez. Bu, kullanıcı verileridir ve eklentinin bu verilere erişmek için izne ihtiyacı vardır. Bu izni almak için deployment.json öğesini güncelleyin ve eklentinin gerektirdiği kapsamlar listesine openid ve email OAuth kapsamlarını ekleyin. OAuth kapsamları eklendikten sonra, eklenti kullanıcıları bir sonraki kullanımda erişim izni vermeye yönlendirir.
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"openid",
"email"
],
Ardından, dağıtım tanımlayıcısını güncellemek için Cloud Shell'de şu komutu çalıştırın:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Eklenti sunucusunu güncelleme
Eklenti, kullanıcı kimliğini isteyecek şekilde yapılandırılmış olsa da uygulamanın güncellenmesi gerekir.
Kimlik jetonunu ayrıştırma
Google kimlik doğrulama kitaplığını projeye ekleyerek başlayın:
npm install --save google-auth-library
Ardından, index.js değerini OAuth2Client gerektirecek şekilde düzenleyin:
const { OAuth2Client } = require('google-auth-library');
Ardından, kimlik jetonunu ayrıştırmak için bir yardımcı yöntem ekleyin:
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
Kullanıcı kimliğini görüntüleme
Görev listesi işlevlerinin tamamını eklemeden önce kontrol noktası oluşturmak için uygun bir zamandır. Uygulamanın rotasını, "Merhaba dünya" yerine kullanıcının e-posta adresini ve benzersiz kimliğini yazdıracak şekilde güncelleyin.
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);
}));
Bu değişikliklerden sonra ortaya çıkan index.js dosyası şu şekilde görünmelidir:
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}`)
});
Yeniden dağıtma ve test etme
Eklentiyi yeniden oluşturup yeniden dağıtın. Cloud Shell'de şu komutu çalıştırın:
gcloud builds submit
Sunucu yeniden dağıtıldıktan sonra Gmail'i açın veya yeniden yükleyin ve eklentiyi tekrar açın. Kapsamlar değiştiği için eklenti yeniden yetkilendirme isteğinde bulunur. Eklentiyi tekrar yetkilendirin. İşlem tamamlandığında eklenti, e-posta adresinizi ve kullanıcı kimliğinizi gösterir.
Eklenti artık kullanıcının kim olduğunu bildiğine göre görev listesi işlevini eklemeye başlayabilirsiniz.
6. Görev listesini uygulama
Codelab'in ilk veri modeli basittir: Her biri görev açıklayıcı metni ve zaman damgası özelliklerine sahip Task öğelerinin listesi.
Veri deposu dizinini oluşturma
Datastore, codelab'in önceki bölümlerinde proje için etkinleştirilmişti. Şema gerektirmez ancak bileşik sorgular için dizinlerin açıkça oluşturulması gerekir. Dizin oluşturma işlemi birkaç dakika sürebilir. Bu nedenle, önce dizini oluşturmanız gerekir.
Aşağıdaki bilgileri içeren index.yaml adlı bir dosya oluşturun:
indexes:
- kind: Task
ancestor: yes
properties:
- name: created
Ardından Datastore dizinlerini güncelleyin:
gcloud datastore indexes create index.yaml
Devam etmeniz istendiğinde klavyenizde ENTER tuşuna basın. Dizin oluşturma işlemi arka planda gerçekleşir. Bu sırada, "todos"u uygulamak için eklenti kodunu güncellemeye başlayın.
Eklenti arka ucunu güncelleme
Datastore kitaplığını projeye yükleyin:
npm install --save @google-cloud/datastore
Datastore'da okuma ve yazma
Veri deposu kitaplığını içe aktarıp istemciyi oluşturarak başlayarak "todos"u uygulamak için index.js dosyasını güncelleyin:
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
Datastore'dan görev okuma ve yazma yöntemleri ekleyin:
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);
}
Kullanıcı arayüzünün oluşturulmasını uygulama
Değişikliklerin çoğu eklenti kullanıcı arayüzünde yapıldı. Daha önce, kullanıcı arayüzü tarafından döndürülen tüm kartlar statikti ve mevcut verilere göre değişmiyordu. Burada kartın, kullanıcının mevcut görev listesine göre dinamik olarak oluşturulması gerekir.
Codelab'in kullanıcı arayüzü, bir metin girişinin yanı sıra tamamlandı olarak işaretlemek için onay kutularının bulunduğu bir görev listesinden oluşur. Bunların her birinde, kullanıcı bir görev eklediğinde veya sildiğinde eklenti sunucusuna geri çağırma işlemi yapılmasına neden olan bir onChangeAction özelliği de bulunur. Bu durumların her birinde, kullanıcı arayüzünün güncellenmiş görev listesiyle yeniden oluşturulması gerekir. Bunu ele almak için kart kullanıcı arayüzü oluşturmaya yönelik yeni bir yöntem sunalım.
index.js düzenlemeye devam edin ve aşağıdaki yöntemi ekleyin:
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;
}
Rotaları güncelleme
Artık Datastore'a okuma ve yazma işlemleri yapmaya, kullanıcı arayüzü oluşturmaya yardımcı yöntemler olduğuna göre bunları uygulama rotalarında birleştirelim. Mevcut rotayı değiştirin ve iki tane daha ekleyin: biri görev eklemek, diğeri ise görev silmek için.
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);
}));
Son ve tamamen işlevsel index.js dosyası aşağıdadır:
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}`)
});
Yeniden dağıtma ve test etme
Eklentiyi yeniden oluşturup yeniden dağıtmak için derleme başlatın. Cloud Shell'de şunu çalıştırın:
gcloud builds submit
Gmail'de eklentiyi yeniden yüklediğinizde yeni kullanıcı arayüzü görünür. Bir dakikanızı ayırarak eklentiyi inceleyin. Girişe metin girip klavyenizde ENTER tuşuna basarak birkaç görev ekleyin, ardından bunları silmek için onay kutusunu tıklayın.

İsterseniz bu codelab'deki son adıma geçip projenizi temizleyebilirsiniz. Eklentiler hakkında daha fazla bilgi edinmek isterseniz tamamlayabileceğiniz bir adım daha var.
7. (İsteğe bağlı) Bağlam bilgisi ekleme
Eklentilerin en güçlü özelliklerinden biri bağlama duyarlı olmasıdır. Eklentiler, kullanıcı izniyle Google Workspace bağlamlarına (ör. kullanıcının baktığı e-posta, takvim etkinliği ve doküman) erişebilir. Eklentiler, içerik ekleme gibi işlemler de yapabilir. Bu codelab'de, Workspace düzenleyicilerinde (Dokümanlar, E-Tablolar ve Slaytlar) oluşturulan görevlere mevcut dokümanı eklemek için Workspace düzenleyicilerine bağlam desteği ekleyeceksiniz. Görev gösterildiğinde tıklanırsa belge yeni bir sekmede açılır ve kullanıcı görevi tamamlamak için belgeye geri döner.
Eklenti arka ucunu güncelleme
newTask rotasını güncelleme
Öncelikle, /newTask rotasını güncelleyerek varsa doküman kimliğini göreve ekleyin:
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);
}));
Yeni oluşturulan görevler artık mevcut doküman kimliğini içeriyor. Ancak, düzenleyicilerdeki bağlam varsayılan olarak paylaşılmaz. Diğer kullanıcı verilerinde olduğu gibi, kullanıcının eklentinin verilere erişmesi için izin vermesi gerekir. Bilgilerin aşırı paylaşılmasını önlemek için tercih edilen yaklaşım, dosya bazında izin istemek ve vermektir.
Kullanıcı arayüzünü güncelleme
index.js bölümünde, iki değişiklik yapmak için buildCard öğesini güncelleyin. İlk olarak, görevlerin oluşturulmasını güncelleyerek varsa dokümana bağlantı ekliyoruz. İkincisi ise eklenti bir düzenleyicide oluşturuluyorsa ve dosya erişimi henüz verilmemişse isteğe bağlı bir yetkilendirme istemi göstermektir.
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;
}
Dosya yetkilendirme rotasını uygulama
Yetkilendirme düğmesi uygulamaya yeni bir rota ekler. Bu nedenle, düğmeyi uygulayalım. Bu rota, yeni bir kavram olan barındırıcı uygulama işlemlerini tanıtır. Bunlar, eklentinin barındırıldığı uygulamayla etkileşim kurmaya yönelik özel talimatlardır. Bu durumda, mevcut düzenleyici dosyasına erişim isteğinde bulunmak için kullanılır.
index.js bölümünde /authorizeFile rotasını ekleyin:
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
Son ve tamamen işlevsel index.js dosyası aşağıdadır:
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}`)
});
Dağıtım tanımlayıcısına kapsam ekleme
Sunucuyu yeniden oluşturmadan önce, eklenti dağıtım tanımlayıcısını https://www.googleapis.com/auth/drive.file OAuth kapsamını içerecek şekilde güncelleyin. deployment.json öğesini güncelleyerek OAuth kapsamları listesine https://www.googleapis.com/auth/drive.file öğesini ekleyin:
"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"
]
Bu Cloud Shell komutunu çalıştırarak yeni sürümü yükleyin:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
Yeniden dağıtma ve test etme
Son olarak, sunucuyu yeniden oluşturun. Cloud Shell'de şu komutu çalıştırın:
gcloud builds submit
İşlem tamamlandıktan sonra Gmail'i açmak yerine mevcut bir Google dokümanını açın veya doc.new adresini açarak yeni bir doküman oluşturun. Yeni bir doküman oluşturuyorsanız metin girin veya dosyaya ad verin.
Eklentiyi açın. Eklentinin alt kısmında Dosya Erişimine İzin Ver düğmesi gösterilir. Düğmeyi tıklayın, ardından dosyaya erişimi yetkilendirin.
Yetkilendirildikten sonra düzenleyicideyken görev ekleyin. Görevde, dokümanın eklendiğini belirten bir etiket bulunur. Bağlantıyı tıkladığınızda doküman yeni bir sekmede açılır. Elbette, zaten açık olan dokümanı açmak biraz anlamsızdır. Kullanıcı arayüzünü mevcut dokümandaki bağlantıları filtreleyecek şekilde optimize etmek isterseniz bu da ekstra puan kazandırır.
8. Tebrikler
Tebrikler! Cloud Run'ı kullanarak bir Google Workspace eklentisi oluşturup dağıttınız. Bu codelab'de eklenti oluşturmayla ilgili birçok temel kavram ele alınsa da keşfedebileceğiniz daha pek çok şey var. Aşağıdaki kaynaklara göz atın ve ek ücretlerden kaçınmak için projenizi temizlemeyi unutmayın.
Temizleme
Eklentiyi hesabınızdan kaldırmak için Cloud Shell'de şu komutu çalıştırın:
gcloud workspace-add-ons deployments uninstall todo-add-on
Bu eğiticide kullanılan kaynaklar için Google Cloud Platform hesabınızın ücretlendirilmesini istemiyorsanız şunları yapın:
- Cloud Console'da Kaynakları yönetin sayfasına gidin. Sol üst köşede Menü
> IAM ve Yönetici > Kaynakları Yönet'i tıklayın.
- Proje listesinde projenizi seçip Sil'i tıklayın.
- İletişim kutusunda proje kimliğini yazın ve projeyi silmek için Kapat'ı tıklayın.
Daha fazla bilgi
- Google Workspace Eklentilerine genel bakış
- Marketplace'te mevcut uygulama ve eklentileri bulma