使用 @ 選單建立第三方資源

本頁面說明如何建構 Google Workspace 外掛程式,讓 Google 文件使用者在 Google 文件中透過第三方服務建立資源,例如支援案件或專案工作。

您可以使用 Google Workspace 外掛程式,將服務新增至 Google 文件的 @ 選單。外掛程式會新增可讓使用者透過 Google 文件中的表單對話方塊,在服務中建立資源的選單項目。

使用者建立資源的方式

如要在 Google 文件文件中建立服務資源,使用者可以在文件中輸入 @,然後從 @ 選單中選取服務:

使用者預覽資訊卡

當使用者在文件中輸入 @ 並選取您的服務時,您會向他們顯示資訊卡,其中包含使用者建立資源所需的表單輸入內容。使用者提交資源建立表單後,外掛程式應在服務中建立資源,並產生指向該資源的網址。

外掛程式會在建立的資源文件中插入方塊。當使用者將游標停留在這個方塊上時,系統會叫用外掛程式的相關連結預覽觸發事件。請確認你的外掛程式會插入連結模式,並且支援連結預覽觸發條件。

必要條件

Apps Script

  • 這項 Google Workspace 外掛程式可支援使用者建立的資源連結模式的連結預覽。如要建構含有連結預覽畫面的外掛程式,請參閱「使用智慧型方塊預覽連結」一文。

Node.js

  • 這項 Google Workspace 外掛程式可支援使用者建立的資源連結模式的連結預覽。如要建構含有連結預覽畫面的外掛程式,請參閱「使用智慧型方塊預覽連結」一文。

Python

  • 這項 Google Workspace 外掛程式可支援使用者建立的資源連結模式的連結預覽。如要建構含有連結預覽畫面的外掛程式,請參閱「使用智慧型方塊預覽連結」一文。

Java

  • 這項 Google Workspace 外掛程式可支援使用者建立的資源連結模式的連結預覽。如要建構含有連結預覽畫面的外掛程式,請參閱「使用智慧型方塊預覽連結」一文。

為外掛程式設定資源建立作業

本節說明如何設定外掛程式的資源建立作業,包括下列步驟:

  1. 在外掛程式的資訊清單中設定資源建立程序
  2. 建立使用者需要在服務中建立資源的表單卡片。
  3. 處理表單提交作業,以便在使用者提交表單時執行建立資源的函式。

設定資源建立作業

如要設定資源建立作業,請在外掛程式的資訊清單中指定下列部分和欄位:

  1. docs 欄位的 addOns 部分下方,實作包含 runFunctioncreateActionTriggers 觸發事件。(您會在後續的「建立表單資訊卡」一節中定義此函式)。

    如要瞭解可在 createActionTriggers 觸發事件中指定哪些欄位,請參閱 Apps Script 資訊清單其他執行階段的部署資源的參考說明文件。

  2. oauthScopes 欄位中新增範圍 https://www.googleapis.com/auth/workspace.linkcreate,方便使用者授權外掛程式建立資源。具體來說,這個範圍可讓外掛程式讀取使用者提交至資源建立表單的資訊,並根據該資訊在文件中插入智慧方塊。

舉例來說,請參閱資訊清單的 addons 部分,瞭解如何為下列支援案件服務設定資源建立作業:

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

在這個範例中,Google Workspace 外掛程式可讓使用者建立客服案件。每個 createActionTriggers 觸發事件都必須包含下列欄位:

  • 專屬 ID
  • Google 文件 @ 選單中顯示的文字標籤
  • 指向圖示的標誌網址,該圖示會顯示在 @ 選單中標籤文字旁邊
  • 回呼函式,參照 Apps Script 函式或傳回卡片的 HTTP 端點

建立表單資訊卡

如要透過 Google 文件 @ 選單在服務中建立資源,您必須實作 createActionTriggers 物件中指定的所有函式。

當使用者與其中一個選單項目互動時,系統會觸發對應的 createActionTriggers 觸發事件,其回呼函式會顯示含有表單輸入內容的資訊卡,以便建立資源。

支援的元素和動作

如要建立資訊卡介面,您可以使用小工具來顯示使用者建立資源所需的資訊和輸入內容。大部分的 Google Workspace 外掛程式小工具和動作都支援,但以下項目除外:

  • 不支援資訊卡頁尾。
  • 不支援通知。
  • 導覽功能僅支援 updateCard 導覽。

含表單輸入欄位的資訊卡範例

