From ece111df69770694e83a6e893982d24e7a3fd6cd Mon Sep 17 00:00:00 2001 From: Pierrick Voulet <6769971+PierrickVoulet@users.noreply.github.com> Date: Thu, 25 Jan 2024 20:36:59 +0000 Subject: [PATCH] feat: Upgrade preview-link code sample to 3p-resources (#186) * Upgrade preview-link code sample for Apps Script to include create 3P resources * Fix preview link incompatability with 3p resource creation * Nit documentation edits * Upgrade preview-link code sample for Node to include create 3P resources * Fixed small typos in title texts * Upgrade and fix preview-link code sample for Python to enable create 3P resources * Implemented 3P resources creation function for Python * Upgrade preview-link code sample for Java and initatie create 3P resources * Fix preview-link code sample for Java and implement create 3P resources * Upgraded Java impl of 3p-resources to use JsonObjects * Upgrade from json encoded string to URL params in Java * Upgrade from json envoded string to URL params in Node and migrated to Node 20 * Upgrade from json envoded string to URL params in Python * Upgrade from json envoded string to URL params in Apps Script * Remove .vscode files from change * Reviewed Apps Script comments * Reviewed all sources specific to languages * Reviewed Python, NodeJS and Java comments * Remove people preview link implementation * Fix copyright year * Review fixes --------- Co-authored-by: pierrick --- .gitignore | 1 + apps-script/3p-resources/3p-resources.gs | 271 ++++++++++++++ apps-script/3p-resources/README.md | 11 + .../appsscript.json | 26 +- apps-script/preview-links/README.md | 4 - apps-script/preview-links/preview-link.gs | 85 ----- java/3p-resources/README.md | 82 +++++ .../deployment.json | 28 +- java/{preview-links => 3p-resources}/pom.xml | 11 +- .../src/main/java/Create3pResources.java | 332 ++++++++++++++++++ .../src/main/java/CreateLinkPreview.java | 116 ++++++ java/preview-links/README.md | 61 ---- .../src/main/java/PreviewLink.java | 147 -------- node/3p-resources/README.md | 82 +++++ .../deployment.json | 28 +- node/3p-resources/index.js | 328 +++++++++++++++++ .../package.json | 7 +- node/preview-links/README.md | 61 ---- node/preview-links/index.js | 118 ------- python/3p-resources/README.md | 82 +++++ .../3p-resources/create_3p_resources/main.py | 270 ++++++++++++++ .../create_3p_resources/requirements.txt | 2 + .../3p-resources/create_link_preview/main.py | 80 +++++ .../create_link_preview/requirements.txt | 2 + .../deployment.json | 28 +- python/preview-links/README.md | 61 ---- python/preview-links/main.py | 125 ------- python/preview-links/requirements.txt | 1 - 28 files changed, 1719 insertions(+), 731 deletions(-) create mode 100644 .gitignore create mode 100644 apps-script/3p-resources/3p-resources.gs create mode 100644 apps-script/3p-resources/README.md rename apps-script/{preview-links => 3p-resources}/appsscript.json (71%) delete mode 100644 apps-script/preview-links/README.md delete mode 100644 apps-script/preview-links/preview-link.gs create mode 100644 java/3p-resources/README.md rename java/{preview-links => 3p-resources}/deployment.json (67%) rename java/{preview-links => 3p-resources}/pom.xml (88%) create mode 100644 java/3p-resources/src/main/java/Create3pResources.java create mode 100644 java/3p-resources/src/main/java/CreateLinkPreview.java delete mode 100644 java/preview-links/README.md delete mode 100644 java/preview-links/src/main/java/PreviewLink.java create mode 100644 node/3p-resources/README.md rename node/{preview-links => 3p-resources}/deployment.json (67%) create mode 100644 node/3p-resources/index.js rename node/{preview-links => 3p-resources}/package.json (74%) delete mode 100644 node/preview-links/README.md delete mode 100644 node/preview-links/index.js create mode 100644 python/3p-resources/README.md create mode 100644 python/3p-resources/create_3p_resources/main.py create mode 100644 python/3p-resources/create_3p_resources/requirements.txt create mode 100644 python/3p-resources/create_link_preview/main.py create mode 100644 python/3p-resources/create_link_preview/requirements.txt rename python/{preview-links => 3p-resources}/deployment.json (67%) delete mode 100644 python/preview-links/README.md delete mode 100644 python/preview-links/main.py delete mode 100644 python/preview-links/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs new file mode 100644 index 0000000..ea2d159 --- /dev/null +++ b/apps-script/3p-resources/3p-resources.gs @@ -0,0 +1,271 @@ +/** + * 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. + */ +// [START add_ons_preview_link] +// [START add_ons_case_preview_link] + +/** +* 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; +} + +// [END add_ons_case_preview_link] +// [END add_ons_preview_link] + +// [START add_ons_3p_resources] +// [START add_ons_3p_resources_create_case_card] + +/** + * 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; + } +} + +// [END add_ons_3p_resources_create_case_card] +// [START add_ons_3p_resources_submit_create_case] + +/** + * 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("&"); +} + +// [END add_ons_3p_resources_submit_create_case] +// [START add_ons_3p_resources_validate_inputs] + +/** + * 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('Error: ' + errorMessage + ''); +} + +// [END add_ons_3p_resources_validate_inputs] +// [START add_ons_3p_resources_link_render_action] + +/** + * 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 + }] + } + } + }; +} + +// [END add_ons_3p_resources_link_render_action] +// [END add_ons_3p_resources] \ No newline at end of file diff --git a/apps-script/3p-resources/README.md b/apps-script/3p-resources/README.md new file mode 100644 index 0000000..9ddf9b8 --- /dev/null +++ b/apps-script/3p-resources/README.md @@ -0,0 +1,11 @@ +# Third-Party Resources + +## Preview Links with Smart Chips + +For more information on preview link with Smart Chips, please read the +[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links). + +## Create Third-Party Resources from the @ Menu + +For more information on creating third-party resources from the @ menu, please read the +[guide](https://developers.devsite.corp.google.com/workspace/add-ons/guides/create-insert-resource-smart-chip). diff --git a/apps-script/preview-links/appsscript.json b/apps-script/3p-resources/appsscript.json similarity index 71% rename from apps-script/preview-links/appsscript.json rename to apps-script/3p-resources/appsscript.json index 6b6cd96..c1752c0 100644 --- a/apps-script/preview-links/appsscript.json +++ b/apps-script/3p-resources/appsscript.json @@ -3,12 +3,13 @@ "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview" + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { - "name": "Preview support cases", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png", + "name": "Manage support cases", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } @@ -35,20 +36,17 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, + } + ], + "createActionTriggers": [ { - "runFunction": "peopleLinkPreview", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", + "id": "createCase", + "labelText": "Create support case", "localizedLabelText": { - "es": "Personas" + "es": "Crear caso de soporte" }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" + "runFunction": "createCaseInputCard", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" } ] } diff --git a/apps-script/preview-links/README.md b/apps-script/preview-links/README.md deleted file mode 100644 index bca7b3e..0000000 --- a/apps-script/preview-links/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Preview Links with Smart Chips - -For more information on preview link with Smart Chips, please read the -[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links). diff --git a/apps-script/preview-links/preview-link.gs b/apps-script/preview-links/preview-link.gs deleted file mode 100644 index 02eedfe..0000000 --- a/apps-script/preview-links/preview-link.gs +++ /dev/null @@ -1,85 +0,0 @@ -/** - * 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. - */ -// [START add_ons_preview_link] -// [START add_ons_case_preview_link] - -/** -* Entry point for a support case link preview -* -* @param {!Object} event -* @return {!Card} -*/ -// Creates a function that passes an event object as a parameter. -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 ID. - const segments = event.docs.matchedUrl.url.split('/'); - const caseId = segments[segments.length - 1]; - - // Builds a preview card with the case ID, title, and description - const caseHeader = CardService.newCardHeader() - .setTitle(`Case ${caseId}: Title bar is broken.`); - const caseDescription = CardService.newTextParagraph() - .setText('Customer can\'t view title on mobile device.'); - - // 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(); - } -} - -// [END add_ons_case_preview_link] -// [START add_ons_people_preview_link] - -/** -* Entry point for an employee profile link preview -* -* @param {!Object} event -* @return {!Card} -*/ -function peopleLinkPreview(event) { - - // If the event object URL matches a specified pattern for employee profile links. - if (event.docs.matchedUrl.url) { - - // Builds a preview card with an employee's name, title, email, and profile photo. - const userHeader = CardService.newCardHeader().setTitle("Rosario Cruz"); - const userImage = CardService.newImage() - .setImageUrl("https://developers.google.com/workspace/add-ons/images/employee-profile.png"); - const userInfo = CardService.newDecoratedText() - .setText("rosario@example.com") - .setBottomLabel("Case Manager") - .setIcon(CardService.Icon.EMAIL); - const userSection = CardService.newCardSection() - .addWidget(userImage) - .addWidget(userInfo); - - // Returns the card. Uses the text from the card's header for the title of the smart chip. - return CardService.newCardBuilder() - .setHeader(userHeader) - .addSection(userSection) - .build(); - } -} - -// [END add_ons_people_preview_link] -// [END add_ons_preview_link] diff --git a/java/3p-resources/README.md b/java/3p-resources/README.md new file mode 100644 index 0000000..1a2d8d3 --- /dev/null +++ b/java/3p-resources/README.md @@ -0,0 +1,82 @@ +# Third-Party Resources + +The solution is made of two Cloud Functions, one for the link preview trigger and +one for the third-party resource create action trigger. +To learn about writing Cloud Functions, +see the documentation: https://cloud.google.com/functions/docs/writing. + +For more information on preview link with Smart Chips, please read the +[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links). + +For more information on creating third-party resources from the @ menu, please read the +[guide](https://developers.devsite.corp.google.com/workspace/add-ons/guides/create-insert-resource-smart-chip). + +## Create and deploy the Cloud Functions + +### Turn on the Cloud Functions, Cloud Build, and the Add-ons API + +```sh +gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com +``` + +### Deploy the functions + +```sh +gcloud functions deploy createLinkPreview --runtime java11 --trigger-http --entry-point CreateLinkPreview +gcloud functions deploy create3pResources --runtime java11 --trigger-http --entry-point Create3pResources +``` + +### Set the URL of the create3pResources function + +```sh +gcloud functions describe create3pResources +``` + +Run the following command after having replaced `$URL` with the deployed +function URL retrieved previously to set the environment variable `URL`. + +```sh +gcloud functions deploy create3pResources --update-env-vars URL=$URL +``` + +## Create an add-on deployment + +### Find the service account email for the add-on + +```sh +gcloud workspace-add-ons get-authorization +``` + +### Grant the service account the ``cloudfunctions.invoker`` role + +```sh +gcloud functions add-iam-policy-binding createLinkPreview \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +gcloud functions add-iam-policy-binding create3pResources \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +``` + +### Set the URLs of the deployed functions + +```sh +gcloud functions describe createLinkPreview +gcloud functions describe create3pResources +``` + +Replace `$URL1` in deployment.json with the first deployed function URL +and replace `$URL2` in deployment.json with the second deployed function URL. + +### Create the deployment + +```sh +gcloud workspace-add-ons deployments create manageSupportCases \ + --deployment-file=deployment.json +``` + +## Install the add-on + +```sh +gcloud workspace-add-ons deployments install manageSupportCases +``` diff --git a/java/preview-links/deployment.json b/java/3p-resources/deployment.json similarity index 67% rename from java/preview-links/deployment.json rename to java/3p-resources/deployment.json index 57f9bf0..7a7cc83 100644 --- a/java/preview-links/deployment.json +++ b/java/3p-resources/deployment.json @@ -1,11 +1,12 @@ { "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview" + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { - "name": "Preview support cases", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png", + "name": "Manage support cases", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } @@ -13,7 +14,7 @@ "docs": { "linkPreviewTriggers": [ { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -32,20 +33,17 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, + } + ], + "createActionTriggers": [ { - "runFunction": "$URL", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", + "id": "createCase", + "labelText": "Create support case", "localizedLabelText": { - "es": "Personas" + "es": "Crear caso de soporte" }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" + "runFunction": "$URL2", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" } ] } diff --git a/java/preview-links/pom.xml b/java/3p-resources/pom.xml similarity index 88% rename from java/preview-links/pom.xml rename to java/3p-resources/pom.xml index cf1e825..251d5d3 100644 --- a/java/preview-links/pom.xml +++ b/java/3p-resources/pom.xml @@ -35,19 +35,16 @@ limitations under the License. functions-framework-api 1.0.4 - - com.google.code.gson gson 2.9.1 - - com.google.apis - google-api-services-chat - v1-rev20211125-1.32.1 - + org.apache.httpcomponents + httpclient + 4.5.1 + diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java new file mode 100644 index 0000000..2fc05b2 --- /dev/null +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -0,0 +1,332 @@ +/** + * 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. + */ +// [START add_ons_3p_resources] + +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(), false))); + } + } + + // [START add_ons_3p_resources_create_case_card] + + /** + * 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 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; + } + + // [END add_ons_3p_resources_create_case_card] + // [START add_ons_3p_resources_submit_create_case] + + /** + * 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 caseDetails = new HashMap(); + 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 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()); + } + } + + // [END add_ons_3p_resources_submit_create_case] + // [START add_ons_3p_resources_validate_inputs] + + /** + * 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 validateFormInputs(Map caseDetails) { + Map errors = new HashMap(); + 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("Error: " + errorMessage + "")); + + JsonObject textParagraphWidget = new JsonObject(); + textParagraphWidget.add("textParagraph", textParagraph); + + return textParagraphWidget; + } + + // [END add_ons_3p_resources_validate_inputs] + // [START add_ons_3p_resources_link_render_action] + + /** + * 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; + } + + // [END add_ons_3p_resources_link_render_action] +} + +// [END add_ons_3p_resources] diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java new file mode 100644 index 0000000..e3d4372 --- /dev/null +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -0,0 +1,116 @@ +/** + * 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. + */ +// [START add_ons_preview_link] + +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("{}"); + } + + // [START add_ons_case_preview_link] + + /** + * 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 caseDetails = new HashMap(); + 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; + } + + // [END add_ons_case_preview_link] +} + +// [END add_ons_preview_link] diff --git a/java/preview-links/README.md b/java/preview-links/README.md deleted file mode 100644 index 2135c4d..0000000 --- a/java/preview-links/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Preview Links with Smart Chips - -For more information on preview link with Smart Chips, please read the -[guide](https://developers.google.com/workspace/add-ons/guides/preview-links-smart-chips). - -This Cloud Function specifies link previews for two link preview triggers. -Alternatively, you can specify a Cloud Function for each trigger. -To learn about writing Cloud Functions, -see the documentation: https://cloud.google.com/functions/docs/writing. - -## Create and deploy a Cloud Function - -### Turn on the Cloud Functions, Cloud Build, and the Add-ons API - -```sh -gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com -``` - -### Deploy the function - -```sh -gcloud functions deploy createLinkPreview --entry-point PreviewLink --runtime java11 --trigger-http -``` - -## Create an add-on deployment - -### Find the service account email for the add-on - -```sh -gcloud workspace-add-ons get-authorization -``` - -### Grant the service account the ``cloudfunctions.invoker`` role - -```sh -gcloud functions add-iam-policy-binding createLinkPreview \ - --role roles/cloudfunctions.invoker \ - --member serviceAccount:SERVICE_ACCOUNT_EMAIL -``` - -### Get URL of the deployed function - -```sh -gcloud functions describe createLinkPreview -``` - -Replace `$URL` in deployment.json with the deployed function URL - -### Create the deployment - -```sh -gcloud workspace-add-ons deployments create linkpreview \ - --deployment-file=deployment.json -``` - -## Install the add-on - -```sh -gcloud workspace-add-ons deployments install linkpreview -``` - diff --git a/java/preview-links/src/main/java/PreviewLink.java b/java/preview-links/src/main/java/PreviewLink.java deleted file mode 100644 index aedc1d9..0000000 --- a/java/preview-links/src/main/java/PreviewLink.java +++ /dev/null @@ -1,147 +0,0 @@ -/** - * 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. - */ -// [START add_ons_preview_link] - -import com.google.api.services.chat.v1.model.Card; -import com.google.api.services.chat.v1.model.CardHeader; -import com.google.api.services.chat.v1.model.Image; -import com.google.api.services.chat.v1.model.KeyValue; -import com.google.api.services.chat.v1.model.Section; -import com.google.api.services.chat.v1.model.TextParagraph; -import com.google.api.services.chat.v1.model.WidgetMarkup; -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.JsonObject; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; - -public class PreviewLink implements HttpFunction { - private static final Gson gson = new Gson(); - - /** - * Responds to any HTTP request. - * - * @param request An HTTP request context. - * @param response An HTTP response context. - */ - @Override - public void service(HttpRequest request, HttpResponse response) throws Exception { - JsonObject body = gson.fromJson(request.getReader(), JsonObject.class); - String url = body.getAsJsonObject("docs") - .getAsJsonObject("matchedUrl") - .get("url") - .getAsString(); - - response.getWriter().write(gson.toJson(createCard(url))); - } - - /** - * Creates a preview link card for either a case link or people link. - * - * @param url A URL. - * @return A case link preview card or a people link preview card. - */ - Card createCard(String url) throws MalformedURLException { - URL parsedURL = new URL(url); - - if (!parsedURL.getHost().equals("www.example.com")) { - return new Card(); - } - - if (parsedURL.getPath().startsWith("/support/cases/")) { - return caseLinkPreview(url); - } - - if (parsedURL.getPath().startsWith("/people/")) { - return peopleLinkPreview(); - } - - return new Card(); - } - - // [START add_ons_case_preview_link] - - /** - * Creates a case link preview card. - * - * @param url A URL. - * @return A case link preview card. - */ - Card caseLinkPreview(String url) { - String[] segments = url.split("/"); - String caseId = segments[segments.length - 1]; - - CardHeader cardHeader = new CardHeader(); - cardHeader.setTitle(String.format("Case %s: Title bar is broken.", caseId)); - - TextParagraph textParagraph = new TextParagraph(); - textParagraph.setText("Customer can't view title on mobile device."); - - WidgetMarkup widget = new WidgetMarkup(); - widget.setTextParagraph(textParagraph); - Section section = new Section(); - section.setWidgets(List.of(widget)); - - Card card = new Card(); - card.setHeader(cardHeader); - card.setSections(List.of(section)); - - return card; - } - - // [END add_ons_case_preview_link] - // [START add_ons_people_preview_link] - - /** - * Creates a people link preview card. - * - * @return A people link preview card. - */ - Card peopleLinkPreview() { - CardHeader cardHeader = new CardHeader(); - cardHeader.setTitle("Rosario Cruz"); - - Image image = new Image(); - image.setImageUrl("https://developers.google.com/workspace/add-ons/images/employee-profile.png"); - - WidgetMarkup imageWidget = new WidgetMarkup(); - imageWidget.setImage(image); - - KeyValue keyValue = new KeyValue(); - keyValue.setIcon("EMAIL"); - keyValue.setContent("rosario@example.com"); - keyValue.setBottomLabel("Case Manager"); - - WidgetMarkup keyValueWidget = new WidgetMarkup(); - keyValueWidget.setKeyValue(keyValue); - - Section section = new Section(); - section.setWidgets(List.of(imageWidget, keyValueWidget)); - - Card card = new Card(); - card.setHeader(cardHeader); - card.setSections(List.of(section)); - - return card; - } - - // [END add_ons_people_preview_link] -} - -// [END add_ons_preview_link] diff --git a/node/3p-resources/README.md b/node/3p-resources/README.md new file mode 100644 index 0000000..13870c9 --- /dev/null +++ b/node/3p-resources/README.md @@ -0,0 +1,82 @@ +# Third-Party Resources + +The solution is made of two Cloud Functions, one for the link preview trigger and +one for the third-party resource create action trigger. +To learn about writing Cloud Functions, +see the documentation: https://cloud.google.com/functions/docs/writing. + +For more information on preview link with Smart Chips, please read the +[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links). + +For more information on creating third-party resources from the @ menu, please read the +[guide](https://developers.devsite.corp.google.com/workspace/add-ons/guides/create-insert-resource-smart-chip). + +## Create and deploy the Cloud Functions + +### Turn on the Cloud Functions, Cloud Build, and the Add-ons API + +```sh +gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com +``` + +### Deploy the functions + +```sh +gcloud functions deploy createLinkPreview --runtime nodejs20 --trigger-http +gcloud functions deploy create3pResources --runtime nodejs20 --trigger-http +``` + +### Set the URL of the create3pResources function + +```sh +gcloud functions describe create3pResources +``` + +Run the following command after having replaced `$URL` with the deployed +function URL retrieved previously to set the environment variable `URL`. + +```sh +gcloud functions deploy create3pResources --update-env-vars URL=$URL +``` + +## Create an add-on deployment + +### Find the service account email for the add-on + +```sh +gcloud workspace-add-ons get-authorization +``` + +### Grant the service account the ``cloudfunctions.invoker`` role + +```sh +gcloud functions add-iam-policy-binding createLinkPreview \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +gcloud functions add-iam-policy-binding create3pResources \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +``` + +### Set the URLs of the deployed functions + +```sh +gcloud functions describe createLinkPreview +gcloud functions describe create3pResources +``` + +Replace `$URL1` in deployment.json with the first deployed function URL +and replace `$URL2` in deployment.json with the second deployed function URL. + +### Create the deployment + +```sh +gcloud workspace-add-ons deployments create manageSupportCases \ + --deployment-file=deployment.json +``` + +## Install the add-on + +```sh +gcloud workspace-add-ons deployments install manageSupportCases +``` diff --git a/node/preview-links/deployment.json b/node/3p-resources/deployment.json similarity index 67% rename from node/preview-links/deployment.json rename to node/3p-resources/deployment.json index 57f9bf0..7a7cc83 100644 --- a/node/preview-links/deployment.json +++ b/node/3p-resources/deployment.json @@ -1,11 +1,12 @@ { "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview" + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { - "name": "Preview support cases", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png", + "name": "Manage support cases", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } @@ -13,7 +14,7 @@ "docs": { "linkPreviewTriggers": [ { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -32,20 +33,17 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, + } + ], + "createActionTriggers": [ { - "runFunction": "$URL", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", + "id": "createCase", + "labelText": "Create support case", "localizedLabelText": { - "es": "Personas" + "es": "Crear caso de soporte" }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" + "runFunction": "$URL2", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" } ] } diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js new file mode 100644 index 0000000..000c2ba --- /dev/null +++ b/node/3p-resources/index.js @@ -0,0 +1,328 @@ +/** + * 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. + */ +// [START add_ons_preview_link] + +/** + * 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)); + } + } + } +}; + +// [START add_ons_case_preview_link] + +/** + * + * 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") + } + }] + }] + } + } + } + }; +} + +// [END add_ons_case_preview_link] +// [END add_ons_preview_link] + +// [START add_ons_3p_resources] + +/** + * 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)); + } +}; + +// [START add_ons_3p_resources_create_case_card] + +/** + * 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 + }] + } + }; + } +} + +// [END add_ons_3p_resources_create_case_card] +// [START add_ons_3p_resources_submit_create_case] + +/** + * 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); + } +} + +// [END add_ons_3p_resources_submit_create_case] +// [START add_ons_3p_resources_validate_inputs] + +/** + * 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: 'Error: ' + errorMessage + '' + } + } +} + +// [END add_ons_3p_resources_validate_inputs] +// [START add_ons_3p_resources_link_render_action] + +/** + * 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 + }] + } + } + }; +} + +// [END add_ons_3p_resources_link_render_action] +// [END add_ons_3p_resources] diff --git a/node/preview-links/package.json b/node/3p-resources/package.json similarity index 74% rename from node/preview-links/package.json rename to node/3p-resources/package.json index ff4e5ff..25875f4 100644 --- a/node/preview-links/package.json +++ b/node/3p-resources/package.json @@ -1,13 +1,16 @@ { - "name": "preview-link", + "name": "3p-resources", "version": "1.0.0", - "description": "Preview support cases", + "description": "Manage support cases", "main": "index.js", "repository": { "type": "git", "url": "git+https://github.com/googleworkspace/add-ons-samples.git" }, "author": "https://github.com/vinay-google", + "contributors": [ + "https://github.com/PierrickVoulet" + ], "license": "ISC", "bugs": { "url": "https://github.com/googleworkspace/add-ons-samples/issues" diff --git a/node/preview-links/README.md b/node/preview-links/README.md deleted file mode 100644 index 56506de..0000000 --- a/node/preview-links/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Preview Links with Smart Chips - -For more information on preview link with Smart Chips, please read the -[guide](https://developers.google.com/workspace/add-ons/guides/preview-links-smart-chips). - -This Cloud Function specifies link previews for two link preview triggers. -Alternatively, you can specify a Cloud Function for each trigger. -To learn about writing Cloud Functions, -see the documentation: https://cloud.google.com/functions/docs/writing. - -## Create and deploy a Cloud Function - -### Turn on the Cloud Functions, Cloud Build, and the Add-ons API - -```sh -gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com -``` - -### Deploy the function - -```sh -gcloud functions deploy createLinkPreview --runtime nodejs16 --trigger-http -``` - -## Create an add-on deployment - -### Find the service account email for the add-on - -```sh -gcloud workspace-add-ons get-authorization -``` - -### Grant the service account the ``cloudfunctions.invoker`` role - -```sh -gcloud functions add-iam-policy-binding createLinkPreview \ - --role roles/cloudfunctions.invoker \ - --member serviceAccount:SERVICE_ACCOUNT_EMAIL -``` - -### Get URL of the deployed function - -```sh -gcloud functions describe createLinkPreview -``` - -Replace `$URL` in deployment.json with the deployed function URL - -### Create the deployment - -```sh -gcloud workspace-add-ons deployments create linkpreview \ - --deployment-file=deployment.json -``` - -## Install the add-on - -```sh -gcloud workspace-add-ons deployments install linkpreview -``` - diff --git a/node/preview-links/index.js b/node/preview-links/index.js deleted file mode 100644 index 0c8aca8..0000000 --- a/node/preview-links/index.js +++ /dev/null @@ -1,118 +0,0 @@ -/** - * 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. - */ -// [START add_ons_preview_link] - -const UrlParser = require('url'); - -/** - * Responds to any HTTP request. - * - * @param {Object} req HTTP request context. - * @param {Object} res HTTP response context. - */ -exports.createLinkPreview = (req, res) => { - const event = req.body; - if (event.docs.matchedUrl.url) { - res.json(createCard(event.docs.matchedUrl.url)); - } -}; - -/** - * Creates a preview link card for either a case link or people link. - * - * @param {!String} url - * @return {!Card} - */ -function createCard(url) { - const parsedUrl = UrlParser.parse(url); - if (parsedUrl.hostname === 'www.example.com') { - if (parsedUrl.path.startsWith('/support/cases/')) { - return caseLinkPreview(url); - } - - if (parsedUrl.path.startsWith('/people/')) { - return peopleLinkPreview(); - } - } -} - -// [START add_ons_case_preview_link] - -/** - * - * A support case link preview. - * - * @param {!string} url - * @return {!Card} - */ -function caseLinkPreview(url) { - - // Parses the URL to identify the case ID. - const segments = url.split('/'); - const caseId = segments[segments.length - 1]; - - // Returns the card. - // Uses the text from the card's header for the title of the smart chip. - return { - header: { - title: `Case ${caseId}: Title bar is broken.` - }, - sections: [{ - widgets: [{ - textParagraph: { - text: `Customer can't view title on mobile device.` - } - }] - }] - }; -} - -// [END add_ons_case_preview_link] -// [START add_ons_people_preview_link] - -/** - * An employee profile link preview. - * - * @return {!Card} - */ -function peopleLinkPreview() { - - // Builds a preview card with an employee's name, title, email, and profile photo. - // Returns the card. Uses the text from the card's header for the title of the smart chip. - return { - header: { - title: "Rosario Cruz" - }, - sections: [{ - widgets: [ - { - image: { - imageUrl: 'https://developers.google.com/workspace/add-ons/images/employee-profile.png' - } - }, { - keyValue: { - icon: "EMAIL", - content: "rosario@example.com", - bottomLabel: "Case Manager" - } - } - ] - }] - }; -} - -// [END add_ons_people_preview_link] -// [END add_ons_preview_link] diff --git a/python/3p-resources/README.md b/python/3p-resources/README.md new file mode 100644 index 0000000..734342f --- /dev/null +++ b/python/3p-resources/README.md @@ -0,0 +1,82 @@ +# Third-Party Resources + +The solution is made of two Cloud Functions, one for the link preview trigger and +one for the third-party resource create action trigger. +To learn about writing Cloud Functions, +see the documentation: https://cloud.google.com/functions/docs/writing. + +For more information on preview link with Smart Chips, please read the +[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links). + +For more information on creating third-party resources from the @ menu, please read the +[guide](https://developers.devsite.corp.google.com/workspace/add-ons/guides/create-insert-resource-smart-chip). + +## Create and deploy the Cloud Functions + +### Turn on the Cloud Functions, Cloud Build, and the Add-ons API + +```sh +gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com +``` + +### Deploy the functions + +```sh +gcloud functions deploy create_link_preview --runtime python312 --trigger-http --source ./create_link_preview +gcloud functions deploy create_3p_resources --runtime python312 --trigger-http --source ./create_3p_resources +``` + +### Set the URL of the create3pResources function + +```sh +gcloud functions describe create_3p_resources +``` + +Run the following command after having replaced `$URL` with the deployed +function URL retrieved previously to set the environment variable `URL`. + +```sh +gcloud functions deploy create_3p_resources --update-env-vars URL=$URL +``` + +## Create an add-on deployment + +### Find the service account email for the add-on + +```sh +gcloud workspace-add-ons get-authorization +``` + +### Grant the service account the ``cloudfunctions.invoker`` role + +```sh +gcloud functions add-iam-policy-binding create_link_preview \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +gcloud functions add-iam-policy-binding create_3p_resources \ + --role roles/cloudfunctions.invoker \ + --member serviceAccount:SERVICE_ACCOUNT_EMAIL +``` + +### Set the URLs of the deployed functions + +```sh +gcloud functions describe create_link_preview +gcloud functions describe create_3p_resources +``` + +Replace `$URL1` in deployment.json with the first deployed function URL +and replace `$URL2` in deployment.json with the second deployed function URL. + +### Create the deployment + +```sh +gcloud workspace-add-ons deployments create manageSupportCases \ + --deployment-file=deployment.json +``` + +## Install the add-on + +```sh +gcloud workspace-add-ons deployments install manageSupportCases +``` diff --git a/python/3p-resources/create_3p_resources/main.py b/python/3p-resources/create_3p_resources/main.py new file mode 100644 index 0000000..e89ea9f --- /dev/null +++ b/python/3p-resources/create_3p_resources/main.py @@ -0,0 +1,270 @@ +# 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. +# [START add_ons_3p_resources] + +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) + + +# [START add_ons_3p_resources_create_case_card] + + +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 + }] + } + } + + +# [END add_ons_3p_resources_create_case_card] +# [START add_ons_3p_resources_submit_create_case] + + +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) + + +# [END add_ons_3p_resources_submit_create_case] +# [START add_ons_3p_resources_validate_inputs] + + +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": 'Error: ' + error_message + '' + } + } + + +# [END add_ons_3p_resources_validate_inputs] +# [START add_ons_3p_resources_link_render_action] + + +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 + }] + } + } + } + +# [END add_ons_3p_resources_link_render_action] +# [END add_ons_3p_resources] diff --git a/python/3p-resources/create_3p_resources/requirements.txt b/python/3p-resources/create_3p_resources/requirements.txt new file mode 100644 index 0000000..1cdcddf --- /dev/null +++ b/python/3p-resources/create_3p_resources/requirements.txt @@ -0,0 +1,2 @@ +Flask>=2.2.2 +functions-framework>=3.5.0 \ No newline at end of file diff --git a/python/3p-resources/create_link_preview/main.py b/python/3p-resources/create_link_preview/main.py new file mode 100644 index 0000000..2b970de --- /dev/null +++ b/python/3p-resources/create_link_preview/main.py @@ -0,0 +1,80 @@ +# 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. +# [START add_ons_preview_link] + +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 {} + + +# [START add_ons_case_preview_link] + + +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] + } + }] + }], + } + } + } + } + + +# [END add_ons_case_preview_link] +# [END add_ons_preview_link] diff --git a/python/3p-resources/create_link_preview/requirements.txt b/python/3p-resources/create_link_preview/requirements.txt new file mode 100644 index 0000000..1cdcddf --- /dev/null +++ b/python/3p-resources/create_link_preview/requirements.txt @@ -0,0 +1,2 @@ +Flask>=2.2.2 +functions-framework>=3.5.0 \ No newline at end of file diff --git a/python/preview-links/deployment.json b/python/3p-resources/deployment.json similarity index 67% rename from python/preview-links/deployment.json rename to python/3p-resources/deployment.json index 57f9bf0..7a7cc83 100644 --- a/python/preview-links/deployment.json +++ b/python/3p-resources/deployment.json @@ -1,11 +1,12 @@ { "oauthScopes": [ - "https://www.googleapis.com/auth/workspace.linkpreview" + "https://www.googleapis.com/auth/workspace.linkpreview", + "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { - "name": "Preview support cases", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png", + "name": "Manage support cases", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } @@ -13,7 +14,7 @@ "docs": { "linkPreviewTriggers": [ { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -32,20 +33,17 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, + } + ], + "createActionTriggers": [ { - "runFunction": "$URL", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", + "id": "createCase", + "labelText": "Create support case", "localizedLabelText": { - "es": "Personas" + "es": "Crear caso de soporte" }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" + "runFunction": "$URL2", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" } ] } diff --git a/python/preview-links/README.md b/python/preview-links/README.md deleted file mode 100644 index 93403e0..0000000 --- a/python/preview-links/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Preview Links with Smart Chips - -For more information on preview link with Smart Chips, please read the -[guide](https://developers.google.com/workspace/add-ons/guides/preview-links-smart-chips). - -This Cloud Function specifies link previews for two link preview triggers. -Alternatively, you can specify a Cloud Function for each trigger. -To learn about writing Cloud Functions, -see the documentation: https://cloud.google.com/functions/docs/writing. - -## Create and deploy a Cloud Function - -### Turn on the Cloud Functions, Cloud Build, and the Add-ons API - -```sh -gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.googleapis.com -``` - -### Deploy the function - -```sh -gcloud functions deploy create_link_preview --runtime python311 --trigger-http -``` - -## Create an add-on deployment - -### Find the service account email for the add-on - -```sh -gcloud workspace-add-ons get-authorization -``` - -### Grant the service account the ``cloudfunctions.invoker`` role - -```sh -gcloud functions add-iam-policy-binding create_link_preview \ - --role roles/cloudfunctions.invoker \ - --member serviceAccount:SERVICE_ACCOUNT_EMAIL -``` - -### Get URL of the deployed function - -```sh -gcloud functions describe create_link_preview -``` - -Replace `$URL` in deployment.json with the deployed function URL - -### Create the deployment - -```sh -gcloud workspace-add-ons deployments create linkpreview \ - --deployment-file=deployment.json -``` - -## Install the add-on - -```sh -gcloud workspace-add-ons deployments install linkpreview -``` - diff --git a/python/preview-links/main.py b/python/preview-links/main.py deleted file mode 100644 index c0ed303..0000000 --- a/python/preview-links/main.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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. -# [START add_ons_preview_link] - -from typing import Any, Mapping -from urllib.parse import urlparse - -import flask -import functions_framework - - -@functions_framework.http -def create_link_preview(req: flask.Request): - """Responds to any HTTP request. - Args: - req: HTTP request context. - Returns: - The response object. - """ - event = req.get_json(silent=True) - if event["docs"]["matchedUrl"]["url"]: - return create_card(event["docs"]["matchedUrl"]["url"]) - - -def create_card(url): - """Creates a preview link card for either a case link or people link. - Args: - url: The matched url. - Returns: - A case link preview card or a people link preview card. - """ - parsed_url = urlparse(url) - if parsed_url.hostname != "www.example.com": - return {} - - if parsed_url.path.startswith("/support/cases/"): - return case_link_preview(url) - - if parsed_url.path.startswith("/people/"): - return people_link_preview() - - return {} - - -# [START add_ons_case_preview_link] - - -def case_link_preview(url): - """A support case link preview. - Args: - url: The case link. - Returns: - A case link preview card. - """ - - # Parses the URL to identify the case ID. - segments = url.split("/") - case_id = segments[-1] - - # Returns the card. - # Uses the text from the card's header for the title of the smart chip. - return { - "header": {"title": f"Case {case_id}: Title bar is broken."}, - "sections": [ - { - "widgets": [ - { - "textParagraph": { - "text": "Customer can't view title on mobile device." - } - } - ] - } - ], - } - - -# [END add_ons_case_preview_link] -# [START add_ons_people_preview_link] - - -def people_link_preview(): - """An employee profile link preview. - Returns: - A people link preview card. - """ - - # Builds a preview card with an employee's name, title, email, and profile photo. - # Returns the card. Uses the text from the card's header for the title of the smart chip. - return { - "header": {"title": "Rosario Cruz"}, - "sections": [ - { - "widgets": [ - { - "image": { - "imageUrl": "https:#developers.google.com/workspace/add-ons/images/employee-profile.png" - } - }, - { - "keyValue": { - "icon": "EMAIL", - "content": "rosario@example.com", - "bottomLabel": "Case Manager", - } - }, - ] - } - ], - } - - -# [END add_ons_people_preview_link] -# [END add_ons_preview_link] diff --git a/python/preview-links/requirements.txt b/python/preview-links/requirements.txt deleted file mode 100644 index 0864e3d..0000000 --- a/python/preview-links/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Flask>=2.2.2