Create third-party resources from the @ menu

  • This guide details building a Google Workspace add-on to create and manage external resources (like support cases) directly within Google Docs.

  • Users can create resources via a form within Docs, which then inserts a smart chip linking to the resource in the external service.

  • The add-on requires configuration in the manifest file and utilizes Apps Script, Node.js, Python, or Java for development.

  • Comprehensive code samples are provided to guide developers through card creation, form submission, and error handling.

  • Smart chips representing the created resources offer link previews, enhancing user experience and information access.

This page explains how to build a Google Workspace add-on that lets Google Docs users create resources, such as a support case or project task, in a third-party service from within Google Docs.

With a Google Workspace add-on, you can add your service to the @ menu in Docs. The add-on adds menu items that let users create resources in your service through a form dialog in Docs.

How users create resources

To create a resource in your service from within a Google Docs document, users type @ in a document and select your service from the @ menu:

User previews a card

When users type @ in a document and select your service, you present them with a card that includes the form inputs that users need in order to create a resource. After the user submits the resource creation form, your add-on should create the resource in your service and generate a URL that points to it.

The add-on inserts a chip into the document for the created resource. When users hold the pointer over this chip, it invokes the add-on's associated link preview trigger. Make sure your add-on inserts chips with link patterns that are supported by your link preview triggers.

Prerequisites

Apps Script

  • A Google Workspace add-on that supports link previews for the link patterns of the resources that users create. To build an add-on with link previews, refer to Preview links with smart chips.

Node.js

  • A Google Workspace add-on that supports link previews for the link patterns of the resources that users create. To build an add-on with link previews, refer to Preview links with smart chips.

Python

  • A Google Workspace add-on that supports link previews for the link patterns of the resources that users create. To build an add-on with link previews, refer to Preview links with smart chips.

Java

  • A Google Workspace add-on that supports link previews for the link patterns of the resources that users create. To build an add-on with link previews, refer to Preview links with smart chips.

Set up resource creation for your add-on

This section explains how to set up resource creation for your add-on, which includes the following steps:

  1. Configure resource creation in your add-on's manifest.
  2. Build the form cards that users need to create resources within your service.
  3. Handle form submissions so that the function that creates the resource runs when users submit the form.

Configure resource creation

To configure resource creation, specify the following sections and fields in your add-on's manifest:

  1. Under the addOns section in the docs field, implement the createActionTriggers trigger that includes a runFunction. (You define this function in the following section, Build the form cards.)

    To learn about what fields you can specify in the createActionTriggers trigger, see the reference documentation for Apps Script manifests or deployment resources for other runtimes.

  2. In the oauthScopes field, add the scope https://www.googleapis.com/auth/workspace.linkcreate so that users can authorize the add-on to create resources. Specifically, this scope allows the add-on to read the information that users submit to the resource creation form and insert a smart chip into the document based on that information.

As an example, see the addons section of a manifest that configures resource creation for the following support case service:

{
"oauthScopes":[
"https://www.googleapis.com/auth/workspace.linkpreview",
"https://www.googleapis.com/auth/workspace.linkcreate"
],
"addOns":{
"docs":{
"linkPreviewTriggers":[
...
],
"createActionTriggers":[
{
"id":"createCase",
"labelText":"Create support case",
"localizedLabelText":{
"es":"Crear caso de soporte"
},
"runFunction":"createCaseInputCard",
"logoUrl":"https://www.example.com/images/case.png"
}
]
}
}
}

In the example, the Google Workspace add-on lets users create support cases. Each createActionTriggers trigger must have the following fields:

  • A unique ID
  • A text label that appears in the Docs @ menu
  • A logo URL pointing to an icon that appears next to the label text in the @ menu
  • A callback function that references either an Apps Script function or an HTTP endpoint that returns a card

Build the form cards

To create resources in your service from the Docs @ menu, you must implement any functions that you specified in the createActionTriggers object.

When a user interacts with one of your menu items, the corresponding createActionTriggers trigger fires and its callback function presents a card with form inputs for creating the resource.

Supported elements and actions

To create the card interface, you use widgets to display information and inputs that users need in order to create the resource. Most Google Workspace add-on widgets and actions are supported with the following exceptions:

  • Card footers aren't supported.
  • Notifications aren't supported.
  • For navigations, only the updateCard navigation is supported.

Example of card with form inputs

The following example shows an Apps Script callback function that displays a card when a user selects Create support case from the @ menu:

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
 * Produces a support case creation form card.
 * 
 * @param {!Object} event The event object.
 * @param {!Object=} errors An optional map of per-field error messages.
 * @param {boolean} isUpdate Whether to return the form as an update card navigation.
 * @return {!Card|!ActionResponse} The resulting card or action response.
 */
functioncreateCaseInputCard(event,errors,isUpdate){
constcardHeader=CardService.newCardHeader()
.setTitle('Create a support case')
constcardSectionTextInput1=CardService.newTextInput()
.setFieldName('name')
.setTitle('Name')
.setMultiline(false);
constcardSectionTextInput2=CardService.newTextInput()
.setFieldName('description')
.setTitle('Description')
.setMultiline(true);
constcardSectionSelectionInput1=CardService.newSelectionInput()
.setFieldName('priority')
.setTitle('Priority')
.setType(CardService.SelectionInputType.DROPDOWN)
.addItem('P0','P0',false)
.addItem('P1','P1',false)
.addItem('P2','P2',false)
.addItem('P3','P3',false);
constcardSectionSelectionInput2=CardService.newSelectionInput()
.setFieldName('impact')
.setTitle('Impact')
.setType(CardService.SelectionInputType.CHECK_BOX)
.addItem('Blocks a critical customer operation','Blocks a critical customer operation',false);
constcardSectionButtonListButtonAction=CardService.newAction()
.setPersistValues(true)
.setFunctionName('submitCaseCreationForm')
.setParameters({});
constcardSectionButtonListButton=CardService.newTextButton()
.setText('Create')
.setTextButtonStyle(CardService.TextButtonStyle.TEXT)
.setOnClickAction(cardSectionButtonListButtonAction);
constcardSectionButtonList=CardService.newButtonSet()
.addButton(cardSectionButtonListButton);
// Builds the form inputs with error texts for invalid values.
constcardSection=CardService.newCardSection();
if(errors?.name){
cardSection.addWidget(createErrorTextParagraph(errors.name));
}
cardSection.addWidget(cardSectionTextInput1);
if(errors?.description){
cardSection.addWidget(createErrorTextParagraph(errors.description));
}
cardSection.addWidget(cardSectionTextInput2);
if(errors?.priority){
cardSection.addWidget(createErrorTextParagraph(errors.priority));
}
cardSection.addWidget(cardSectionSelectionInput1);
if(errors?.impact){
cardSection.addWidget(createErrorTextParagraph(errors.impact));
}
cardSection.addWidget(cardSectionSelectionInput2);
cardSection.addWidget(cardSectionButtonList);
constcard=CardService.newCardBuilder()
.setHeader(cardHeader)
.addSection(cardSection)
.build();
if(isUpdate){
returnCardService.newActionResponseBuilder()
.setNavigation(CardService.newNavigation().updateCard(card))
.build();
}else{
returncard;
}
}

Node.js

node/3p-resources/index.js
/**
 * Produces a support case creation form card.
 * 
 * @param {!Object} event The event object.
 * @param {!Object=} errors An optional map of per-field error messages.
 * @param {boolean} isUpdate Whether to return the form as an update card navigation.
 * @return {!Card|!ActionResponse} The resulting card or action response.
 */
functioncreateCaseInputCard(event,errors,isUpdate){
constcardHeader1={
title:"Create a support case"
};
constcardSection1TextInput1={
textInput:{
name:"name",
label:"Name"
}
};
constcardSection1TextInput2={
textInput:{
name:"description",
label:"Description",
type:"MULTIPLE_LINE"
}
};
constcardSection1SelectionInput1={
selectionInput:{
name:"priority",
label:"Priority",
type:"DROPDOWN",
items:[{
text:"P0",
value:"P0"
},{
text:"P1",
value:"P1"
},{
text:"P2",
value:"P2"
},{
text:"P3",
value:"P3"
}]
}
};
constcardSection1SelectionInput2={
selectionInput:{
name:"impact",
label:"Impact",
items:[{
text:"Blocks a critical customer operation",
value:"Blocks a critical customer operation"
}]
}
};
constcardSection1ButtonList1Button1Action1={
function:process.env.URL,
parameters:[
{
key:"submitCaseCreationForm",
value:true
}
],
persistValues:true
};
constcardSection1ButtonList1Button1={
text:"Create",
onClick:{
action:cardSection1ButtonList1Button1Action1
}
};
constcardSection1ButtonList1={
buttonList:{
buttons:[cardSection1ButtonList1Button1]
}
};
// Builds the creation form and adds error text for invalid inputs.
constcardSection1=[];
if(errors?.name){
cardSection1.push(createErrorTextParagraph(errors.name));
}
cardSection1.push(cardSection1TextInput1);
if(errors?.description){
cardSection1.push(createErrorTextParagraph(errors.description));
}
cardSection1.push(cardSection1TextInput2);
if(errors?.priority){
cardSection1.push(createErrorTextParagraph(errors.priority));
}
cardSection1.push(cardSection1SelectionInput1);
if(errors?.impact){
cardSection1.push(createErrorTextParagraph(errors.impact));
}
cardSection1.push(cardSection1SelectionInput2);
cardSection1.push(cardSection1ButtonList1);
constcard={
header:cardHeader1,
sections:[{
widgets:cardSection1
}]
};
if(isUpdate){
return{
renderActions:{
action:{
navigations:[{
updateCard:card
}]
}
}
};
}else{
return{
action:{
navigations:[{
pushCard:card
}]
}
};
}
}