以下範例顯示 Apps Script 回呼函式,當使用者從 @ 選單中選取「Create support case」時,會顯示資訊卡:

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.  */ function createCaseInputCard(event, errors, isUpdate) {    const cardHeader = CardService.newCardHeader()     .setTitle('Create a support case')    const cardSectionTextInput1 = CardService.newTextInput()     .setFieldName('name')     .setTitle('Name')     .setMultiline(false);    const cardSectionTextInput2 = CardService.newTextInput()     .setFieldName('description')     .setTitle('Description')     .setMultiline(true);    const cardSectionSelectionInput1 = 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);    const cardSectionSelectionInput2 = CardService.newSelectionInput()     .setFieldName('impact')     .setTitle('Impact')     .setType(CardService.SelectionInputType.CHECK_BOX)     .addItem('Blocks a critical customer operation', 'Blocks a critical customer operation', false);    const cardSectionButtonListButtonAction = CardService.newAction()     .setPersistValues(true)     .setFunctionName('submitCaseCreationForm')     .setParameters({});    const cardSectionButtonListButton = CardService.newTextButton()     .setText('Create')     .setTextButtonStyle(CardService.TextButtonStyle.TEXT)     .setOnClickAction(cardSectionButtonListButtonAction);    const cardSectionButtonList = CardService.newButtonSet()     .addButton(cardSectionButtonListButton);    // Builds the form inputs with error texts for invalid values.   const cardSection = 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);    const card = CardService.newCardBuilder()     .setHeader(cardHeader)     .addSection(cardSection)     .build();    if (isUpdate) {     return CardService.newActionResponseBuilder()       .setNavigation(CardService.newNavigation().updateCard(card))       .build();   } else {     return card;   } }

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.  */ function createCaseInputCard(event, errors, isUpdate) {    const cardHeader1 = {     title: "Create a support case"   };    const cardSection1TextInput1 = {     textInput: {       name: "name",       label: "Name"     }   };    const cardSection1TextInput2 = {     textInput: {       name: "description",       label: "Description",       type: "MULTIPLE_LINE"     }   };    const cardSection1SelectionInput1 = {     selectionInput: {       name: "priority",       label: "Priority",       type: "DROPDOWN",       items: [{         text: "P0",         value: "P0"       }, {         text: "P1",         value: "P1"       }, {         text: "P2",         value: "P2"       }, {         text: "P3",         value: "P3"       }]     }   };    const cardSection1SelectionInput2 = {     selectionInput: {       name: "impact",       label: "Impact",       items: [{         text: "Blocks a critical customer operation",         value: "Blocks a critical customer operation"       }]     }   };    const cardSection1ButtonList1Button1Action1 = {     function: process.env.URL,     parameters: [       {         key: "submitCaseCreationForm",         value: true       }     ],     persistValues: true   };    const cardSection1ButtonList1Button1 = {     text: "Create",     onClick: {       action: cardSection1ButtonList1Button1Action1     }   };    const cardSection1ButtonList1 = {     buttonList: {       buttons: [cardSection1ButtonList1Button1]     }   };    // Builds the creation form and adds error text for invalid inputs.   const cardSection1 = [];   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);    const card = {     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
def create_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.  */ JsonObject createCaseInputCard(JsonObject event, Map<String, String> errors, boolean isUpdate) {   JsonObject cardHeader = new JsonObject();   cardHeader.add("title", new JsonPrimitive("Create a support case"));    JsonObject cardSectionTextInput1 = new JsonObject();   cardSectionTextInput1.add("name", new JsonPrimitive("name"));   cardSectionTextInput1.add("label", new JsonPrimitive("Name"));    JsonObject cardSectionTextInput1Widget = new JsonObject();   cardSectionTextInput1Widget.add("textInput", cardSectionTextInput1);    JsonObject cardSectionTextInput2 = new JsonObject();   cardSectionTextInput2.add("name", new JsonPrimitive("description"));   cardSectionTextInput2.add("label", new JsonPrimitive("Description"));   cardSectionTextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE"));    JsonObject cardSectionTextInput2Widget = new JsonObject();   cardSectionTextInput2Widget.add("textInput", cardSectionTextInput2);    JsonObject cardSectionSelectionInput1ItemsItem1 = new JsonObject();   cardSectionSelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0"));   cardSectionSelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0"));    JsonObject cardSectionSelectionInput1ItemsItem2 = new JsonObject();   cardSectionSelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1"));   cardSectionSelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1"));    JsonObject cardSectionSelectionInput1ItemsItem3 = new JsonObject();   cardSectionSelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2"));   cardSectionSelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2"));    JsonObject cardSectionSelectionInput1ItemsItem4 = new JsonObject();   cardSectionSelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3"));   cardSectionSelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3"));    JsonArray cardSectionSelectionInput1Items = new JsonArray();   cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);   cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);   cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);   cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);    JsonObject cardSectionSelectionInput1 = new JsonObject();   cardSectionSelectionInput1.add("name", new JsonPrimitive("priority"));   cardSectionSelectionInput1.add("label", new JsonPrimitive("Priority"));   cardSectionSelectionInput1.add("type", new JsonPrimitive("DROPDOWN"));   cardSectionSelectionInput1.add("items", cardSectionSelectionInput1Items);    JsonObject cardSectionSelectionInput1Widget = new JsonObject();   cardSectionSelectionInput1Widget.add("selectionInput", cardSectionSelectionInput1);    JsonObject cardSectionSelectionInput2ItemsItem = new JsonObject();   cardSectionSelectionInput2ItemsItem.add("text", new JsonPrimitive("Blocks a critical customer operation"));   cardSectionSelectionInput2ItemsItem.add("value", new JsonPrimitive("Blocks a critical customer operation"));    JsonArray cardSectionSelectionInput2Items = new JsonArray();   cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);    JsonObject cardSectionSelectionInput2 = new JsonObject();   cardSectionSelectionInput2.add("name", new JsonPrimitive("impact"));   cardSectionSelectionInput2.add("label", new JsonPrimitive("Impact"));   cardSectionSelectionInput2.add("items", cardSectionSelectionInput2Items);    JsonObject cardSectionSelectionInput2Widget = new JsonObject();   cardSectionSelectionInput2Widget.add("selectionInput", cardSectionSelectionInput2);    JsonObject cardSectionButtonListButtonActionParametersParameter = new JsonObject();   cardSectionButtonListButtonActionParametersParameter.add("key", new JsonPrimitive("submitCaseCreationForm"));   cardSectionButtonListButtonActionParametersParameter.add("value", new JsonPrimitive(true));    JsonArray cardSectionButtonListButtonActionParameters = new JsonArray();   cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);    JsonObject cardSectionButtonListButtonAction = new JsonObject();   cardSectionButtonListButtonAction.add("function", new JsonPrimitive(System.getenv().get("URL")));   cardSectionButtonListButtonAction.add("parameters", cardSectionButtonListButtonActionParameters);   cardSectionButtonListButtonAction.add("persistValues", new JsonPrimitive(true));    JsonObject cardSectionButtonListButtonOnCLick = new JsonObject();   cardSectionButtonListButtonOnCLick.add("action", cardSectionButtonListButtonAction);    JsonObject cardSectionButtonListButton = new JsonObject();   cardSectionButtonListButton.add("text", new JsonPrimitive("Create"));   cardSectionButtonListButton.add("onClick", cardSectionButtonListButtonOnCLick);    JsonArray cardSectionButtonListButtons = new JsonArray();   cardSectionButtonListButtons.add(cardSectionButtonListButton);    JsonObject cardSectionButtonList = new JsonObject();   cardSectionButtonList.add("buttons", cardSectionButtonListButtons);    JsonObject cardSectionButtonListWidget = new JsonObject();   cardSectionButtonListWidget.add("buttonList", cardSectionButtonList);    // Builds the form inputs with error texts for invalid values.   JsonArray cardSection = new JsonArray();   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);    JsonObject cardSectionWidgets = new JsonObject();   cardSectionWidgets.add("widgets", cardSection);    JsonArray sections = new JsonArray();   sections.add(cardSectionWidgets);    JsonObject card = new JsonObject();   card.add("header", cardHeader);   card.add("sections", sections);    JsonObject navigation = new JsonObject();   if (isUpdate) {     navigation.add("updateCard", card);   } else {     navigation.add("pushCard", card);   }    JsonArray navigations = new JsonArray();   navigations.add(navigation);    JsonObject action = new JsonObject();   action.add("navigations", navigations);    JsonObject renderActions = new JsonObject();   renderActions.add("action", action);    if (!isUpdate) {     return renderActions;   }    JsonObject update = new JsonObject();   update.add("renderActions", renderActions);    return update; }