Python

python/3p-resources/create_3p_resources/main.py
defcreate_case_input_card(event, errors = {}, isUpdate = False):
"""Produces a support case creation form card.
 Args:
 event: The event object.
 errors: An optional dict of per-field error messages.
 isUpdate: Whether to return the form as an update card navigation.
 Returns:
 The resulting card or action response.
 """
 card_header1 = {
 "title": "Create a support case"
 }
 card_section1_text_input1 = {
 "textInput": {
 "name": "name",
 "label": "Name"
 }
 }
 card_section1_text_input2 = {
 "textInput": {
 "name": "description",
 "label": "Description",
 "type": "MULTIPLE_LINE"
 }
 }
 card_section1_selection_input1 = {
 "selectionInput": {
 "name": "priority",
 "label": "Priority",
 "type": "DROPDOWN",
 "items": [{
 "text": "P0",
 "value": "P0"
 }, {
 "text": "P1",
 "value": "P1"
 }, {
 "text": "P2",
 "value": "P2"
 }, {
 "text": "P3",
 "value": "P3"
 }]
 }
 }
 card_section1_selection_input2 = {
 "selectionInput": {
 "name": "impact",
 "label": "Impact",
 "items": [{
 "text": "Blocks a critical customer operation",
 "value": "Blocks a critical customer operation"
 }]
 }
 }
 card_section1_button_list1_button1_action1 = {
 "function": os.environ["URL"],
 "parameters": [
 {
 "key": "submitCaseCreationForm",
 "value": True
 }
 ],
 "persistValues": True
 }
 card_section1_button_list1_button1 = {
 "text": "Create",
 "onClick": {
 "action": card_section1_button_list1_button1_action1
 }
 }
 card_section1_button_list1 = {
 "buttonList": {
 "buttons": [card_section1_button_list1_button1]
 }
 }
 # Builds the creation form and adds error text for invalid inputs.
 card_section1 = []
 if "name" in errors:
 card_section1.append(create_error_text_paragraph(errors["name"]))
 card_section1.append(card_section1_text_input1)
 if "description" in errors:
 card_section1.append(create_error_text_paragraph(errors["description"]))
 card_section1.append(card_section1_text_input2)
 if "priority" in errors:
 card_section1.append(create_error_text_paragraph(errors["priority"]))
 card_section1.append(card_section1_selection_input1)
 if "impact" in errors:
 card_section1.append(create_error_text_paragraph(errors["impact"]))
 card_section1.append(card_section1_selection_input2)
 card_section1.append(card_section1_button_list1)
 card = {
 "header": card_header1,
 "sections": [{
 "widgets": card_section1
 }]
 }
 if isUpdate:
 return {
 "renderActions": {
 "action": {
 "navigations": [{
 "updateCard": card
 }]
 }
 }
 }
 else:
 return {
 "action": {
 "navigations": [{
 "pushCard": card
 }]
 }
 }

Java

java/3p-resources/src/main/java/Create3pResources.java
/**
 * Produces a support case creation form.
 * 
 * @param event The event object.
 * @param errors A map of per-field error messages.
 * @param isUpdate Whether to return the form as an update card navigation.
 * @return The resulting card or action response.
 */
JsonObjectcreateCaseInputCard(JsonObjectevent,Map<String,String>errors,booleanisUpdate){
JsonObjectcardHeader=newJsonObject();
cardHeader.add("title",newJsonPrimitive("Create a support case"));
JsonObjectcardSectionTextInput1=newJsonObject();
cardSectionTextInput1.add("name",newJsonPrimitive("name"));
cardSectionTextInput1.add("label",newJsonPrimitive("Name"));
JsonObjectcardSectionTextInput1Widget=newJsonObject();
cardSectionTextInput1Widget.add("textInput",cardSectionTextInput1);
JsonObjectcardSectionTextInput2=newJsonObject();
cardSectionTextInput2.add("name",newJsonPrimitive("description"));
cardSectionTextInput2.add("label",newJsonPrimitive("Description"));
cardSectionTextInput2.add("type",newJsonPrimitive("MULTIPLE_LINE"));
JsonObjectcardSectionTextInput2Widget=newJsonObject();
cardSectionTextInput2Widget.add("textInput",cardSectionTextInput2);
JsonObjectcardSectionSelectionInput1ItemsItem1=newJsonObject();
cardSectionSelectionInput1ItemsItem1.add("text",newJsonPrimitive("P0"));
cardSectionSelectionInput1ItemsItem1.add("value",newJsonPrimitive("P0"));
JsonObjectcardSectionSelectionInput1ItemsItem2=newJsonObject();
cardSectionSelectionInput1ItemsItem2.add("text",newJsonPrimitive("P1"));
cardSectionSelectionInput1ItemsItem2.add("value",newJsonPrimitive("P1"));
JsonObjectcardSectionSelectionInput1ItemsItem3=newJsonObject();
cardSectionSelectionInput1ItemsItem3.add("text",newJsonPrimitive("P2"));
cardSectionSelectionInput1ItemsItem3.add("value",newJsonPrimitive("P2"));
JsonObjectcardSectionSelectionInput1ItemsItem4=newJsonObject();
cardSectionSelectionInput1ItemsItem4.add("text",newJsonPrimitive("P3"));
cardSectionSelectionInput1ItemsItem4.add("value",newJsonPrimitive("P3"));
JsonArraycardSectionSelectionInput1Items=newJsonArray();
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);
JsonObjectcardSectionSelectionInput1=newJsonObject();
cardSectionSelectionInput1.add("name",newJsonPrimitive("priority"));
cardSectionSelectionInput1.add("label",newJsonPrimitive("Priority"));
cardSectionSelectionInput1.add("type",newJsonPrimitive("DROPDOWN"));
cardSectionSelectionInput1.add("items",cardSectionSelectionInput1Items);
JsonObjectcardSectionSelectionInput1Widget=newJsonObject();
cardSectionSelectionInput1Widget.add("selectionInput",cardSectionSelectionInput1);
JsonObjectcardSectionSelectionInput2ItemsItem=newJsonObject();
cardSectionSelectionInput2ItemsItem.add("text",newJsonPrimitive("Blocks a critical customer operation"));
cardSectionSelectionInput2ItemsItem.add("value",newJsonPrimitive("Blocks a critical customer operation"));
JsonArraycardSectionSelectionInput2Items=newJsonArray();
cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);
JsonObjectcardSectionSelectionInput2=newJsonObject();
cardSectionSelectionInput2.add("name",newJsonPrimitive("impact"));
cardSectionSelectionInput2.add("label",newJsonPrimitive("Impact"));
cardSectionSelectionInput2.add("items",cardSectionSelectionInput2Items);
JsonObjectcardSectionSelectionInput2Widget=newJsonObject();
cardSectionSelectionInput2Widget.add("selectionInput",cardSectionSelectionInput2);
JsonObjectcardSectionButtonListButtonActionParametersParameter=newJsonObject();
cardSectionButtonListButtonActionParametersParameter.add("key",newJsonPrimitive("submitCaseCreationForm"));
cardSectionButtonListButtonActionParametersParameter.add("value",newJsonPrimitive(true));
JsonArraycardSectionButtonListButtonActionParameters=newJsonArray();
cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);
JsonObjectcardSectionButtonListButtonAction=newJsonObject();
cardSectionButtonListButtonAction.add("function",newJsonPrimitive(System.getenv().get("URL")));
cardSectionButtonListButtonAction.add("parameters",cardSectionButtonListButtonActionParameters);
cardSectionButtonListButtonAction.add("persistValues",newJsonPrimitive(true));
JsonObjectcardSectionButtonListButtonOnCLick=newJsonObject();
cardSectionButtonListButtonOnCLick.add("action",cardSectionButtonListButtonAction);
JsonObjectcardSectionButtonListButton=newJsonObject();
cardSectionButtonListButton.add("text",newJsonPrimitive("Create"));
cardSectionButtonListButton.add("onClick",cardSectionButtonListButtonOnCLick);
JsonArraycardSectionButtonListButtons=newJsonArray();
cardSectionButtonListButtons.add(cardSectionButtonListButton);
JsonObjectcardSectionButtonList=newJsonObject();
cardSectionButtonList.add("buttons",cardSectionButtonListButtons);
JsonObjectcardSectionButtonListWidget=newJsonObject();
cardSectionButtonListWidget.add("buttonList",cardSectionButtonList);
// Builds the form inputs with error texts for invalid values.
JsonArraycardSection=newJsonArray();
if(errors.containsKey("name")){
cardSection.add(createErrorTextParagraph(errors.get("name").toString()));
}
cardSection.add(cardSectionTextInput1Widget);
if(errors.containsKey("description")){
cardSection.add(createErrorTextParagraph(errors.get("description").toString()));
}
cardSection.add(cardSectionTextInput2Widget);
if(errors.containsKey("priority")){
cardSection.add(createErrorTextParagraph(errors.get("priority").toString()));
}
cardSection.add(cardSectionSelectionInput1Widget);
if(errors.containsKey("impact")){
cardSection.add(createErrorTextParagraph(errors.get("impact").toString()));
}
cardSection.add(cardSectionSelectionInput2Widget);
cardSection.add(cardSectionButtonListWidget);
JsonObjectcardSectionWidgets=newJsonObject();
cardSectionWidgets.add("widgets",cardSection);
JsonArraysections=newJsonArray();
sections.add(cardSectionWidgets);
JsonObjectcard=newJsonObject();
card.add("header",cardHeader);
card.add("sections",sections);
JsonObjectnavigation=newJsonObject();
if(isUpdate){
navigation.add("updateCard",card);
}else{
navigation.add("pushCard",card);
}
JsonArraynavigations=newJsonArray();
navigations.add(navigation);
JsonObjectaction=newJsonObject();
action.add("navigations",navigations);
JsonObjectrenderActions=newJsonObject();
renderActions.add("action",action);
if(!isUpdate){
returnrenderActions;
}
JsonObjectupdate=newJsonObject();
update.add("renderActions",renderActions);
returnupdate;
}

The createCaseInputCard function renders the following card:

Card with form inputs

The card includes text inputs, a drop-down menu, and a checkbox. It also has a text button with anonClick action that runs another function to handle the submission of the creation form.

After the user fills out the form and clicks Create, the add-on sends the form inputs to theonClick action function–called submitCaseCreationForm in our example–at which point the add-on can validate the inputs and use them to create the resource in the third-party service.

Handle form submissions

After a user submits the creation form, the function associated with the onClick action runs. For an ideal user experience, your add-on should handle both successful and erroneous form submissions.

Handle successful resource creation

The onClick function of your add-on should create the resource in your third-party service and generate a URL that points to it.

In order to communicate the resource's URL back to Docs for chip creation, the onClick function should return a SubmitFormResponse with a one-element array in renderActions.action.links that points to a link. The link title should represent the title of the created resource and the URL should point to that resource.

The following example shows a SubmitFormResponse for a created resource:

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param {string} title The title of the link to insert.
 * @param {string} url The URL of the link to insert.
 * @return {!SubmitFormResponse} The resulting submit form response.
 */
functioncreateLinkRenderAction(title,url){
return{
renderActions:{
action:{
links:[{
title:title,
url:url
}]
}
}
};
}

Node.js

node/3p-resources/index.js
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param {string} title The title of the link to insert.
 * @param {string} url The URL of the link to insert.
 * @return {!SubmitFormResponse} The resulting submit form response.
 */
functioncreateLinkRenderAction(title,url){
return{
renderActions:{
action:{
links:[{
title:title,
url:url
}]
}
}
};
}

Python

python/3p-resources/create_3p_resources/main.py
defcreate_link_render_action(title, url):
"""Returns a submit form response that inserts a link into the document.
 Args:
 title: The title of the link to insert.
 url: The URL of the link to insert.
 Returns:
 The resulting submit form response.
 """
 return {
 "renderActions": {
 "action": {
 "links": [{
 "title": title,
 "url": url
 }]
 }
 }
 }

Java

java/3p-resources/src/main/java/Create3pResources.java
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param title The title of the link to insert.
 * @param url The URL of the link to insert.
 * @return The resulting submit form response.
 */
JsonObjectcreateLinkRenderAction(Stringtitle,Stringurl){
JsonObjectlink=newJsonObject();
link.add("title",newJsonPrimitive(title));
link.add("url",newJsonPrimitive(url));
JsonArraylinks=newJsonArray();
links.add(link);
JsonObjectaction=newJsonObject();
action.add("links",links);
JsonObjectrenderActions=newJsonObject();
renderActions.add("action",action);
JsonObjectlinkRenderAction=newJsonObject();
linkRenderAction.add("renderActions",renderActions);
returnlinkRenderAction;
}

After the SubmitFormResponse is returned, the modal dialog closes and the add-on inserts a chip into the document. When users hold the pointer over this chip, it invokes the associated link preview trigger. Make sure your add-on doesn't insert chips with link patterns not supported by your link preview triggers.

Handle errors

If a user tries to submit a form with invalid fields, instead of returning a SubmitFormResponse with a link, the add-on should return a render action that displays an error using an updateCard navigation. This lets the user see what they did wrong and try again. See updateCard(card) for Apps Script and updateCard for other runtimes. Notifications and pushCard navigations aren't supported.

Example of error handling

The following example shows the code that's invoked when a user submits the form. If the inputs are invalid, the card updates and shows error messages. If the inputs are valid, the add-on returns a SubmitFormResponse with a link to the created resource.

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param {!Object} event The event object with form input values.
 * @return {!ActionResponse|!SubmitFormResponse} The resulting response.
 */
functionsubmitCaseCreationForm(event){
constcaseDetails={
name:event.formInput.name,
description:event.formInput.description,
priority:event.formInput.priority,
impact:!!event.formInput.impact,
};
consterrors=validateFormInputs(caseDetails);
if(Object.keys(errors).length > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
consttitle=`Case ${caseDetails.name}`;
// Adds the case details as parameters to the generated link URL.
consturl='https://example.com/support/cases/?'+generateQuery(caseDetails);
returncreateLinkRenderAction(title,url);
}
}
/**
* Build a query path with URL parameters.
*
* @param {!Map} parameters A map with the URL parameters.
* @return {!string} The resulting query path.
*/
functiongenerateQuery(parameters){
returnObject.entries(parameters).flatMap(([k,v])=>
Array.isArray(v)?v.map(e=>`${k}=${encodeURIComponent(e)}`):`${k}=${encodeURIComponent(v)}`
).join("&");
}

Node.js

node/3p-resources/index.js
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param {!Object} event The event object with form input values.
 * @return {!ActionResponse|!SubmitFormResponse} The resulting response.
 */
functionsubmitCaseCreationForm(event){
constcaseDetails={
name:event.commonEventObject.formInputs?.name?.stringInputs?.value[0],
description:event.commonEventObject.formInputs?.description?.stringInputs?.value[0],
priority:event.commonEventObject.formInputs?.priority?.stringInputs?.value[0],
impact:!!event.commonEventObject.formInputs?.impact?.stringInputs?.value[0],
};
consterrors=validateFormInputs(caseDetails);
if(Object.keys(errors).length > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
consttitle=`Case ${caseDetails.name}`;
// Adds the case details as parameters to the generated link URL.
consturl=newURL('https://example.com/support/cases/');
for(const[key,value]ofObject.entries(caseDetails)){
url.searchParams.append(key,value);
}
returncreateLinkRenderAction(title,url.href);
}
}

Python

python/3p-resources/create_3p_resources/main.py
defsubmit_case_creation_form(event):
"""Submits the creation form.
 If valid, returns a render action that inserts a new link
 into the document. If invalid, returns an update card navigation that
 re-renders the creation form with error messages.
 Args:
 event: The event object with form input values.
 Returns:
 The resulting response.
 """
 formInputs = event["commonEventObject"]["formInputs"] if "formInputs" in event["commonEventObject"] else None
 case_details = {
 "name": None,
 "description": None,
 "priority": None,
 "impact": None,
 }
 if formInputs is not None:
 case_details["name"] = formInputs["name"]["stringInputs"]["value"][0] if "name" in formInputs else None
 case_details["description"] = formInputs["description"]["stringInputs"]["value"][0] if "description" in formInputs else None
 case_details["priority"] = formInputs["priority"]["stringInputs"]["value"][0] if "priority" in formInputs else None
 case_details["impact"] = formInputs["impact"]["stringInputs"]["value"][0] if "impact" in formInputs else False
 errors = validate_form_inputs(case_details)
 if len(errors) > 0:
 return create_case_input_card(event, errors, True) # Update mode
 else:
 title = f'Case {case_details["name"]}'
 # Adds the case details as parameters to the generated link URL.
 url = "https://example.com/support/cases/?" + urlencode(case_details)
 return create_link_render_action(title, url)

Java