createCaseInputCard 函式會轉譯下列資訊卡:

含表單輸入欄位的資訊卡

資訊卡包含文字輸入欄、下拉式選單和核取方塊。它也包含文字按鈕,其中的 onClick 動作會執行其他函式,以便處理建立表單的提交作業。

使用者填寫表單並按一下「Create」後,外掛程式會將表單輸入內容傳送至 onClick 動作函式 (在本例中稱為 submitCaseCreationForm),此時外掛程式就能驗證輸入內容,並使用這些內容在第三方服務中建立資源。

處理表單提交

使用者提交建立表單後,系統會執行與 onClick 動作相關聯的函式。為了提供理想的使用者體驗,外掛程式應處理表單提交成功和提交失敗的情況。

處理成功建立的資源

外掛程式的 onClick 函式應在第三方服務中建立資源,並產生指向該資源的網址。

為了將資源的網址傳回至 Docs 以建立方塊,onClick 函式應傳回 SubmitFormResponse,其中 renderActions.action.links 中的一維陣列會指向連結。連結標題應代表已建立資源的標題,而網址應會指向該資源。

以下範例顯示已建立資源的 SubmitFormResponse

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.  */ function createLinkRenderAction(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.  */ function createLinkRenderAction(title, url) {   return {     renderActions: {       action: {         links: [{           title: title,           url: url         }]       }     }   }; }

Python

python/3p-resources/create_3p_resources/main.py
def create_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.  */ JsonObject createLinkRenderAction(String title, String url) {   JsonObject link = new JsonObject();   link.add("title", new JsonPrimitive(title));   link.add("url", new JsonPrimitive(url));    JsonArray links = new JsonArray();   links.add(link);    JsonObject action = new JsonObject();   action.add("links", links);    JsonObject renderActions = new JsonObject();   renderActions.add("action", action);    JsonObject linkRenderAction = new JsonObject();   linkRenderAction.add("renderActions", renderActions);    return linkRenderAction; }

SubmitFormResponse 傳回後,模式對話方塊會關閉,外掛程式會在文件中插入方塊。當使用者將游標懸停在這個方塊上時,系統會叫用相關聯的連結預覽觸發事件。請確認外掛程式不會插入連結模式不符合連結預覽觸發條件的方塊。

處理錯誤

如果使用者嘗試提交含有無效欄位的表單,附加元件應傳回顯示錯誤的轉譯動作,而不是傳回含有連結的 SubmitFormResponseupdateCard這樣使用者就能瞭解自己做錯了什麼,並重新嘗試。如要查看 Apps Script 的相關資訊,請參閱 updateCard(card);如要查看其他執行階段的相關資訊,請參閱 updateCard。不支援通知和 pushCard 導覽。

錯誤處理示例

以下範例顯示使用者提交表單時會叫用的程式碼。如果輸入內容無效,資訊卡會更新並顯示錯誤訊息。如果輸入內容有效,外掛程式會傳回 SubmitFormResponse,並附上已建立資源的連結。

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.  */ function submitCaseCreationForm(event) {   const caseDetails = {     name: event.formInput.name,     description: event.formInput.description,     priority: event.formInput.priority,     impact: !!event.formInput.impact,   };    const errors = validateFormInputs(caseDetails);   if (Object.keys(errors).length > 0) {     return createCaseInputCard(event, errors, /* isUpdate= */ true);   } else {     const title = `Case ${caseDetails.name}`;     // Adds the case details as parameters to the generated link URL.     const url = 'https://example.com/support/cases/?' + generateQuery(caseDetails);     return createLinkRenderAction(title, url);   } }  /** * Build a query path with URL parameters. * * @param {!Map} parameters A map with the URL parameters. * @return {!string} The resulting query path. */ function generateQuery(parameters) {   return Object.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.  */ function submitCaseCreationForm(event) {   const caseDetails = {     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],   };    const errors = validateFormInputs(caseDetails);   if (Object.keys(errors).length > 0) {     return createCaseInputCard(event, errors, /* isUpdate= */ true);   } else {     const title = `Case ${caseDetails.name}`;     // Adds the case details as parameters to the generated link URL.     const url = new URL('https://example.com/support/cases/');     for (const [key, value] of Object.entries(caseDetails)) {       url.searchParams.append(key, value);     }     return createLinkRenderAction(title, url.href);   } }

Python

python/3p-resources/create_3p_resources/main.py
def submit_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.  */ JsonObject submitCaseCreationForm(JsonObject event) throws Exception {   JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");   Map<String, String> caseDetails = new HashMap<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) {     return createCaseInputCard(event, errors, /* isUpdate= */ true);   } else {     String title = String.format("Case %s", caseDetails.get("name"));     // Adds the case details as parameters to the generated link URL.     URIBuilder uriBuilder = new URIBuilder("https://example.com/support/cases/");     for (String caseDetailKey : caseDetails.keySet()) {       uriBuilder.addParameter(caseDetailKey, caseDetails.get(caseDetailKey));     }     return createLinkRenderAction(title, uriBuilder.build().toURL().toString());   } }

以下程式碼範例會驗證表單輸入內容,並為無效的輸入內容建立錯誤訊息:

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.  */ function validateFormInputs(caseDetails) {   const errors = {};   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';   }    return errors; }  /**  * 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.  */ function createErrorTextParagraph(errorMessage) {   return CardService.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.  */ function validateFormInputs(caseDetails) {   const errors = {};   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';   }    return errors; }  /**  * 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.  */ function createErrorTextParagraph(errorMessage) {   return {     textParagraph: {       text: '<font color=\"#BA0300\"><b>Error:</b> ' + errorMessage + '</font>'     }   } }

Python

python/3p-resources/create_3p_resources/main.py
def validate_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   def create_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 = new HashMap<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(new String[]{"P0", "P1"}).contains(caseDetails.get("priority"))) {     errors.put("impact", "If an issue blocks a critical customer operation, priority must be P0 or P1");   }    return errors; }  /**  * 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.  */ JsonObject createErrorTextParagraph(String errorMessage) {   JsonObject textParagraph = new JsonObject();   textParagraph.add("text", new JsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> " + errorMessage + "</font>"));    JsonObject textParagraphWidget = new JsonObject();   textParagraphWidget.add("textParagraph", textParagraph);    return textParagraphWidget; }

完整範例:客服案件加購

以下範例顯示 Google Workspace 外掛程式,可預覽公司支援案件的連結,並讓使用者在 Google 文件中建立支援案件。

這個範例會執行以下操作:

  • 產生含有表單欄位的資訊卡,以便透過 Docs @ 選單建立客服案件。
  • 驗證表單輸入內容,並針對無效的輸入內容傳回錯誤訊息。
  • 將建立的客服案件名稱和連結插入 Google 文件文件中,做為智慧型方塊。
  • 預覽客服案件的連結,例如 https://www.example.com/support/cases/1234。智慧型方塊會顯示圖示,預覽資訊卡則會包含案件名稱、優先順序和說明。

資訊清單

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"         }       ]     }   } }

程式碼

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. */ function caseLinkPreview(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.     const caseDetails = parseQuery(event.docs.matchedUrl.url);      // Builds a preview card with the case name, and description     const caseHeader = CardService.newCardHeader()       .setTitle(`Case ${caseDetails["name"][0]}`);     const caseDescription = CardService.newTextParagraph()       .setText(caseDetails["description"][0]);      // Returns the card.     // Uses the text from the card's header for the title of the smart chip.     return CardService.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. */ function parseQuery(url) {   const query = url.split("?")[1];   if (query) {     return query.split("&")     .reduce(function(o, e) {       var temp = e.split("=");       var key = temp[0].trim();       var value = temp[1].trim();       value = isNaN(value) ? value : Number(value);       if (o[key]) {         o[key].push(value);       } else {         o[key] = [value];       }       return o;     }, {});   }   return null; }    /**  * 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.  */ function createCaseInputCard(event, errors, isUpdate) {    const cardHeader = CardService.newCardHeader()     .setTitle('Create a support case')    const cardSectionTextInput1 = CardService.newTextInput()     .setFieldName('name')     .setTitle('Name')     .setMultiline(false);    const cardSectionTextInput2 = CardService.newTextInput()     .setFieldName('description')     .setTitle('Description')     .setMultiline(true);    const cardSectionSelectionInput1 = 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);    const cardSectionSelectionInput2 = CardService.newSelectionInput()     .setFieldName('impact')     .setTitle('Impact')     .setType(CardService.SelectionInputType.CHECK_BOX)     .addItem('Blocks a critical customer operation', 'Blocks a critical customer operation', false);    const cardSectionButtonListButtonAction = CardService.newAction()     .setPersistValues(true)     .setFunctionName('submitCaseCreationForm')     .setParameters({});    const cardSectionButtonListButton = CardService.newTextButton()     .setText('Create')     .setTextButtonStyle(CardService.TextButtonStyle.TEXT)     .setOnClickAction(cardSectionButtonListButtonAction);    const cardSectionButtonList = CardService.newButtonSet()     .addButton(cardSectionButtonListButton);    // Builds the form inputs with error texts for invalid values.   const cardSection = 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);    const card = CardService.newCardBuilder()     .setHeader(cardHeader)     .addSection(cardSection)     .build();    if (isUpdate) {     return CardService.newActionResponseBuilder()       .setNavigation(CardService.newNavigation().updateCard(card))       .build();   } else {     return 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.  */ function submitCaseCreationForm(event) {   const caseDetails = {     name: event.formInput.name,     description: event.formInput.description,     priority: event.formInput.priority,     impact: !!event.formInput.impact,   };    const errors = validateFormInputs(caseDetails);   if (Object.keys(errors).length > 0) {     return createCaseInputCard(event, errors, /* isUpdate= */ true);   } else {     const title = `Case ${caseDetails.name}`;     // Adds the case details as parameters to the generated link URL.     const url = 'https://example.com/support/cases/?' + generateQuery(caseDetails);     return createLinkRenderAction(title, url);   } }  /** * Build a query path with URL parameters. * * @param {!Map} parameters A map with the URL parameters. * @return {!string} The resulting query path. */ function generateQuery(parameters) {   return Object.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.  */ function validateFormInputs(caseDetails) {   const errors = {};   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';   }    return errors; }  /**  * 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.  */ function createErrorTextParagraph(errorMessage) {   return CardService.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.  */ function createLinkRenderAction(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) => {   const event = req.body;   if (event.docs.matchedUrl.url) {     const url = event.docs.matchedUrl.url;     const parsedUrl = new URL(url);     // If the event object URL matches a specified pattern for preview links.     if (parsedUrl.hostname === 'example.com') {       if (parsedUrl.pathname.startsWith('/support/cases/')) {         return res.json(caseLinkPreview(parsedUrl));       }     }   } };   /**  *   * A support case link preview.  *  * @param {!URL} url The event object.  * @return {!Card} The resulting preview link card.  */ function caseLinkPreview(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.   const name = `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) => {   const event = 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.  */ function createCaseInputCard(event, errors, isUpdate) {    const cardHeader1 = {     title: "Create a support case"   };    const cardSection1TextInput1 = {     textInput: {       name: "name",       label: "Name"     }   };    const cardSection1TextInput2 = {     textInput: {       name: "description",       label: "Description",       type: "MULTIPLE_LINE"     }   };    const cardSection1SelectionInput1 = {     selectionInput: {       name: "priority",       label: "Priority",       type: "DROPDOWN",       items: [{         text: "P0",         value: "P0"       }, {         text: "P1",         value: "P1"       }, {         text: "P2",         value: "P2"       }, {         text: "P3",         value: "P3"       }]     }   };    const cardSection1SelectionInput2 = {     selectionInput: {       name: "impact",       label: "Impact",       items: [{         text: "Blocks a critical customer operation",         value: "Blocks a critical customer operation"       }]     }   };    const cardSection1ButtonList1Button1Action1 = {     function: process.env.URL,     parameters: [       {         key: "submitCaseCreationForm",         value: true       }     ],     persistValues: true   };    const cardSection1ButtonList1Button1 = {     text: "Create",     onClick: {       action: cardSection1ButtonList1Button1Action1     }   };    const cardSection1ButtonList1 = {     buttonList: {       buttons: [cardSection1ButtonList1Button1]     }   };    // Builds the creation form and adds error text for invalid inputs.   const cardSection1 = [];   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);    const card = {     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.  */ function submitCaseCreationForm(event) {   const caseDetails = {     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],   };    const errors = validateFormInputs(caseDetails);   if (Object.keys(errors).length > 0) {     return createCaseInputCard(event, errors, /* isUpdate= */ true);   } else {     const title = `Case ${caseDetails.name}`;     // Adds the case details as parameters to the generated link URL.     const url = new URL('https://example.com/support/cases/');     for (const [key, value] of Object.entries(caseDetails)) {       url.searchParams.append(key, value);     }     return createLinkRenderAction(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.  */ function validateFormInputs(caseDetails) {   const errors = {};   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';   }    return errors; }  /**  * 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.  */ function createErrorTextParagraph(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.  */ function createLinkRenderAction(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.  from typing import Any, Mapping from urllib.parse import urlencode  import os import flask import functions_framework   @functions_framework.http def create_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)     def create_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                 }]             }         }     def submit_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)     def validate_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   def create_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>'         }     }     def create_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                 }]             }         }     }

以下程式碼說明如何為已建立的資源實作連結預覽:

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.  from typing import Any, Mapping from urllib.parse import urlparse, parse_qs  import flask import functions_framework   @functions_framework.http def create_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 {}     def case_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.  */  import java.util.Arrays; import java.util.HashMap; import java.util.Map;  import org.apache.http.client.utils.URIBuilder;  import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive;  public class Create3pResources implements HttpFunction {   private static final Gson gson = new Gson();    /**    * Responds to any HTTP request related to 3p resource creations.    *    * @param request  An HTTP request context.    * @param response An HTTP response context.    */   @Override   public void service(HttpRequest request, HttpResponse response) throws Exception {     JsonObject event = gson.fromJson(request.getReader(), JsonObject.class);     JsonObject parameters = 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, new HashMap<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.    */   JsonObject createCaseInputCard(JsonObject event, Map<String, String> errors, boolean isUpdate) {     JsonObject cardHeader = new JsonObject();     cardHeader.add("title", new JsonPrimitive("Create a support case"));      JsonObject cardSectionTextInput1 = new JsonObject();     cardSectionTextInput1.add("name", new JsonPrimitive("name"));     cardSectionTextInput1.add("label", new JsonPrimitive("Name"));      JsonObject cardSectionTextInput1Widget = new JsonObject();     cardSectionTextInput1Widget.add("textInput", cardSectionTextInput1);      JsonObject cardSectionTextInput2 = new JsonObject();     cardSectionTextInput2.add("name", new JsonPrimitive("description"));     cardSectionTextInput2.add("label", new JsonPrimitive("Description"));     cardSectionTextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE"));      JsonObject cardSectionTextInput2Widget = new JsonObject();     cardSectionTextInput2Widget.add("textInput", cardSectionTextInput2);      JsonObject cardSectionSelectionInput1ItemsItem1 = new JsonObject();     cardSectionSelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0"));     cardSectionSelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0"));      JsonObject cardSectionSelectionInput1ItemsItem2 = new JsonObject();     cardSectionSelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1"));     cardSectionSelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1"));      JsonObject cardSectionSelectionInput1ItemsItem3 = new JsonObject();     cardSectionSelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2"));     cardSectionSelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2"));      JsonObject cardSectionSelectionInput1ItemsItem4 = new JsonObject();     cardSectionSelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3"));     cardSectionSelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3"));      JsonArray cardSectionSelectionInput1Items = new JsonArray();     cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);     cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);     cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);     cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);      JsonObject cardSectionSelectionInput1 = new JsonObject();     cardSectionSelectionInput1.add("name", new JsonPrimitive("priority"));     cardSectionSelectionInput1.add("label", new JsonPrimitive("Priority"));     cardSectionSelectionInput1.add("type", new JsonPrimitive("DROPDOWN"));     cardSectionSelectionInput1.add("items", cardSectionSelectionInput1Items);      JsonObject cardSectionSelectionInput1Widget = new JsonObject();     cardSectionSelectionInput1Widget.add("selectionInput", cardSectionSelectionInput1);      JsonObject cardSectionSelectionInput2ItemsItem = new JsonObject();     cardSectionSelectionInput2ItemsItem.add("text", new JsonPrimitive("Blocks a critical customer operation"));     cardSectionSelectionInput2ItemsItem.add("value", new JsonPrimitive("Blocks a critical customer operation"));      JsonArray cardSectionSelectionInput2Items = new JsonArray();     cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);      JsonObject cardSectionSelectionInput2 = new JsonObject();     cardSectionSelectionInput2.add("name", new JsonPrimitive("impact"));     cardSectionSelectionInput2.add("label", new JsonPrimitive("Impact"));     cardSectionSelectionInput2.add("items", cardSectionSelectionInput2Items);      JsonObject cardSectionSelectionInput2Widget = new JsonObject();     cardSectionSelectionInput2Widget.add("selectionInput", cardSectionSelectionInput2);      JsonObject cardSectionButtonListButtonActionParametersParameter = new JsonObject();     cardSectionButtonListButtonActionParametersParameter.add("key", new JsonPrimitive("submitCaseCreationForm"));     cardSectionButtonListButtonActionParametersParameter.add("value", new JsonPrimitive(true));      JsonArray cardSectionButtonListButtonActionParameters = new JsonArray();     cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);      JsonObject cardSectionButtonListButtonAction = new JsonObject();     cardSectionButtonListButtonAction.add("function", new JsonPrimitive(System.getenv().get("URL")));     cardSectionButtonListButtonAction.add("parameters", cardSectionButtonListButtonActionParameters);     cardSectionButtonListButtonAction.add("persistValues", new JsonPrimitive(true));      JsonObject cardSectionButtonListButtonOnCLick = new JsonObject();     cardSectionButtonListButtonOnCLick.add("action", cardSectionButtonListButtonAction);      JsonObject cardSectionButtonListButton = new JsonObject();     cardSectionButtonListButton.add("text", new JsonPrimitive("Create"));     cardSectionButtonListButton.add("onClick", cardSectionButtonListButtonOnCLick);      JsonArray cardSectionButtonListButtons = new JsonArray();     cardSectionButtonListButtons.add(cardSectionButtonListButton);      JsonObject cardSectionButtonList = new JsonObject();     cardSectionButtonList.add("buttons", cardSectionButtonListButtons);      JsonObject cardSectionButtonListWidget = new JsonObject();     cardSectionButtonListWidget.add("buttonList", cardSectionButtonList);      // Builds the form inputs with error texts for invalid values.     JsonArray cardSection = new JsonArray();     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);      JsonObject cardSectionWidgets = new JsonObject();     cardSectionWidgets.add("widgets", cardSection);      JsonArray sections = new JsonArray();     sections.add(cardSectionWidgets);      JsonObject card = new JsonObject();     card.add("header", cardHeader);     card.add("sections", sections);      JsonObject navigation = new JsonObject();     if (isUpdate) {       navigation.add("updateCard", card);     } else {       navigation.add("pushCard", card);     }      JsonArray navigations = new JsonArray();     navigations.add(navigation);      JsonObject action = new JsonObject();     action.add("navigations", navigations);      JsonObject renderActions = new JsonObject();     renderActions.add("action", action);      if (!isUpdate) {       return renderActions;     }      JsonObject update = new JsonObject();     update.add("renderActions", renderActions);      return update;   }     /**    * 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.    */   JsonObject submitCaseCreationForm(JsonObject event) throws Exception {     JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");     Map<String, String> caseDetails = new HashMap<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) {       return createCaseInputCard(event, errors, /* isUpdate= */ true);     } else {       String title = String.format("Case %s", caseDetails.get("name"));       // Adds the case details as parameters to the generated link URL.       URIBuilder uriBuilder = new URIBuilder("https://example.com/support/cases/");       for (String caseDetailKey : caseDetails.keySet()) {         uriBuilder.addParameter(caseDetailKey, caseDetails.get(caseDetailKey));       }       return createLinkRenderAction(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 = new HashMap<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(new String[]{"P0", "P1"}).contains(caseDetails.get("priority"))) {       errors.put("impact", "If an issue blocks a critical customer operation, priority must be P0 or P1");     }      return errors;   }    /**    * 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.    */   JsonObject createErrorTextParagraph(String errorMessage) {     JsonObject textParagraph = new JsonObject();     textParagraph.add("text", new JsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> " + errorMessage + "</font>"));      JsonObject textParagraphWidget = new JsonObject();     textParagraphWidget.add("textParagraph", textParagraph);      return textParagraphWidget;   }     /**    * 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.    */   JsonObject createLinkRenderAction(String title, String url) {     JsonObject link = new JsonObject();     link.add("title", new JsonPrimitive(title));     link.add("url", new JsonPrimitive(url));      JsonArray links = new JsonArray();     links.add(link);      JsonObject action = new JsonObject();     action.add("links", links);      JsonObject renderActions = new JsonObject();     renderActions.add("action", action);      JsonObject linkRenderAction = new JsonObject();     linkRenderAction.add("renderActions", renderActions);      return linkRenderAction;   }  }

以下程式碼說明如何為已建立的資源實作連結預覽:

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.  */  import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive;  import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map;  public class CreateLinkPreview implements HttpFunction {   private static final Gson gson = new Gson();    /**    * Responds to any HTTP request related to link previews.    *    * @param request An HTTP request context.    * @param response An HTTP response context.    */   @Override   public void service(HttpRequest request, HttpResponse response) throws Exception {     JsonObject event = gson.fromJson(request.getReader(), JsonObject.class);     String url = event.getAsJsonObject("docs")         .getAsJsonObject("matchedUrl")         .get("url")         .getAsString();     URL parsedURL = new URL(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.    */   JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException {     // Parses the URL and identify the case details.     Map<String, String> caseDetails = new HashMap<String, String>();     for (String pair : 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.     JsonObject cardHeader = new JsonObject();     String caseName = String.format("Case %s", caseDetails.get("name"));     cardHeader.add("title", new JsonPrimitive(caseName));      JsonObject textParagraph = new JsonObject();     textParagraph.add("text", new JsonPrimitive(caseDetails.get("description")));      JsonObject widget = new JsonObject();     widget.add("textParagraph", textParagraph);      JsonArray widgets = new JsonArray();     widgets.add(widget);      JsonObject section = new JsonObject();     section.add("widgets", widgets);      JsonArray sections = new JsonArray();     sections.add(section);      JsonObject previewCard = new JsonObject();     previewCard.add("header", cardHeader);     previewCard.add("sections", sections);      JsonObject linkPreview = new JsonObject();     linkPreview.add("title", new JsonPrimitive(caseName));     linkPreview.add("previewCard", previewCard);      JsonObject action = new JsonObject();     action.add("linkPreview", linkPreview);      JsonObject renderActions = new JsonObject();     renderActions.add("action", action);      return renderActions;   }  }