java/3p-resources/src/main/java/Create3pResources.java
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param event The event object with form input values.
 * @return The resulting response.
 */
JsonObjectsubmitCaseCreationForm(JsonObjectevent)throwsException{
JsonObjectformInputs=event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");
Map<String,String>caseDetails=newHashMap<String,String>();
if(formInputs!=null){
if(formInputs.has("name")){
caseDetails.put("name",formInputs.getAsJsonObject("name").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("description")){
caseDetails.put("description",formInputs.getAsJsonObject("description").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("priority")){
caseDetails.put("priority",formInputs.getAsJsonObject("priority").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("impact")){
caseDetails.put("impact",formInputs.getAsJsonObject("impact").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
}
Map<String,String>errors=validateFormInputs(caseDetails);
if(errors.size() > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
Stringtitle=String.format("Case %s",caseDetails.get("name"));
// Adds the case details as parameters to the generated link URL.
URIBuilderuriBuilder=newURIBuilder("https://example.com/support/cases/");
for(StringcaseDetailKey:caseDetails.keySet()){
uriBuilder.addParameter(caseDetailKey,caseDetails.get(caseDetailKey));
}
returncreateLinkRenderAction(title,uriBuilder.build().toURL().toString());
}
}

The following code sample validates the form inputs and creates error messages for invalid inputs:

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
 * Validates case creation form input values.
 * 
 * @param {!Object} caseDetails The values of each form input submitted by the user.
 * @return {!Object} A map from field name to error message. An empty object
 * represents a valid form submission.
 */
functionvalidateFormInputs(caseDetails){
consterrors={};
if(!caseDetails.name){
errors.name='You must provide a name';
}
if(!caseDetails.description){
errors.description='You must provide a description';
}
if(!caseDetails.priority){
errors.priority='You must provide a priority';
}
if(caseDetails.impact && caseDetails.priority!=='P0' && caseDetails.priority!=='P1'){
errors.impact='If an issue blocks a critical customer operation, priority must be P0 or P1';
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param {string} errorMessage A description of input value error.
 * @return {!TextParagraph} The resulting text paragraph.
 */
functioncreateErrorTextParagraph(errorMessage){
returnCardService.newTextParagraph()
.setText('<font color=\"#BA0300\"><b>Error:</b> '+errorMessage+'</font>');
}

Node.js

node/3p-resources/index.js
/**
 * Validates case creation form input values.
 * 
 * @param {!Object} caseDetails The values of each form input submitted by the user.
 * @return {!Object} A map from field name to error message. An empty object
 * represents a valid form submission.
 */
functionvalidateFormInputs(caseDetails){
consterrors={};
if(caseDetails.name===undefined){
errors.name='You must provide a name';
}
if(caseDetails.description===undefined){
errors.description='You must provide a description';
}
if(caseDetails.priority===undefined){
errors.priority='You must provide a priority';
}
if(caseDetails.impact && !(['P0','P1']).includes(caseDetails.priority)){
errors.impact='If an issue blocks a critical customer operation, priority must be P0 or P1';
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param {string} errorMessage A description of input value error.
 * @return {!TextParagraph} The resulting text paragraph.
 */
functioncreateErrorTextParagraph(errorMessage){
return{
textParagraph:{
text:'<font color=\"#BA0300\"><b>Error:</b> '+errorMessage+'</font>'
}
}
}

Python

python/3p-resources/create_3p_resources/main.py
defvalidate_form_inputs(case_details):
"""Validates case creation form input values.
 Args:
 case_details: The values of each form input submitted by the user.
 Returns:
 A dict from field name to error message. An empty object represents a valid form submission.
 """
 errors = {}
 if case_details["name"] is None:
 errors["name"] = "You must provide a name"
 if case_details["description"] is None:
 errors["description"] = "You must provide a description"
 if case_details["priority"] is None:
 errors["priority"] = "You must provide a priority"
 if case_details["impact"] is not None and case_details["priority"] not in ['P0', 'P1']:
 errors["impact"] = "If an issue blocks a critical customer operation, priority must be P0 or P1"
 return errors
defcreate_error_text_paragraph(error_message):
"""Returns a text paragraph with red text indicating a form field validation error.
 Args:
 error_essage: A description of input value error.
 Returns:
 The resulting text paragraph.
 """
 return {
 "textParagraph": {
 "text": '<font color=\"#BA0300\"><b>Error:</b> ' + error_message + '</font>'
 }
 }

Java

java/3p-resources/src/main/java/Create3pResources.java
/**
 * Validates case creation form input values.
 * 
 * @param caseDetails The values of each form input submitted by the user.
 * @return A map from field name to error message. An empty object
 * represents a valid form submission.
 */
Map<String,String>validateFormInputs(Map<String,String>caseDetails){
Map<String,String>errors=newHashMap<String,String>();
if(!caseDetails.containsKey("name")){
errors.put("name","You must provide a name");
}
if(!caseDetails.containsKey("description")){
errors.put("description","You must provide a description");
}
if(!caseDetails.containsKey("priority")){
errors.put("priority","You must provide a priority");
}
if(caseDetails.containsKey("impact") && !Arrays.asList(newString[]{"P0","P1"}).contains(caseDetails.get("priority"))){
errors.put("impact","If an issue blocks a critical customer operation, priority must be P0 or P1");
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param errorMessage A description of input value error.
 * @return The resulting text paragraph.
 */
JsonObjectcreateErrorTextParagraph(StringerrorMessage){
JsonObjecttextParagraph=newJsonObject();
textParagraph.add("text",newJsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> "+errorMessage+"</font>"));
JsonObjecttextParagraphWidget=newJsonObject();
textParagraphWidget.add("textParagraph",textParagraph);
returntextParagraphWidget;
}

Complete example: Support case add-on

The following example shows a Google Workspace add-on that previews links to a company's support cases and lets users create support cases from within Google Docs.

The example does the following:

  • Generates a card with form fields to create a support case from the Docs @ menu.
  • Validates form inputs and returns error messages for invalid inputs.
  • Inserts the created support case's name and link into the Docs document as a smart chip.
  • Previews the link to the support case, such as https://www.example.com/support/cases/1234. The smart chip displays an icon, and the preview card includes the case name, priority, and description.

Manifest

Apps Script

apps-script/3p-resources/appsscript.json
{
"timeZone":"America/New_York",
"exceptionLogging":"STACKDRIVER",
"runtimeVersion":"V8",
"oauthScopes":[
"https://www.googleapis.com/auth/workspace.linkpreview",
"https://www.googleapis.com/auth/workspace.linkcreate"
],
"addOns":{
"common":{
"name":"Manage support cases",
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png",
"layoutProperties":{
"primaryColor":"#dd4b39"
}
},
"docs":{
"linkPreviewTriggers":[
{
"runFunction":"caseLinkPreview",
"patterns":[
{
"hostPattern":"example.com",
"pathPrefix":"support/cases"
},
{
"hostPattern":"*.example.com",
"pathPrefix":"cases"
},
{
"hostPattern":"cases.example.com"
}
],
"labelText":"Support case",
"localizedLabelText":{
"es":"Caso de soporte"
},
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png"
}
],
"createActionTriggers":[
{
"id":"createCase",
"labelText":"Create support case",
"localizedLabelText":{
"es":"Crear caso de soporte"
},
"runFunction":"createCaseInputCard",
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png"
}
]
}
}
}

Node.js

node/3p-resources/deployment.json
{
"oauthScopes":[
"https://www.googleapis.com/auth/workspace.linkpreview",
"https://www.googleapis.com/auth/workspace.linkcreate"
],
"addOns":{
"common":{
"name":"Manage support cases",
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png",
"layoutProperties":{
"primaryColor":"#dd4b39"
}
},
"docs":{
"linkPreviewTriggers":[
{
"runFunction":"$URL1",
"patterns":[
{
"hostPattern":"example.com",
"pathPrefix":"support/cases"
},
{
"hostPattern":"*.example.com",
"pathPrefix":"cases"
},
{
"hostPattern":"cases.example.com"
}
],
"labelText":"Support case",
"localizedLabelText":{
"es":"Caso de soporte"
},
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png"
}
],
"createActionTriggers":[
{
"id":"createCase",
"labelText":"Create support case",
"localizedLabelText":{
"es":"Crear caso de soporte"
},
"runFunction":"$URL2",
"logoUrl":"https://developers.google.com/workspace/add-ons/images/support-icon.png"
}
]
}
}
}

Code

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
 * Copyright 2024 Google LLC
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * https://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
* Entry point for a support case link preview.
*
* @param {!Object} event The event object.
* @return {!Card} The resulting preview link card.
*/
functioncaseLinkPreview(event){
// If the event object URL matches a specified pattern for support case links.
if(event.docs.matchedUrl.url){
// Uses the event object to parse the URL and identify the case details.
constcaseDetails=parseQuery(event.docs.matchedUrl.url);
// Builds a preview card with the case name, and description
constcaseHeader=CardService.newCardHeader()
.setTitle(`Case ${caseDetails["name"][0]}`);
constcaseDescription=CardService.newTextParagraph()
.setText(caseDetails["description"][0]);
// Returns the card.
// Uses the text from the card's header for the title of the smart chip.
returnCardService.newCardBuilder()
.setHeader(caseHeader)
.addSection(CardService.newCardSection().addWidget(caseDescription))
.build();
}
}
/**
* Extracts the URL parameters from the given URL.
*
* @param {!string} url The URL to parse.
* @return {!Map} A map with the extracted URL parameters.
*/
functionparseQuery(url){
constquery=url.split("?")[1];
if(query){
returnquery.split("&")
.reduce(function(o,e){
vartemp=e.split("=");
varkey=temp[0].trim();
varvalue=temp[1].trim();
value=isNaN(value)?value:Number(value);
if(o[key]){
o[key].push(value);
}else{
o[key]=[value];
}
returno;
},{});
}
returnnull;
}
/**
 * Produces a support case creation form card.
 * 
 * @param {!Object} event The event object.
 * @param {!Object=} errors An optional map of per-field error messages.
 * @param {boolean} isUpdate Whether to return the form as an update card navigation.
 * @return {!Card|!ActionResponse} The resulting card or action response.
 */
functioncreateCaseInputCard(event,errors,isUpdate){
constcardHeader=CardService.newCardHeader()
.setTitle('Create a support case')
constcardSectionTextInput1=CardService.newTextInput()
.setFieldName('name')
.setTitle('Name')
.setMultiline(false);
constcardSectionTextInput2=CardService.newTextInput()
.setFieldName('description')
.setTitle('Description')
.setMultiline(true);
constcardSectionSelectionInput1=CardService.newSelectionInput()
.setFieldName('priority')
.setTitle('Priority')
.setType(CardService.SelectionInputType.DROPDOWN)
.addItem('P0','P0',false)
.addItem('P1','P1',false)
.addItem('P2','P2',false)
.addItem('P3','P3',false);
constcardSectionSelectionInput2=CardService.newSelectionInput()
.setFieldName('impact')
.setTitle('Impact')
.setType(CardService.SelectionInputType.CHECK_BOX)
.addItem('Blocks a critical customer operation','Blocks a critical customer operation',false);
constcardSectionButtonListButtonAction=CardService.newAction()
.setPersistValues(true)
.setFunctionName('submitCaseCreationForm')
.setParameters({});
constcardSectionButtonListButton=CardService.newTextButton()
.setText('Create')
.setTextButtonStyle(CardService.TextButtonStyle.TEXT)
.setOnClickAction(cardSectionButtonListButtonAction);
constcardSectionButtonList=CardService.newButtonSet()
.addButton(cardSectionButtonListButton);
// Builds the form inputs with error texts for invalid values.
constcardSection=CardService.newCardSection();
if(errors?.name){
cardSection.addWidget(createErrorTextParagraph(errors.name));
}
cardSection.addWidget(cardSectionTextInput1);
if(errors?.description){
cardSection.addWidget(createErrorTextParagraph(errors.description));
}
cardSection.addWidget(cardSectionTextInput2);
if(errors?.priority){
cardSection.addWidget(createErrorTextParagraph(errors.priority));
}
cardSection.addWidget(cardSectionSelectionInput1);
if(errors?.impact){
cardSection.addWidget(createErrorTextParagraph(errors.impact));
}
cardSection.addWidget(cardSectionSelectionInput2);
cardSection.addWidget(cardSectionButtonList);
constcard=CardService.newCardBuilder()
.setHeader(cardHeader)
.addSection(cardSection)
.build();
if(isUpdate){
returnCardService.newActionResponseBuilder()
.setNavigation(CardService.newNavigation().updateCard(card))
.build();
}else{
returncard;
}
}
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param {!Object} event The event object with form input values.
 * @return {!ActionResponse|!SubmitFormResponse} The resulting response.
 */
functionsubmitCaseCreationForm(event){
constcaseDetails={
name:event.formInput.name,
description:event.formInput.description,
priority:event.formInput.priority,
impact:!!event.formInput.impact,
};
consterrors=validateFormInputs(caseDetails);
if(Object.keys(errors).length > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
consttitle=`Case ${caseDetails.name}`;
// Adds the case details as parameters to the generated link URL.
consturl='https://example.com/support/cases/?'+generateQuery(caseDetails);
returncreateLinkRenderAction(title,url);
}
}
/**
* Build a query path with URL parameters.
*
* @param {!Map} parameters A map with the URL parameters.
* @return {!string} The resulting query path.
*/
functiongenerateQuery(parameters){
returnObject.entries(parameters).flatMap(([k,v])=>
Array.isArray(v)?v.map(e=>`${k}=${encodeURIComponent(e)}`):`${k}=${encodeURIComponent(v)}`
).join("&");
}
/**
 * Validates case creation form input values.
 * 
 * @param {!Object} caseDetails The values of each form input submitted by the user.
 * @return {!Object} A map from field name to error message. An empty object
 * represents a valid form submission.
 */
functionvalidateFormInputs(caseDetails){
consterrors={};
if(!caseDetails.name){
errors.name='You must provide a name';
}
if(!caseDetails.description){
errors.description='You must provide a description';
}
if(!caseDetails.priority){
errors.priority='You must provide a priority';
}
if(caseDetails.impact && caseDetails.priority!=='P0' && caseDetails.priority!=='P1'){
errors.impact='If an issue blocks a critical customer operation, priority must be P0 or P1';
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param {string} errorMessage A description of input value error.
 * @return {!TextParagraph} The resulting text paragraph.
 */
functioncreateErrorTextParagraph(errorMessage){
returnCardService.newTextParagraph()
.setText('<font color=\"#BA0300\"><b>Error:</b> '+errorMessage+'</font>');
}
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param {string} title The title of the link to insert.
 * @param {string} url The URL of the link to insert.
 * @return {!SubmitFormResponse} The resulting submit form response.
 */
functioncreateLinkRenderAction(title,url){
return{
renderActions:{
action:{
links:[{
title:title,
url:url
}]
}
}
};
}

Node.js

node/3p-resources/index.js
/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * Responds to any HTTP request related to link previews.
 *
 * @param {Object} req An HTTP request context.
 * @param {Object} res An HTTP response context.
 */
exports.createLinkPreview=(req,res)=>{
constevent=req.body;
if(event.docs.matchedUrl.url){
consturl=event.docs.matchedUrl.url;
constparsedUrl=newURL(url);
// If the event object URL matches a specified pattern for preview links.
if(parsedUrl.hostname==='example.com'){
if(parsedUrl.pathname.startsWith('/support/cases/')){
returnres.json(caseLinkPreview(parsedUrl));
}
}
}
};
/**
 * 
 * A support case link preview.
 *
 * @param {!URL} url The event object.
 * @return {!Card} The resulting preview link card.
 */
functioncaseLinkPreview(url){
// Builds a preview card with the case name, and description
// Uses the text from the card's header for the title of the smart chip.
// Parses the URL and identify the case details.
constname=`Case ${url.searchParams.get("name")}`;
return{
action:{
linkPreview:{
title:name,
previewCard:{
header:{
title:name
},
sections:[{
widgets:[{
textParagraph:{
text:url.searchParams.get("description")
}
}]
}]
}
}
}
};
}
/**
 * Responds to any HTTP request related to 3P resource creations.
 *
 * @param {Object} req An HTTP request context.
 * @param {Object} res An HTTP response context.
 */
exports.create3pResources=(req,res)=>{
constevent=req.body;
if(event.commonEventObject.parameters?.submitCaseCreationForm){
res.json(submitCaseCreationForm(event));
}else{
res.json(createCaseInputCard(event));
}
};
/**
 * Produces a support case creation form card.
 * 
 * @param {!Object} event The event object.
 * @param {!Object=} errors An optional map of per-field error messages.
 * @param {boolean} isUpdate Whether to return the form as an update card navigation.
 * @return {!Card|!ActionResponse} The resulting card or action response.
 */
functioncreateCaseInputCard(event,errors,isUpdate){
constcardHeader1={
title:"Create a support case"
};
constcardSection1TextInput1={
textInput:{
name:"name",
label:"Name"
}
};
constcardSection1TextInput2={
textInput:{
name:"description",
label:"Description",
type:"MULTIPLE_LINE"
}
};
constcardSection1SelectionInput1={
selectionInput:{
name:"priority",
label:"Priority",
type:"DROPDOWN",
items:[{
text:"P0",
value:"P0"
},{
text:"P1",
value:"P1"
},{
text:"P2",
value:"P2"
},{
text:"P3",
value:"P3"
}]
}
};
constcardSection1SelectionInput2={
selectionInput:{
name:"impact",
label:"Impact",
items:[{
text:"Blocks a critical customer operation",
value:"Blocks a critical customer operation"
}]
}
};
constcardSection1ButtonList1Button1Action1={
function:process.env.URL,
parameters:[
{
key:"submitCaseCreationForm",
value:true
}
],
persistValues:true
};
constcardSection1ButtonList1Button1={
text:"Create",
onClick:{
action:cardSection1ButtonList1Button1Action1
}
};
constcardSection1ButtonList1={
buttonList:{
buttons:[cardSection1ButtonList1Button1]
}
};
// Builds the creation form and adds error text for invalid inputs.
constcardSection1=[];
if(errors?.name){
cardSection1.push(createErrorTextParagraph(errors.name));
}
cardSection1.push(cardSection1TextInput1);
if(errors?.description){
cardSection1.push(createErrorTextParagraph(errors.description));
}
cardSection1.push(cardSection1TextInput2);
if(errors?.priority){
cardSection1.push(createErrorTextParagraph(errors.priority));
}
cardSection1.push(cardSection1SelectionInput1);
if(errors?.impact){
cardSection1.push(createErrorTextParagraph(errors.impact));
}
cardSection1.push(cardSection1SelectionInput2);
cardSection1.push(cardSection1ButtonList1);
constcard={
header:cardHeader1,
sections:[{
widgets:cardSection1
}]
};
if(isUpdate){
return{
renderActions:{
action:{
navigations:[{
updateCard:card
}]
}
}
};
}else{
return{
action:{
navigations:[{
pushCard:card
}]
}
};
}
}
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param {!Object} event The event object with form input values.
 * @return {!ActionResponse|!SubmitFormResponse} The resulting response.
 */
functionsubmitCaseCreationForm(event){
constcaseDetails={
name:event.commonEventObject.formInputs?.name?.stringInputs?.value[0],
description:event.commonEventObject.formInputs?.description?.stringInputs?.value[0],
priority:event.commonEventObject.formInputs?.priority?.stringInputs?.value[0],
impact:!!event.commonEventObject.formInputs?.impact?.stringInputs?.value[0],
};
consterrors=validateFormInputs(caseDetails);
if(Object.keys(errors).length > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
consttitle=`Case ${caseDetails.name}`;
// Adds the case details as parameters to the generated link URL.
consturl=newURL('https://example.com/support/cases/');
for(const[key,value]ofObject.entries(caseDetails)){
url.searchParams.append(key,value);
}
returncreateLinkRenderAction(title,url.href);
}
}
/**
 * Validates case creation form input values.
 * 
 * @param {!Object} caseDetails The values of each form input submitted by the user.
 * @return {!Object} A map from field name to error message. An empty object
 * represents a valid form submission.
 */
functionvalidateFormInputs(caseDetails){
consterrors={};
if(caseDetails.name===undefined){
errors.name='You must provide a name';
}
if(caseDetails.description===undefined){
errors.description='You must provide a description';
}
if(caseDetails.priority===undefined){
errors.priority='You must provide a priority';
}
if(caseDetails.impact && !(['P0','P1']).includes(caseDetails.priority)){
errors.impact='If an issue blocks a critical customer operation, priority must be P0 or P1';
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param {string} errorMessage A description of input value error.
 * @return {!TextParagraph} The resulting text paragraph.
 */
functioncreateErrorTextParagraph(errorMessage){
return{
textParagraph:{
text:'<font color=\"#BA0300\"><b>Error:</b> '+errorMessage+'</font>'
}
}
}
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param {string} title The title of the link to insert.
 * @param {string} url The URL of the link to insert.
 * @return {!SubmitFormResponse} The resulting submit form response.
 */
functioncreateLinkRenderAction(title,url){
return{
renderActions:{
action:{
links:[{
title:title,
url:url
}]
}
}
};
}

Python

python/3p-resources/create_3p_resources/main.py
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
fromtypingimport Any, Mapping
fromurllib.parseimport urlencode
importos
importflask
importfunctions_framework
@functions_framework.http
defcreate_3p_resources(req: flask.Request):
"""Responds to any HTTP request related to 3P resource creations.
 Args:
 req: An HTTP request context.
 Returns:
 An HTTP response context.
 """
 event = req.get_json(silent=True)
 parameters = event["commonEventObject"]["parameters"] if "parameters" in event["commonEventObject"] else None
 if parameters is not None and parameters["submitCaseCreationForm"]:
 return submit_case_creation_form(event)
 else:
 return create_case_input_card(event)
defcreate_case_input_card(event, errors = {}, isUpdate = False):
"""Produces a support case creation form card.
 Args:
 event: The event object.
 errors: An optional dict of per-field error messages.
 isUpdate: Whether to return the form as an update card navigation.
 Returns:
 The resulting card or action response.
 """
 card_header1 = {
 "title": "Create a support case"
 }
 card_section1_text_input1 = {
 "textInput": {
 "name": "name",
 "label": "Name"
 }
 }
 card_section1_text_input2 = {
 "textInput": {
 "name": "description",
 "label": "Description",
 "type": "MULTIPLE_LINE"
 }
 }
 card_section1_selection_input1 = {
 "selectionInput": {
 "name": "priority",
 "label": "Priority",
 "type": "DROPDOWN",
 "items": [{
 "text": "P0",
 "value": "P0"
 }, {
 "text": "P1",
 "value": "P1"
 }, {
 "text": "P2",
 "value": "P2"
 }, {
 "text": "P3",
 "value": "P3"
 }]
 }
 }
 card_section1_selection_input2 = {
 "selectionInput": {
 "name": "impact",
 "label": "Impact",
 "items": [{
 "text": "Blocks a critical customer operation",
 "value": "Blocks a critical customer operation"
 }]
 }
 }
 card_section1_button_list1_button1_action1 = {
 "function": os.environ["URL"],
 "parameters": [
 {
 "key": "submitCaseCreationForm",
 "value": True
 }
 ],
 "persistValues": True
 }
 card_section1_button_list1_button1 = {
 "text": "Create",
 "onClick": {
 "action": card_section1_button_list1_button1_action1
 }
 }
 card_section1_button_list1 = {
 "buttonList": {
 "buttons": [card_section1_button_list1_button1]
 }
 }
 # Builds the creation form and adds error text for invalid inputs.
 card_section1 = []
 if "name" in errors:
 card_section1.append(create_error_text_paragraph(errors["name"]))
 card_section1.append(card_section1_text_input1)
 if "description" in errors:
 card_section1.append(create_error_text_paragraph(errors["description"]))
 card_section1.append(card_section1_text_input2)
 if "priority" in errors:
 card_section1.append(create_error_text_paragraph(errors["priority"]))
 card_section1.append(card_section1_selection_input1)
 if "impact" in errors:
 card_section1.append(create_error_text_paragraph(errors["impact"]))
 card_section1.append(card_section1_selection_input2)
 card_section1.append(card_section1_button_list1)
 card = {
 "header": card_header1,
 "sections": [{
 "widgets": card_section1
 }]
 }
 if isUpdate:
 return {
 "renderActions": {
 "action": {
 "navigations": [{
 "updateCard": card
 }]
 }
 }
 }
 else:
 return {
 "action": {
 "navigations": [{
 "pushCard": card
 }]
 }
 }
defsubmit_case_creation_form(event):
"""Submits the creation form.
 If valid, returns a render action that inserts a new link
 into the document. If invalid, returns an update card navigation that
 re-renders the creation form with error messages.
 Args:
 event: The event object with form input values.
 Returns:
 The resulting response.
 """
 formInputs = event["commonEventObject"]["formInputs"] if "formInputs" in event["commonEventObject"] else None
 case_details = {
 "name": None,
 "description": None,
 "priority": None,
 "impact": None,
 }
 if formInputs is not None:
 case_details["name"] = formInputs["name"]["stringInputs"]["value"][0] if "name" in formInputs else None
 case_details["description"] = formInputs["description"]["stringInputs"]["value"][0] if "description" in formInputs else None
 case_details["priority"] = formInputs["priority"]["stringInputs"]["value"][0] if "priority" in formInputs else None
 case_details["impact"] = formInputs["impact"]["stringInputs"]["value"][0] if "impact" in formInputs else False
 errors = validate_form_inputs(case_details)
 if len(errors) > 0:
 return create_case_input_card(event, errors, True) # Update mode
 else:
 title = f'Case {case_details["name"]}'
 # Adds the case details as parameters to the generated link URL.
 url = "https://example.com/support/cases/?" + urlencode(case_details)
 return create_link_render_action(title, url)
defvalidate_form_inputs(case_details):
"""Validates case creation form input values.
 Args:
 case_details: The values of each form input submitted by the user.
 Returns:
 A dict from field name to error message. An empty object represents a valid form submission.
 """
 errors = {}
 if case_details["name"] is None:
 errors["name"] = "You must provide a name"
 if case_details["description"] is None:
 errors["description"] = "You must provide a description"
 if case_details["priority"] is None:
 errors["priority"] = "You must provide a priority"
 if case_details["impact"] is not None and case_details["priority"] not in ['P0', 'P1']:
 errors["impact"] = "If an issue blocks a critical customer operation, priority must be P0 or P1"
 return errors
defcreate_error_text_paragraph(error_message):
"""Returns a text paragraph with red text indicating a form field validation error.
 Args:
 error_essage: A description of input value error.
 Returns:
 The resulting text paragraph.
 """
 return {
 "textParagraph": {
 "text": '<font color=\"#BA0300\"><b>Error:</b> ' + error_message + '</font>'
 }
 }
defcreate_link_render_action(title, url):
"""Returns a submit form response that inserts a link into the document.
 Args:
 title: The title of the link to insert.
 url: The URL of the link to insert.
 Returns:
 The resulting submit form response.
 """
 return {
 "renderActions": {
 "action": {
 "links": [{
 "title": title,
 "url": url
 }]
 }
 }
 }

The following code shows how to implement a link preview for the created resource:

python/3p-resources/create_link_preview/main.py
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License")
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
fromtypingimport Any, Mapping
fromurllib.parseimport urlparse, parse_qs
importflask
importfunctions_framework
@functions_framework.http
defcreate_link_preview(req: flask.Request):
"""Responds to any HTTP request related to link previews.
 Args:
 req: An HTTP request context.
 Returns:
 An HTTP response context.
 """
 event = req.get_json(silent=True)
 if event["docs"]["matchedUrl"]["url"]:
 url = event["docs"]["matchedUrl"]["url"]
 parsed_url = urlparse(url)
 # If the event object URL matches a specified pattern for preview links.
 if parsed_url.hostname == "example.com":
 if parsed_url.path.startswith("/support/cases/"):
 return case_link_preview(parsed_url)
 return {}
defcase_link_preview(url):
"""A support case link preview.
 Args:
 url: A matching URL.
 Returns:
 The resulting preview link card.
 """
 # Parses the URL and identify the case details.
 query_string = parse_qs(url.query)
 name = f'Case {query_string["name"][0]}'
 # Uses the text from the card's header for the title of the smart chip.
 return {
 "action": {
 "linkPreview": {
 "title": name,
 "previewCard": {
 "header": {
 "title": name
 },
 "sections": [{
 "widgets": [{
 "textParagraph": {
 "text": query_string["description"][0]
 }
 }]
 }],
 }
 }
 }
 }

Java

java/3p-resources/src/main/java/Create3pResources.java
/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
importjava.util.Arrays;
importjava.util.HashMap;
importjava.util.Map;
importorg.apache.http.client.utils.URIBuilder;
importcom.google.cloud.functions.HttpFunction;
importcom.google.cloud.functions.HttpRequest;
importcom.google.cloud.functions.HttpResponse;
importcom.google.gson.Gson;
importcom.google.gson.JsonArray;
importcom.google.gson.JsonObject;
importcom.google.gson.JsonPrimitive;
publicclass Create3pResourcesimplementsHttpFunction{
privatestaticfinalGsongson=newGson();
/**
 * Responds to any HTTP request related to 3p resource creations.
 *
 * @param request An HTTP request context.
 * @param response An HTTP response context.
 */
@Override
publicvoidservice(HttpRequestrequest,HttpResponseresponse)throwsException{
JsonObjectevent=gson.fromJson(request.getReader(),JsonObject.class);
JsonObjectparameters=event.getAsJsonObject("commonEventObject").getAsJsonObject("parameters");
if(parameters!=null && parameters.has("submitCaseCreationForm") && parameters.get("submitCaseCreationForm").getAsBoolean()){
response.getWriter().write(gson.toJson(submitCaseCreationForm(event)));
}else{
response.getWriter().write(gson.toJson(createCaseInputCard(event,newHashMap<String,String>(),false)));
}
}
/**
 * Produces a support case creation form.
 * 
 * @param event The event object.
 * @param errors A map of per-field error messages.
 * @param isUpdate Whether to return the form as an update card navigation.
 * @return The resulting card or action response.
 */
JsonObjectcreateCaseInputCard(JsonObjectevent,Map<String,String>errors,booleanisUpdate){
JsonObjectcardHeader=newJsonObject();
cardHeader.add("title",newJsonPrimitive("Create a support case"));
JsonObjectcardSectionTextInput1=newJsonObject();
cardSectionTextInput1.add("name",newJsonPrimitive("name"));
cardSectionTextInput1.add("label",newJsonPrimitive("Name"));
JsonObjectcardSectionTextInput1Widget=newJsonObject();
cardSectionTextInput1Widget.add("textInput",cardSectionTextInput1);
JsonObjectcardSectionTextInput2=newJsonObject();
cardSectionTextInput2.add("name",newJsonPrimitive("description"));
cardSectionTextInput2.add("label",newJsonPrimitive("Description"));
cardSectionTextInput2.add("type",newJsonPrimitive("MULTIPLE_LINE"));
JsonObjectcardSectionTextInput2Widget=newJsonObject();
cardSectionTextInput2Widget.add("textInput",cardSectionTextInput2);
JsonObjectcardSectionSelectionInput1ItemsItem1=newJsonObject();
cardSectionSelectionInput1ItemsItem1.add("text",newJsonPrimitive("P0"));
cardSectionSelectionInput1ItemsItem1.add("value",newJsonPrimitive("P0"));
JsonObjectcardSectionSelectionInput1ItemsItem2=newJsonObject();
cardSectionSelectionInput1ItemsItem2.add("text",newJsonPrimitive("P1"));
cardSectionSelectionInput1ItemsItem2.add("value",newJsonPrimitive("P1"));
JsonObjectcardSectionSelectionInput1ItemsItem3=newJsonObject();
cardSectionSelectionInput1ItemsItem3.add("text",newJsonPrimitive("P2"));
cardSectionSelectionInput1ItemsItem3.add("value",newJsonPrimitive("P2"));
JsonObjectcardSectionSelectionInput1ItemsItem4=newJsonObject();
cardSectionSelectionInput1ItemsItem4.add("text",newJsonPrimitive("P3"));
cardSectionSelectionInput1ItemsItem4.add("value",newJsonPrimitive("P3"));
JsonArraycardSectionSelectionInput1Items=newJsonArray();
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);
cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);
JsonObjectcardSectionSelectionInput1=newJsonObject();
cardSectionSelectionInput1.add("name",newJsonPrimitive("priority"));
cardSectionSelectionInput1.add("label",newJsonPrimitive("Priority"));
cardSectionSelectionInput1.add("type",newJsonPrimitive("DROPDOWN"));
cardSectionSelectionInput1.add("items",cardSectionSelectionInput1Items);
JsonObjectcardSectionSelectionInput1Widget=newJsonObject();
cardSectionSelectionInput1Widget.add("selectionInput",cardSectionSelectionInput1);
JsonObjectcardSectionSelectionInput2ItemsItem=newJsonObject();
cardSectionSelectionInput2ItemsItem.add("text",newJsonPrimitive("Blocks a critical customer operation"));
cardSectionSelectionInput2ItemsItem.add("value",newJsonPrimitive("Blocks a critical customer operation"));
JsonArraycardSectionSelectionInput2Items=newJsonArray();
cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);
JsonObjectcardSectionSelectionInput2=newJsonObject();
cardSectionSelectionInput2.add("name",newJsonPrimitive("impact"));
cardSectionSelectionInput2.add("label",newJsonPrimitive("Impact"));
cardSectionSelectionInput2.add("items",cardSectionSelectionInput2Items);
JsonObjectcardSectionSelectionInput2Widget=newJsonObject();
cardSectionSelectionInput2Widget.add("selectionInput",cardSectionSelectionInput2);
JsonObjectcardSectionButtonListButtonActionParametersParameter=newJsonObject();
cardSectionButtonListButtonActionParametersParameter.add("key",newJsonPrimitive("submitCaseCreationForm"));
cardSectionButtonListButtonActionParametersParameter.add("value",newJsonPrimitive(true));
JsonArraycardSectionButtonListButtonActionParameters=newJsonArray();
cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);
JsonObjectcardSectionButtonListButtonAction=newJsonObject();
cardSectionButtonListButtonAction.add("function",newJsonPrimitive(System.getenv().get("URL")));
cardSectionButtonListButtonAction.add("parameters",cardSectionButtonListButtonActionParameters);
cardSectionButtonListButtonAction.add("persistValues",newJsonPrimitive(true));
JsonObjectcardSectionButtonListButtonOnCLick=newJsonObject();
cardSectionButtonListButtonOnCLick.add("action",cardSectionButtonListButtonAction);
JsonObjectcardSectionButtonListButton=newJsonObject();
cardSectionButtonListButton.add("text",newJsonPrimitive("Create"));
cardSectionButtonListButton.add("onClick",cardSectionButtonListButtonOnCLick);
JsonArraycardSectionButtonListButtons=newJsonArray();
cardSectionButtonListButtons.add(cardSectionButtonListButton);
JsonObjectcardSectionButtonList=newJsonObject();
cardSectionButtonList.add("buttons",cardSectionButtonListButtons);
JsonObjectcardSectionButtonListWidget=newJsonObject();
cardSectionButtonListWidget.add("buttonList",cardSectionButtonList);
// Builds the form inputs with error texts for invalid values.
JsonArraycardSection=newJsonArray();
if(errors.containsKey("name")){
cardSection.add(createErrorTextParagraph(errors.get("name").toString()));
}
cardSection.add(cardSectionTextInput1Widget);
if(errors.containsKey("description")){
cardSection.add(createErrorTextParagraph(errors.get("description").toString()));
}
cardSection.add(cardSectionTextInput2Widget);
if(errors.containsKey("priority")){
cardSection.add(createErrorTextParagraph(errors.get("priority").toString()));
}
cardSection.add(cardSectionSelectionInput1Widget);
if(errors.containsKey("impact")){
cardSection.add(createErrorTextParagraph(errors.get("impact").toString()));
}
cardSection.add(cardSectionSelectionInput2Widget);
cardSection.add(cardSectionButtonListWidget);
JsonObjectcardSectionWidgets=newJsonObject();
cardSectionWidgets.add("widgets",cardSection);
JsonArraysections=newJsonArray();
sections.add(cardSectionWidgets);
JsonObjectcard=newJsonObject();
card.add("header",cardHeader);
card.add("sections",sections);
JsonObjectnavigation=newJsonObject();
if(isUpdate){
navigation.add("updateCard",card);
}else{
navigation.add("pushCard",card);
}
JsonArraynavigations=newJsonArray();
navigations.add(navigation);
JsonObjectaction=newJsonObject();
action.add("navigations",navigations);
JsonObjectrenderActions=newJsonObject();
renderActions.add("action",action);
if(!isUpdate){
returnrenderActions;
}
JsonObjectupdate=newJsonObject();
update.add("renderActions",renderActions);
returnupdate;
}
/**
 * Submits the creation form. If valid, returns a render action
 * that inserts a new link into the document. If invalid, returns an
 * update card navigation that re-renders the creation form with error messages.
 * 
 * @param event The event object with form input values.
 * @return The resulting response.
 */
JsonObjectsubmitCaseCreationForm(JsonObjectevent)throwsException{
JsonObjectformInputs=event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");
Map<String,String>caseDetails=newHashMap<String,String>();
if(formInputs!=null){
if(formInputs.has("name")){
caseDetails.put("name",formInputs.getAsJsonObject("name").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("description")){
caseDetails.put("description",formInputs.getAsJsonObject("description").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("priority")){
caseDetails.put("priority",formInputs.getAsJsonObject("priority").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
if(formInputs.has("impact")){
caseDetails.put("impact",formInputs.getAsJsonObject("impact").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString());
}
}
Map<String,String>errors=validateFormInputs(caseDetails);
if(errors.size() > 0){
returncreateCaseInputCard(event,errors,/* isUpdate= */true);
}else{
Stringtitle=String.format("Case %s",caseDetails.get("name"));
// Adds the case details as parameters to the generated link URL.
URIBuilderuriBuilder=newURIBuilder("https://example.com/support/cases/");
for(StringcaseDetailKey:caseDetails.keySet()){
uriBuilder.addParameter(caseDetailKey,caseDetails.get(caseDetailKey));
}
returncreateLinkRenderAction(title,uriBuilder.build().toURL().toString());
}
}
/**
 * Validates case creation form input values.
 * 
 * @param caseDetails The values of each form input submitted by the user.
 * @return A map from field name to error message. An empty object
 * represents a valid form submission.
 */
Map<String,String>validateFormInputs(Map<String,String>caseDetails){
Map<String,String>errors=newHashMap<String,String>();
if(!caseDetails.containsKey("name")){
errors.put("name","You must provide a name");
}
if(!caseDetails.containsKey("description")){
errors.put("description","You must provide a description");
}
if(!caseDetails.containsKey("priority")){
errors.put("priority","You must provide a priority");
}
if(caseDetails.containsKey("impact") && !Arrays.asList(newString[]{"P0","P1"}).contains(caseDetails.get("priority"))){
errors.put("impact","If an issue blocks a critical customer operation, priority must be P0 or P1");
}
returnerrors;
}
/**
 * Returns a text paragraph with red text indicating a form field validation error.
 * 
 * @param errorMessage A description of input value error.
 * @return The resulting text paragraph.
 */
JsonObjectcreateErrorTextParagraph(StringerrorMessage){
JsonObjecttextParagraph=newJsonObject();
textParagraph.add("text",newJsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> "+errorMessage+"</font>"));
JsonObjecttextParagraphWidget=newJsonObject();
textParagraphWidget.add("textParagraph",textParagraph);
returntextParagraphWidget;
}
/**
 * Returns a submit form response that inserts a link into the document.
 * 
 * @param title The title of the link to insert.
 * @param url The URL of the link to insert.
 * @return The resulting submit form response.
 */
JsonObjectcreateLinkRenderAction(Stringtitle,Stringurl){
JsonObjectlink=newJsonObject();
link.add("title",newJsonPrimitive(title));
link.add("url",newJsonPrimitive(url));
JsonArraylinks=newJsonArray();
links.add(link);
JsonObjectaction=newJsonObject();
action.add("links",links);
JsonObjectrenderActions=newJsonObject();
renderActions.add("action",action);
JsonObjectlinkRenderAction=newJsonObject();
linkRenderAction.add("renderActions",renderActions);
returnlinkRenderAction;
}
}

The following code shows how to implement a link preview for the created resource:

java/3p-resources/src/main/java/CreateLinkPreview.java
/**
 * Copyright 2024 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
importcom.google.cloud.functions.HttpFunction;
importcom.google.cloud.functions.HttpRequest;
importcom.google.cloud.functions.HttpResponse;
importcom.google.gson.Gson;
importcom.google.gson.JsonArray;
importcom.google.gson.JsonObject;
importcom.google.gson.JsonPrimitive;
importjava.io.UnsupportedEncodingException;
importjava.net.URL;
importjava.net.URLDecoder;
importjava.util.HashMap;
importjava.util.Map;
publicclass CreateLinkPreviewimplementsHttpFunction{
privatestaticfinalGsongson=newGson();
/**
 * Responds to any HTTP request related to link previews.
 *
 * @param request An HTTP request context.
 * @param response An HTTP response context.
 */
@Override
publicvoidservice(HttpRequestrequest,HttpResponseresponse)throwsException{
JsonObjectevent=gson.fromJson(request.getReader(),JsonObject.class);
Stringurl=event.getAsJsonObject("docs")
.getAsJsonObject("matchedUrl")
.get("url")
.getAsString();
URLparsedURL=newURL(url);
// If the event object URL matches a specified pattern for preview links.
if("example.com".equals(parsedURL.getHost())){
if(parsedURL.getPath().startsWith("/support/cases/")){
response.getWriter().write(gson.toJson(caseLinkPreview(parsedURL)));
return;
}
}
response.getWriter().write("{}");
}
/**
 * A support case link preview.
 *
 * @param url A matching URL.
 * @return The resulting preview link card.
 */
JsonObjectcaseLinkPreview(URLurl)throwsUnsupportedEncodingException{
// Parses the URL and identify the case details.
Map<String,String>caseDetails=newHashMap<String,String>();
for(Stringpair:url.getQuery().split("&")){
caseDetails.put(URLDecoder.decode(pair.split("=")[0],"UTF-8"),URLDecoder.decode(pair.split("=")[1],"UTF-8"));
}
// Builds a preview card with the case name, and description
// Uses the text from the card's header for the title of the smart chip.
JsonObjectcardHeader=newJsonObject();
StringcaseName=String.format("Case %s",caseDetails.get("name"));
cardHeader.add("title",newJsonPrimitive(caseName));
JsonObjecttextParagraph=newJsonObject();
textParagraph.add("text",newJsonPrimitive(caseDetails.get("description")));
JsonObjectwidget=newJsonObject();
widget.add("textParagraph",textParagraph);
JsonArraywidgets=newJsonArray();
widgets.add(widget);
JsonObjectsection=newJsonObject();
section.add("widgets",widgets);
JsonArraysections=newJsonArray();
sections.add(section);
JsonObjectpreviewCard=newJsonObject();
previewCard.add("header",cardHeader);
previewCard.add("sections",sections);
JsonObjectlinkPreview=newJsonObject();
linkPreview.add("title",newJsonPrimitive(caseName));
linkPreview.add("previewCard",previewCard);
JsonObjectaction=newJsonObject();
action.add("linkPreview",linkPreview);
JsonObjectrenderActions=newJsonObject();
renderActions.add("action",action);
returnrenderActions;
}
}

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 License, and code samples are licensed under the Apache 2.0 License. For details, see the Google Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2025年10月13日 UTC.