From 25a6eeaebad95c703e5ea22090d772ed0cd8bda7 Mon Sep 17 00:00:00 2001 From: pierrick Date: Wed, 17 Jan 2024 19:59:38 +0000 Subject: [PATCH 01/21] Upgrade preview-link code sample for Apps Script to include create 3P resources --- apps-script/3p-resources/3p-resources.gs | 264 ++++++++++++++++++ apps-script/3p-resources/README.md | 11 + .../appsscript.json | 27 +- apps-script/preview-links/README.md | 4 - apps-script/preview-links/preview-link.gs | 85 ------ 5 files changed, 299 insertions(+), 92 deletions(-) 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 (61%) delete mode 100644 apps-script/preview-links/README.md delete mode 100644 apps-script/preview-links/preview-link.gs diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs new file mode 100644 index 0000000..ff18fb2 --- /dev/null +++ b/apps-script/3p-resources/3p-resources.gs @@ -0,0 +1,264 @@ +/** + * 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 +* @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] + +// [START add_ons_3p_resources] +// [START add_ons_3p_resources_create_case_card] + +/** + * Produces a support case creation form. + * + * @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 updateCard navigation. + * @return {!Card|!ActionResponse} + */ +function createCaseInputCard(event, errors, isUpdate) { + + const cardHeader1 = CardService.newCardHeader() + .setTitle('Create a support case') + + const cardSection1TextInput1 = CardService.newTextInput() + .setFieldName('name') + .setTitle('Name') + .setMultiline(false); + + const cardSection1TextInput2 = CardService.newTextInput() + .setFieldName('description') + .setTitle('Description') + .setMultiline(true); + + const cardSection1SelectionInput1 = 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 cardSection1SelectionInput2 = CardService.newSelectionInput() + .setFieldName('impact') + .setTitle('Impact') + .setType(CardService.SelectionInputType.CHECK_BOX) + .addItem('Blocks a critical customer operation', 'Blocks a critical customer operation', false); + + const cardSection1ButtonList1Button1Action1 = CardService.newAction() + .setPersistValues(true) + .setFunctionName('submitCaseCreationForm') + .setParameters({}); + + const cardSection1ButtonList1Button1 = CardService.newTextButton() + .setText('Create') + .setTextButtonStyle(CardService.TextButtonStyle.TEXT) + .setOnClickAction(cardSection1ButtonList1Button1Action1); + + const cardSection1ButtonList1 = CardService.newButtonSet() + .addButton(cardSection1ButtonList1Button1); + + // Builds the creation form and adds error text for invalid inputs. + const cardSection1 = CardService.newCardSection(); + if (errors?.name) { + cardSection1.addWidget(createErrorTextParagraph(errors.name)); + } + cardSection1.addWidget(cardSection1TextInput1); + if (errors?.description) { + cardSection1.addWidget(createErrorTextParagraph(errors.description)); + } + cardSection1.addWidget(cardSection1TextInput2); + if (errors?.priority) { + cardSection1.addWidget(createErrorTextParagraph(errors.priority)); + } + cardSection1.addWidget(cardSection1SelectionInput1); + if (errors?.impact) { + cardSection1.addWidget(createErrorTextParagraph(errors.impact)); + } + + cardSection1.addWidget(cardSection1SelectionInput2); + cardSection1.addWidget(cardSection1ButtonList1); + + const card = CardService.newCardBuilder() + .setHeader(cardHeader1) + .addSection(cardSection1) + .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] + +/** + * Called when the creation form is submitted. If form input is valid, returns a render action + * that inserts a new link into the document. If invalid, returns an updateCard navigation that + * re-renders the creation form with error messages. + * + * @param {!Object} event The event object containing form inputs. + * @return {!Card|!RenderAction} + */ +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 = caseDetails.name; + const url = 'https://example.com/support/cases/' + encodeURIComponent(JSON.stringify(caseDetails)); + return createLinkRenderAction(title, url); + } +} + +// [END add_ons_3p_resources_submit_create_case] +// [START add_ons_3p_resources_validate_inputs] + +/** + * Validates form inputs for case creation. + * + * @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 && caseDetails.priority === 'P2' || caseDetails.impact && caseDetails.priority === 'P3') { + errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; + } + + return errors; +} + +/** + * Returns a TextParagraph with red text indicating a form field validation error. + * + * @param {string} errorMessage A description of the invalid input. + * @return {!TextParagraph} + */ +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 RenderAction 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 {!RenderAction} + */ +function createLinkRenderAction(title, url) { + return { + renderActions: { + action: { + links: [{ title, url }] + } + } + }; +} + +// [END add_ons_3p_resources_link_render_action] +// [END add_ons_3p_resources] diff --git a/apps-script/3p-resources/README.md b/apps-script/3p-resources/README.md new file mode 100644 index 0000000..a70b59b --- /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 61% rename from apps-script/preview-links/appsscript.json rename to apps-script/3p-resources/appsscript.json index 6b6cd96..172dd5e 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" } @@ -50,6 +51,26 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-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" + }, + { + "id": "createHotlist", + "labelText": "Create a hotlist", + "localizedLabelText": { + "es": "Crear una hotlist" + }, + "runFunction": "createHotlistInputCard", + "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-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] From e7fa7039fb3dedc5f668168cdcd79c4de7193a08 Mon Sep 17 00:00:00 2001 From: pierrick Date: Wed, 17 Jan 2024 20:29:22 +0000 Subject: [PATCH 02/21] Fix preview link incompatability with 3p resource creation --- apps-script/3p-resources/3p-resources.gs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index ff18fb2..87831b3 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -30,13 +30,13 @@ function caseLinkPreview(event) { // 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]; + const caseDetails = JSON.parse(decodeURIComponent(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.`); + .setTitle(`Case: ${caseDetails.name}`); const caseDescription = CardService.newTextParagraph() - .setText('Customer can\'t view title on mobile device.'); + .setText(caseDetails.description); // Returns the card. // Uses the text from the card's header for the title of the smart chip. @@ -261,4 +261,4 @@ function createLinkRenderAction(title, url) { } // [END add_ons_3p_resources_link_render_action] -// [END add_ons_3p_resources] +// [END add_ons_3p_resources] \ No newline at end of file From 4b23cebdeccc7a323d624a251c12025e1de9f772 Mon Sep 17 00:00:00 2001 From: pierrick Date: Thu, 18 Jan 2024 01:57:25 +0000 Subject: [PATCH 03/21] Nit documentation edits --- apps-script/3p-resources/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps-script/3p-resources/README.md b/apps-script/3p-resources/README.md index a70b59b..9ddf9b8 100644 --- a/apps-script/3p-resources/README.md +++ b/apps-script/3p-resources/README.md @@ -5,7 +5,7 @@ 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 +## 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). From c11b00ea63d6039d9079bf8f29dd320ca00d24fb Mon Sep 17 00:00:00 2001 From: pierrick Date: Thu, 18 Jan 2024 15:17:30 +0000 Subject: [PATCH 04/21] Upgrade preview-link code sample for Node to include create 3P resources --- apps-script/3p-resources/3p-resources.gs | 9 +- apps-script/3p-resources/appsscript.json | 9 - node/3p-resources/README.md | 82 ++++ .../deployment.json | 22 +- node/3p-resources/index.js | 369 ++++++++++++++++++ .../package.json | 7 +- node/preview-links/README.md | 61 --- node/preview-links/index.js | 118 ------ 8 files changed, 479 insertions(+), 198 deletions(-) create mode 100644 node/3p-resources/README.md rename node/{preview-links => 3p-resources}/deployment.json (66%) 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 diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 87831b3..504aef6 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -28,11 +28,11 @@ 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. + // Uses the event object to parse the URL and identify the case details. const segments = event.docs.matchedUrl.url.split('/'); const caseDetails = JSON.parse(decodeURIComponent(segments[segments.length - 1])); - // Builds a preview card with the case ID, title, and description + // Builds a preview card with the case name, and description const caseHeader = CardService.newCardHeader() .setTitle(`Case: ${caseDetails.name}`); const caseDescription = CardService.newTextParagraph() @@ -254,7 +254,10 @@ function createLinkRenderAction(title, url) { return { renderActions: { action: { - links: [{ title, url }] + links: [{ + title: title, + url: url + }] } } }; diff --git a/apps-script/3p-resources/appsscript.json b/apps-script/3p-resources/appsscript.json index 172dd5e..6e1f897 100644 --- a/apps-script/3p-resources/appsscript.json +++ b/apps-script/3p-resources/appsscript.json @@ -61,15 +61,6 @@ }, "runFunction": "createCaseInputCard", "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, - { - "id": "createHotlist", - "labelText": "Create a hotlist", - "localizedLabelText": { - "es": "Crear una hotlist" - }, - "runFunction": "createHotlistInputCard", - "logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png" } ] } diff --git a/node/3p-resources/README.md b/node/3p-resources/README.md new file mode 100644 index 0000000..a074a7f --- /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 two link preview triggers 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 nodejs16 --trigger-http +gcloud functions deploy create3pResources --runtime nodejs16 --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 66% rename from node/preview-links/deployment.json rename to node/3p-resources/deployment.json index 57f9bf0..a4757a4 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", @@ -34,7 +35,7 @@ "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" }, { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -47,6 +48,17 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-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" + } ] } } diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js new file mode 100644 index 0000000..e5e4bd6 --- /dev/null +++ b/node/3p-resources/index.js @@ -0,0 +1,369 @@ +/** + * 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] + +/** + * Responds to any HTTP request related to link previews for either a + * case link or people link. + * + * @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) { + const url = event.docs.matchedUrl.url; + const parsedUrl = new URL(url); + if (parsedUrl.hostname === 'example.com') { + if (parsedUrl.pathname.startsWith('/support/cases/')) { + return res.json(caseLinkPreview(url)); + } + + if (parsedUrl.pathname.startsWith('/people/')) { + return res.json(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 caseDetails = JSON.parse(decodeURIComponent(segments[segments.length - 1])); + + // Returns the card. + // Uses the text from the card's header for the title of the smart chip. + return { + action: { + linkPreview: { + title: `Case ${caseDetails.name}`, + previewCard: { + header: { + title: `Case ${caseDetails.name}` + }, + sections: [{ + widgets: [{ + textParagraph: { + text: caseDetails.description + } + }] + }] + } + } + } + }; +} + +// [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 { + action: { + linkPreview: { + title: "Rosario Cruz", + previewCard: { + header: { + title: "Rosario Cruz" + }, + sections: [{ + widgets: [ + { + image: { + imageUrl: 'https://developers.google.com/workspace/add-ons/images/employee-profile.png' + } + }, { + decoratedText: { + startIcon: { + knownIcon: "EMAIL" + }, + text: "rosario@example.com", + bottomLabel: "Case Manager" + } + } + ] + }] + } + } + } + }; +} + +// [START add_ons_3p_resources] +// [START add_ons_3p_resources_create_case_card] + +/** + * Responds to any HTTP request related to 3P resource creations. + * + * @param {Object} req HTTP request context. + * @param {Object} res 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. + * + * @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 updateCard navigation. + * @return {!Card|!ActionResponse} + */ +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] + +/** + * Called when the creation form is submitted. If form input is valid, returns a render action + * that inserts a new link into the document. If invalid, returns an updateCard navigation that + * re-renders the creation form with error messages. + * + * @param {!Object} event The event object containing form inputs. + * @return {!Card|!RenderAction} + */ +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}`; + const url = 'https://example.com/support/cases/' + encodeURIComponent(JSON.stringify(caseDetails)); + return createLinkRenderAction(title, url); + } +} + +// [END add_ons_3p_resources_submit_create_case] +// [START add_ons_3p_resources_validate_inputs] + +/** + * Validates form inputs for case creation. + * + * @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 && caseDetails.priority === 'P2' || caseDetails.impact && caseDetails.priority === 'P3') { + errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; + } + + return errors; +} + +/** + * Returns a TextParagraph with red text indicating a form field validation error. + * + * @param {string} errorMessage A description of the invalid input. + * @return {!TextParagraph} + */ +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 RenderAction 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 {!RenderAction} + */ +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] From aad3428d2115cd2c8ad3a0e3adadd0bb334c9da5 Mon Sep 17 00:00:00 2001 From: pierrick Date: Thu, 18 Jan 2024 21:20:13 +0000 Subject: [PATCH 05/21] Fixed small typos in title texts --- apps-script/3p-resources/3p-resources.gs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 504aef6..3dde67f 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -34,7 +34,7 @@ function caseLinkPreview(event) { // Builds a preview card with the case name, and description const caseHeader = CardService.newCardHeader() - .setTitle(`Case: ${caseDetails.name}`); + .setTitle(`Case ${caseDetails.name}`); const caseDescription = CardService.newTextParagraph() .setText(caseDetails.description); @@ -196,7 +196,7 @@ function submitCaseCreationForm(event) { if (Object.keys(errors).length > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { - const title = caseDetails.name; + const title = `Case ${caseDetails.name}`; const url = 'https://example.com/support/cases/' + encodeURIComponent(JSON.stringify(caseDetails)); return createLinkRenderAction(title, url); } From 25d386a66193d2b066bb2a8d1f3aa188d4760cf6 Mon Sep 17 00:00:00 2001 From: pierrick Date: Fri, 19 Jan 2024 14:51:45 +0000 Subject: [PATCH 06/21] Upgrade and fix preview-link code sample for Python to enable create 3P resources --- node/3p-resources/index.js | 10 +- python/3p-resources/README.md | 82 +++++++++++ .../3p-resources/create_3p_resources/main.py | 38 +++++ .../create_3p_resources/requirements.txt | 2 + .../3p-resources/create_link_preview/main.py | 130 ++++++++++++++++++ .../create_link_preview/requirements.txt | 2 + .../deployment.json | 22 ++- python/preview-links/README.md | 61 -------- python/preview-links/main.py | 125 ----------------- python/preview-links/requirements.txt | 1 - 10 files changed, 278 insertions(+), 195 deletions(-) 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 (66%) 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/node/3p-resources/index.js b/node/3p-resources/index.js index e5e4bd6..4f8e564 100644 --- a/node/3p-resources/index.js +++ b/node/3p-resources/index.js @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * 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. @@ -50,7 +50,7 @@ exports.createLinkPreview = (req, res) => { */ function caseLinkPreview(url) { - // Parses the URL to identify the case ID. + // Parses the URL to identify the case details. const segments = url.split('/'); const caseDetails = JSON.parse(decodeURIComponent(segments[segments.length - 1])); @@ -120,8 +120,10 @@ function peopleLinkPreview() { }; } +// [END add_ons_people_preview_link] +// [END add_ons_preview_link] + // [START add_ons_3p_resources] -// [START add_ons_3p_resources_create_case_card] /** * Responds to any HTTP request related to 3P resource creations. @@ -138,6 +140,8 @@ exports.create3pResources = (req, res) => { } }; +// [START add_ons_3p_resources_create_case_card] + /** * Produces a support case creation form. * diff --git a/python/3p-resources/README.md b/python/3p-resources/README.md new file mode 100644 index 0000000..052a0b4 --- /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 two link preview triggers 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..6540412 --- /dev/null +++ b/python/3p-resources/create_3p_resources/main.py @@ -0,0 +1,38 @@ +# 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 urlparse + +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: 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"]) + + + + +# [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..8dee240 --- /dev/null +++ b/python/3p-resources/create_link_preview/main.py @@ -0,0 +1,130 @@ +# 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, unquote + +import flask +import functions_framework +import json + + +@functions_framework.http +def create_link_preview(req: flask.Request): + """Responds to any HTTP request related to link previews. + Args: + req: HTTP request context. + Returns: + The response object. + """ + event = req.get_json(silent=True) + if event["docs"]["matchedUrl"]["url"]: + url = event["docs"]["matchedUrl"]["url"] + parsed_url = urlparse(url) + if parsed_url.hostname == "example.com": + 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 details. + segments = url.split("/") + case_details = json.loads(unquote(segments[len(segments) - 1])); + print(case_details) + + # Returns the card. + # Uses the text from the card's header for the title of the smart chip. + return { + "action": { + "linkPreview": { + "title": f'Case {case_details["name"]}', + "previewCard": { + "header": { + "title": f'Case {case_details["name"]}' + }, + "sections": [{ + "widgets": [{ + "textParagraph": { + "text": case_details["description"] + } + }] + }], + } + } + } + } + + +# [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 { + "action": { + "linkPreview": { + "title": "Rosario Cruz", + "previewCard": { + "header": { + "title": "Rosario Cruz" + }, + "sections": [{ + "widgets": [ + { + "image": { + "imageUrl": "https://developers.google.com/workspace/add-ons/images/employee-profile.png" + } + }, + { + "decoratedText": { + "startIcon": { + "knownIcon": "EMAIL" + }, + "text": "rosario@example.com", + "bottomLabel": "Case Manager", + } + }, + ] + }], + } + } + } + } + + +# [END add_ons_people_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 66% rename from python/preview-links/deployment.json rename to python/3p-resources/deployment.json index 57f9bf0..3165cf1 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", @@ -34,7 +35,7 @@ "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" }, { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -47,6 +48,17 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-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" + } ] } } 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 From a88606da56844ee51ffdeac7bc72c07e7e40770f Mon Sep 17 00:00:00 2001 From: pierrick Date: Fri, 19 Jan 2024 17:22:21 +0000 Subject: [PATCH 07/21] Implemented 3P resources creation function for Python --- apps-script/3p-resources/3p-resources.gs | 2 +- node/3p-resources/index.js | 2 +- .../3p-resources/create_3p_resources/main.py | 231 +++++++++++++++++- .../3p-resources/create_link_preview/main.py | 3 +- 4 files changed, 232 insertions(+), 6 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 3dde67f..4155da5 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -223,7 +223,7 @@ function validateFormInputs(caseDetails) { if (caseDetails.priority === undefined) { errors.priority = 'You must provide a priority'; } - if (caseDetails.impact && caseDetails.priority === 'P2' || caseDetails.impact && caseDetails.priority === 'P3') { + if (caseDetails.impact && !(['P0', 'P1']).includes(caseDetails.priority)) { errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; } diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js index 4f8e564..2d5f49a 100644 --- a/node/3p-resources/index.js +++ b/node/3p-resources/index.js @@ -326,7 +326,7 @@ function validateFormInputs(caseDetails) { if (caseDetails.priority === undefined) { errors.priority = 'You must provide a priority'; } - if (caseDetails.impact && caseDetails.priority === 'P2' || caseDetails.impact && caseDetails.priority === 'P3') { + if (caseDetails.impact && !(['P0', 'P1']).includes(caseDetails.priority)) { errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; } diff --git a/python/3p-resources/create_3p_resources/main.py b/python/3p-resources/create_3p_resources/main.py index 6540412..7145c18 100644 --- a/python/3p-resources/create_3p_resources/main.py +++ b/python/3p-resources/create_3p_resources/main.py @@ -16,6 +16,8 @@ from typing import Any, Mapping from urllib.parse import urlparse +import os +import json import flask import functions_framework @@ -29,10 +31,235 @@ def create_3p_resources(req: flask.Request): The response object. """ event = req.get_json(silent=True) - if event["docs"]["matchedUrl"]["url"]: - return create_card(event["docs"]["matchedUrl"]["url"]) + 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): + """A support case link preview. + Args: + event: The event object. + errors: An optional dict of per-field error messages. + isUpdate: Whether to return the form as an updateCard navigation. + Returns: + A card or an action reponse. + """ + 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): + """Called when the creation form is submitted. + + If form input is valid, returns a render action that inserts a new link + into the document. If invalid, returns an updateCard navigation that + re-renders the creation form with error messages. + Args: + event: The event object. + Returns: + A card or an action reponse. + """ + 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 None + + 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"]}' + url = "https://example.com/support/cases/" + json.dumps(case_details, separators=(',', ':')) + 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 form inputs for case creation. + 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 the invalid input. + Returns: + A 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 render action 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: + A render action. + """ + 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_link_preview/main.py b/python/3p-resources/create_link_preview/main.py index 8dee240..e115fb3 100644 --- a/python/3p-resources/create_link_preview/main.py +++ b/python/3p-resources/create_link_preview/main.py @@ -16,9 +16,9 @@ from typing import Any, Mapping from urllib.parse import urlparse, unquote +import json import flask import functions_framework -import json @functions_framework.http @@ -57,7 +57,6 @@ def case_link_preview(url): # Parses the URL to identify the case details. segments = url.split("/") case_details = json.loads(unquote(segments[len(segments) - 1])); - print(case_details) # Returns the card. # Uses the text from the card's header for the title of the smart chip. From 66182daad2f2c5eb4a4c65974a5be228b58ac965 Mon Sep 17 00:00:00 2001 From: pierrick Date: Fri, 19 Jan 2024 18:02:01 +0000 Subject: [PATCH 08/21] Upgrade preview-link code sample for Java and initatie create 3P resources --- .vscode/settings.json | 3 + java/3p-resources/README.md | 82 +++++++++++++++++++ .../deployment.json | 22 +++-- java/{preview-links => 3p-resources}/pom.xml | 1 + .../src/main/java/Create3pResources.java | 39 +++++++++ .../src/main/java/CreateLinkPreview.java} | 49 +++++------ java/preview-links/README.md | 61 -------------- 7 files changed, 162 insertions(+), 95 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 java/3p-resources/README.md rename java/{preview-links => 3p-resources}/deployment.json (66%) rename java/{preview-links => 3p-resources}/pom.xml (97%) create mode 100644 java/3p-resources/src/main/java/Create3pResources.java rename java/{preview-links/src/main/java/PreviewLink.java => 3p-resources/src/main/java/CreateLinkPreview.java} (76%) delete mode 100644 java/preview-links/README.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e0f15db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/java/3p-resources/README.md b/java/3p-resources/README.md new file mode 100644 index 0000000..fb4bb30 --- /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 two link preview triggers 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 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 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 66% rename from java/preview-links/deployment.json rename to java/3p-resources/deployment.json index 57f9bf0..3165cf1 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", @@ -34,7 +35,7 @@ "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" }, { - "runFunction": "$URL", + "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", @@ -47,6 +48,17 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-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" + } ] } } diff --git a/java/preview-links/pom.xml b/java/3p-resources/pom.xml similarity index 97% rename from java/preview-links/pom.xml rename to java/3p-resources/pom.xml index cf1e825..5d5e7ed 100644 --- a/java/preview-links/pom.xml +++ b/java/3p-resources/pom.xml @@ -43,6 +43,7 @@ limitations under the License. 2.9.1 + com.google.apis google-api-services-chat 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..a1bf376 --- /dev/null +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -0,0 +1,39 @@ +/** + * 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 com.google.api.services.chat.v1.model.Card; +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import com.google.gson.Gson; + +public class Create3pResources 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 { + response.getWriter().write(gson.toJson(new Card())); + } +} + +// [END add_ons_3p_resources] diff --git a/java/preview-links/src/main/java/PreviewLink.java b/java/3p-resources/src/main/java/CreateLinkPreview.java similarity index 76% rename from java/preview-links/src/main/java/PreviewLink.java rename to java/3p-resources/src/main/java/CreateLinkPreview.java index aedc1d9..013128d 100644 --- a/java/preview-links/src/main/java/PreviewLink.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -27,15 +27,15 @@ 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.net.URLDecoder; import java.util.List; -public class PreviewLink implements HttpFunction { +public class CreateLinkPreview implements HttpFunction { private static final Gson gson = new Gson(); /** - * Responds to any HTTP request. + * Responds to any HTTP request related to link previews. * * @param request An HTTP request context. * @param response An HTTP response context. @@ -47,32 +47,21 @@ public void service(HttpRequest request, HttpResponse response) throws Exception .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(); + if ("example.com".equals(parsedURL.getHost())) { + if (parsedURL.getPath().startsWith("/support/cases/")) { + response.getWriter().write(gson.toJson(caseLinkPreview(url))); + return; + } + + if (parsedURL.getPath().startsWith("/people/")) { + response.getWriter().write(gson.toJson(peopleLinkPreview())); + return; + } } - return new Card(); + // TODO Change for the Action type with link preview + response.getWriter().write(gson.toJson(new Card())); } // [START add_ons_case_preview_link] @@ -85,13 +74,13 @@ Card createCard(String url) throws MalformedURLException { */ Card caseLinkPreview(String url) { String[] segments = url.split("/"); - String caseId = segments[segments.length - 1]; + JsonObject caseDetails = gson.fromJson(URLDecoder.decode(segments[segments.length - 1].replace("+", "%2B"), "UTF-8").replace("%2B", "+"), JsonObject.class); CardHeader cardHeader = new CardHeader(); - cardHeader.setTitle(String.format("Case %s: Title bar is broken.", caseId)); + cardHeader.setTitle(String.format("Case %s", caseDetails.get("name"))); TextParagraph textParagraph = new TextParagraph(); - textParagraph.setText("Customer can't view title on mobile device."); + textParagraph.setText(caseDetails.get("description").toString()); WidgetMarkup widget = new WidgetMarkup(); widget.setTextParagraph(textParagraph); @@ -102,6 +91,7 @@ Card caseLinkPreview(String url) { card.setHeader(cardHeader); card.setSections(List.of(section)); + // TODO Change for the Action type with link preview return card; } @@ -138,6 +128,7 @@ Card peopleLinkPreview() { card.setHeader(cardHeader); card.setSections(List.of(section)); + // TODO Change for the Action type with link preview return card; } 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 -``` - From 0cdd1f6e454438d1c817d465b0a400b182e4fcc3 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 02:02:32 +0000 Subject: [PATCH 09/21] Fix preview-link code sample for Java and implement create 3P resources --- java/3p-resources/README.md | 4 +- java/3p-resources/pom.xml | 9 - .../src/main/java/Create3pResources.java | 295 +++++++++++++++++- .../src/main/java/CreateLinkPreview.java | 112 ++++--- 4 files changed, 360 insertions(+), 60 deletions(-) diff --git a/java/3p-resources/README.md b/java/3p-resources/README.md index fb4bb30..ae89023 100644 --- a/java/3p-resources/README.md +++ b/java/3p-resources/README.md @@ -29,14 +29,14 @@ gcloud functions deploy create3pResources --runtime java11 --trigger-http -entry ### Set the URL of the create3pResources function ```sh -gcloud functions describe create_3p_resources +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 create_3p_resources --update-env-vars URL=$URL +gcloud functions deploy create3pResources --update-env-vars URL=$URL ``` ## Create an add-on deployment diff --git a/java/3p-resources/pom.xml b/java/3p-resources/pom.xml index 5d5e7ed..f8ac14b 100644 --- a/java/3p-resources/pom.xml +++ b/java/3p-resources/pom.xml @@ -35,20 +35,11 @@ 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 - diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java index a1bf376..25fa6a1 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -15,11 +15,19 @@ */ // [START add_ons_3p_resources] -import com.google.api.services.chat.v1.model.Card; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + 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; public class Create3pResources implements HttpFunction { private static final Gson gson = new Gson(); @@ -32,8 +40,291 @@ public class Create3pResources implements HttpFunction { */ @Override public void service(HttpRequest request, HttpResponse response) throws Exception { - response.getWriter().write(gson.toJson(new Card())); + 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))); + return; + } else { + response.getWriter().write(gson.toJson(createCaseInputCard(event, new HashMap(), false))); + return; + } + } + + // [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 updateCard navigation. + * @return A support case creation form card. + */ + Map createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { + + Map cardHeader1 = new HashMap(); + cardHeader1.put("title", "Create a support case"); + + Map cardSection1TextInput1 = new HashMap(); + cardSection1TextInput1.put("name", "name"); + cardSection1TextInput1.put("label", "Name"); + + Map cardSection1TextInput1Widget = new HashMap(); + cardSection1TextInput1Widget.put("textInput", cardSection1TextInput1); + + Map cardSection1TextInput2 = new HashMap(); + cardSection1TextInput2.put("name", "description"); + cardSection1TextInput2.put("label", "Description"); + cardSection1TextInput2.put("type", "MULTIPLE_LINE"); + + Map cardSection1TextInput2Widget = new HashMap(); + cardSection1TextInput2Widget.put("textInput", cardSection1TextInput2); + + Map cardSection1SelectionInput1ItemsItem1 = new HashMap(); + cardSection1SelectionInput1ItemsItem1.put("text", "P0"); + cardSection1SelectionInput1ItemsItem1.put("value", "P0"); + + Map cardSection1SelectionInput1ItemsItem2 = new HashMap(); + cardSection1SelectionInput1ItemsItem2.put("text", "P1"); + cardSection1SelectionInput1ItemsItem2.put("value", "P1"); + + Map cardSection1SelectionInput1ItemsItem3 = new HashMap(); + cardSection1SelectionInput1ItemsItem3.put("text", "P2"); + cardSection1SelectionInput1ItemsItem3.put("value", "P2"); + + Map cardSection1SelectionInput1ItemsItem4 = new HashMap(); + cardSection1SelectionInput1ItemsItem4.put("text", "P3"); + cardSection1SelectionInput1ItemsItem4.put("value", "P3"); + + List cardSection1SelectionInput1Items = new ArrayList(); + cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem1); + cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem2); + cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem3); + cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem4); + + Map cardSection1SelectionInput1 = new HashMap(); + cardSection1SelectionInput1.put("name", "priority"); + cardSection1SelectionInput1.put("label", "Priority"); + cardSection1SelectionInput1.put("type", "DROPDOWN"); + cardSection1SelectionInput1.put("items", cardSection1SelectionInput1Items); + + Map cardSection1SelectionInput1Widget = new HashMap(); + cardSection1SelectionInput1Widget.put("selectionInput", cardSection1SelectionInput1); + + Map cardSection1SelectionInput2ItemsItem1 = new HashMap(); + cardSection1SelectionInput2ItemsItem1.put("text", "Blocks a critical customer operation"); + cardSection1SelectionInput2ItemsItem1.put("value", "Blocks a critical customer operation"); + + List cardSection1SelectionInput2Items = new ArrayList(); + cardSection1SelectionInput2Items.add(cardSection1SelectionInput2ItemsItem1); + + Map cardSection1SelectionInput2 = new HashMap(); + cardSection1SelectionInput2.put("name", "impact"); + cardSection1SelectionInput2.put("label", "Impact"); + cardSection1SelectionInput2.put("items", cardSection1SelectionInput2Items); + + Map cardSection1SelectionInput2Widget = new HashMap(); + cardSection1SelectionInput2Widget.put("selectionInput", cardSection1SelectionInput2); + + Map cardSection1ButtonList1Button1Action1ParametersParameter1 = new HashMap(); + cardSection1ButtonList1Button1Action1ParametersParameter1.put("key", "submitCaseCreationForm"); + cardSection1ButtonList1Button1Action1ParametersParameter1.put("value", true); + + List cardSection1ButtonList1Button1Action1Parameters = new ArrayList(); + cardSection1ButtonList1Button1Action1Parameters.add(cardSection1ButtonList1Button1Action1ParametersParameter1); + + Map cardSection1ButtonList1Button1Action1 = new HashMap(); + cardSection1ButtonList1Button1Action1.put("function", System.getenv().get("URL")); + cardSection1ButtonList1Button1Action1.put("parameters", cardSection1ButtonList1Button1Action1Parameters); + cardSection1ButtonList1Button1Action1.put("persistValues", true); + + Map cardSection1ButtonList1Button1OnCLick = new HashMap(); + cardSection1ButtonList1Button1OnCLick.put("action", cardSection1ButtonList1Button1Action1); + + Map cardSection1ButtonList1Button1 = new HashMap(); + cardSection1ButtonList1Button1.put("text", "Create"); + cardSection1ButtonList1Button1.put("onClick", cardSection1ButtonList1Button1OnCLick); + + List cardSection1ButtonList1Buttons = new ArrayList(); + cardSection1ButtonList1Buttons.add(cardSection1ButtonList1Button1); + + Map cardSection1ButtonList1 = new HashMap(); + cardSection1ButtonList1.put("buttons", cardSection1ButtonList1Buttons); + + Map cardSection1ButtonList1Widget = new HashMap(); + cardSection1ButtonList1Widget.put("buttonList", cardSection1ButtonList1); + + // Builds the creation form and adds error text for invalid inputs. + List cardSection1 = new ArrayList(); + if (errors.containsKey("name")) { + cardSection1.add(createErrorTextParagraph(errors.get("name").toString())); + } + cardSection1.add(cardSection1TextInput1Widget); + if (errors.containsKey("description")) { + cardSection1.add(createErrorTextParagraph(errors.get("description").toString())); + } + cardSection1.add(cardSection1TextInput2Widget); + if (errors.containsKey("priority")) { + cardSection1.add(createErrorTextParagraph(errors.get("priority").toString())); + } + cardSection1.add(cardSection1SelectionInput1Widget); + if (errors.containsKey("impact")) { + cardSection1.add(createErrorTextParagraph(errors.get("impact").toString())); + } + + cardSection1.add(cardSection1SelectionInput2Widget); + cardSection1.add(cardSection1ButtonList1Widget); + + Map cardSection1Widgets = new HashMap(); + cardSection1Widgets.put("widgets", cardSection1); + + List sections = new ArrayList(); + sections.add(cardSection1Widgets); + + Map card = new HashMap(); + card.put("header", cardHeader1); + card.put("sections", sections); + + Map navigation = new HashMap(); + if (isUpdate) { + navigation.put("updateCard", card); + } else { + navigation.put("pushCard", card); + } + + List navigations = new ArrayList(); + navigations.add(navigation); + + Map action = new HashMap(); + action.put("navigations", navigations); + + Map renderActions = new HashMap(); + renderActions.put("action", action); + + if (!isUpdate) { + return renderActions; + } + + Map update = new HashMap(); + update.put("renderActions", renderActions); + + return update; } + + // [END add_ons_3p_resources_create_case_card] + // [START add_ons_3p_resources_submit_create_case] + + /** + * Called when the creation form is submitted. If form input is valid, returns a render action + * that inserts a new link into the document. If invalid, returns an updateCard navigation that + * re-renders the creation form with error messages. + * + * @param event The event object containing form inputs. + * @return The navigation action. + */ + Map submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException{ + 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")); + String url = "https://example.com/support/cases/" + URLEncoder.encode(gson.toJson(caseDetails), "UTF-8").replaceAll("\\+", "%20").replaceAll("\\%21", "!").replaceAll("\\%27", "'").replaceAll("\\%28", "(").replaceAll("\\%29", ")").replaceAll("\\%7E", "~"); + return createLinkRenderAction(title, url); + } + } + + // [END add_ons_3p_resources_submit_create_case] + // [START add_ons_3p_resources_validate_inputs] + + /** + * Validates form inputs for case creation. + * + * @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 TextParagraph with red text indicating a form field validation error. + * + * @param errorMessage A description of the invalid input. + * @return A text paragraph. + */ + Map createErrorTextParagraph(String errorMessage) { + Map textParagraph = new HashMap(); + textParagraph.put("text", "Error: " + errorMessage + ""); + + Map textParagraphWidget = new HashMap(); + textParagraphWidget.put("textParagraph", textParagraph); + + return textParagraphWidget; + } + + // [END add_ons_3p_resources_validate_inputs] + // [START add_ons_3p_resources_link_render_action] + + /** + * Returns a render action 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 render action + */ + Map createLinkRenderAction(String title, String url) { + Map link1 = new HashMap(); + link1.put("title", title); + link1.put("url", url); + + List links = new ArrayList(); + links.add(link1); + + Map action = new HashMap(); + action.put("links", links); + + Map renderActions = new HashMap(); + renderActions.put("action", action); + + Map linkRenderAction = new HashMap(); + linkRenderAction.put("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 index 013128d..6af9dfb 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -15,21 +15,18 @@ */ // [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.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class CreateLinkPreview implements HttpFunction { private static final Gson gson = new Gson(); @@ -42,8 +39,8 @@ public class CreateLinkPreview implements HttpFunction { */ @Override public void service(HttpRequest request, HttpResponse response) throws Exception { - JsonObject body = gson.fromJson(request.getReader(), JsonObject.class); - String url = body.getAsJsonObject("docs") + JsonObject event = gson.fromJson(request.getReader(), JsonObject.class); + String url = event.getAsJsonObject("docs") .getAsJsonObject("matchedUrl") .get("url") .getAsString(); @@ -60,76 +57,97 @@ public void service(HttpRequest request, HttpResponse response) throws Exception } } - // TODO Change for the Action type with link preview - response.getWriter().write(gson.toJson(new Card())); + response.getWriter().write("{}"); } // [START add_ons_case_preview_link] /** - * Creates a case link preview card. + * A support case link preview. * * @param url A URL. * @return A case link preview card. */ - Card caseLinkPreview(String url) { + Map caseLinkPreview(String url) throws UnsupportedEncodingException { String[] segments = url.split("/"); JsonObject caseDetails = gson.fromJson(URLDecoder.decode(segments[segments.length - 1].replace("+", "%2B"), "UTF-8").replace("%2B", "+"), JsonObject.class); - CardHeader cardHeader = new CardHeader(); - cardHeader.setTitle(String.format("Case %s", caseDetails.get("name"))); + Map cardHeader = new HashMap(); + cardHeader.put("title", String.format("Case %s", caseDetails.get("name").getAsString())); + + Map textParagraph = new HashMap(); + textParagraph.put("text", caseDetails.get("description").getAsString()); + + Map widget = new HashMap(); + widget.put("textParagraph", textParagraph); + + Map section = new HashMap(); + section.put("widgets", List.of(widget)); + + Map previewCard = new HashMap(); + previewCard.put("header", cardHeader); + previewCard.put("sections", List.of(section)); - TextParagraph textParagraph = new TextParagraph(); - textParagraph.setText(caseDetails.get("description").toString()); + Map linkPreview = new HashMap(); + linkPreview.put("title", String.format("Case %s", caseDetails.get("name").getAsString())); + linkPreview.put("previewCard", previewCard); - WidgetMarkup widget = new WidgetMarkup(); - widget.setTextParagraph(textParagraph); - Section section = new Section(); - section.setWidgets(List.of(widget)); + Map action = new HashMap(); + action.put("linkPreview", linkPreview); - Card card = new Card(); - card.setHeader(cardHeader); - card.setSections(List.of(section)); + Map renderActions = new HashMap(); + renderActions.put("action", action); - // TODO Change for the Action type with link preview - return card; + return renderActions; } // [END add_ons_case_preview_link] // [START add_ons_people_preview_link] /** - * Creates a people link preview card. + * An employee profile link preview. * * @return A people link preview card. */ - Card peopleLinkPreview() { - CardHeader cardHeader = new CardHeader(); - cardHeader.setTitle("Rosario Cruz"); + Map peopleLinkPreview() { + Map cardHeader = new HashMap(); + cardHeader.put("title", "Rosario Cruz"); + + Map image = new HashMap(); + image.put("imageUrl", "https://developers.google.com/workspace/add-ons/images/employee-profile.png"); + + Map imageWidget = new HashMap(); + imageWidget.put("image", image); + + Map startIcon = new HashMap(); + startIcon.put("knownIcon", "EMAIL"); + + Map decoratedText = new HashMap(); + decoratedText.put("startIcon", startIcon); + decoratedText.put("text", "rosario@example.com"); + decoratedText.put("bottomLabel", "Case Manager"); - Image image = new Image(); - image.setImageUrl("https://developers.google.com/workspace/add-ons/images/employee-profile.png"); + Map decoratedTextWidget = new HashMap(); + decoratedTextWidget.put("decoratedText", decoratedText); - WidgetMarkup imageWidget = new WidgetMarkup(); - imageWidget.setImage(image); + Map section = new HashMap(); + section.put("widgets", List.of(imageWidget, decoratedTextWidget)); - KeyValue keyValue = new KeyValue(); - keyValue.setIcon("EMAIL"); - keyValue.setContent("rosario@example.com"); - keyValue.setBottomLabel("Case Manager"); + Map previewCard = new HashMap(); + previewCard.put("header", cardHeader); + previewCard.put("sections", List.of(section)); - WidgetMarkup keyValueWidget = new WidgetMarkup(); - keyValueWidget.setKeyValue(keyValue); + Map linkPreview = new HashMap(); + linkPreview.put("title", "Rosario Cruz"); + linkPreview.put("previewCard", previewCard); - Section section = new Section(); - section.setWidgets(List.of(imageWidget, keyValueWidget)); + Map action = new HashMap(); + action.put("linkPreview", linkPreview); - Card card = new Card(); - card.setHeader(cardHeader); - card.setSections(List.of(section)); + Map renderActions = new HashMap(); + renderActions.put("action", action); - // TODO Change for the Action type with link preview - return card; + return renderActions; } // [END add_ons_people_preview_link] From 4544efa973762786c229d3f8a96d847600d15581 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 02:38:21 +0000 Subject: [PATCH 10/21] Upgraded Java impl of 3p-resources to use JsonObjects --- .../src/main/java/Create3pResources.java | 208 +++++++++--------- .../src/main/java/CreateLinkPreview.java | 112 +++++----- 2 files changed, 167 insertions(+), 153 deletions(-) diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java index 25fa6a1..65821dc 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -17,17 +17,17 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; 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(); @@ -46,7 +46,7 @@ public void service(HttpRequest request, HttpResponse response) throws Exception response.getWriter().write(gson.toJson(submitCaseCreationForm(event))); return; } else { - response.getWriter().write(gson.toJson(createCaseInputCard(event, new HashMap(), false))); + response.getWriter().write(gson.toJson(createCaseInputCard(event, new HashMap(), false))); return; } } @@ -61,102 +61,102 @@ public void service(HttpRequest request, HttpResponse response) throws Exception * @param isUpdate Whether to return the form as an updateCard navigation. * @return A support case creation form card. */ - Map createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { + JsonObject createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { - Map cardHeader1 = new HashMap(); - cardHeader1.put("title", "Create a support case"); + JsonObject cardHeader1 = new JsonObject(); + cardHeader1.add("title", new JsonPrimitive("Create a support case")); - Map cardSection1TextInput1 = new HashMap(); - cardSection1TextInput1.put("name", "name"); - cardSection1TextInput1.put("label", "Name"); + JsonObject cardSection1TextInput1 = new JsonObject(); + cardSection1TextInput1.add("name", new JsonPrimitive("name")); + cardSection1TextInput1.add("label", new JsonPrimitive("Name")); - Map cardSection1TextInput1Widget = new HashMap(); - cardSection1TextInput1Widget.put("textInput", cardSection1TextInput1); + JsonObject cardSection1TextInput1Widget = new JsonObject(); + cardSection1TextInput1Widget.add("textInput", cardSection1TextInput1); - Map cardSection1TextInput2 = new HashMap(); - cardSection1TextInput2.put("name", "description"); - cardSection1TextInput2.put("label", "Description"); - cardSection1TextInput2.put("type", "MULTIPLE_LINE"); + JsonObject cardSection1TextInput2 = new JsonObject(); + cardSection1TextInput2.add("name", new JsonPrimitive("description")); + cardSection1TextInput2.add("label", new JsonPrimitive("Description")); + cardSection1TextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE")); - Map cardSection1TextInput2Widget = new HashMap(); - cardSection1TextInput2Widget.put("textInput", cardSection1TextInput2); + JsonObject cardSection1TextInput2Widget = new JsonObject(); + cardSection1TextInput2Widget.add("textInput", cardSection1TextInput2); - Map cardSection1SelectionInput1ItemsItem1 = new HashMap(); - cardSection1SelectionInput1ItemsItem1.put("text", "P0"); - cardSection1SelectionInput1ItemsItem1.put("value", "P0"); + JsonObject cardSection1SelectionInput1ItemsItem1 = new JsonObject(); + cardSection1SelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0")); + cardSection1SelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0")); - Map cardSection1SelectionInput1ItemsItem2 = new HashMap(); - cardSection1SelectionInput1ItemsItem2.put("text", "P1"); - cardSection1SelectionInput1ItemsItem2.put("value", "P1"); + JsonObject cardSection1SelectionInput1ItemsItem2 = new JsonObject(); + cardSection1SelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1")); + cardSection1SelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1")); - Map cardSection1SelectionInput1ItemsItem3 = new HashMap(); - cardSection1SelectionInput1ItemsItem3.put("text", "P2"); - cardSection1SelectionInput1ItemsItem3.put("value", "P2"); + JsonObject cardSection1SelectionInput1ItemsItem3 = new JsonObject(); + cardSection1SelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2")); + cardSection1SelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2")); - Map cardSection1SelectionInput1ItemsItem4 = new HashMap(); - cardSection1SelectionInput1ItemsItem4.put("text", "P3"); - cardSection1SelectionInput1ItemsItem4.put("value", "P3"); + JsonObject cardSection1SelectionInput1ItemsItem4 = new JsonObject(); + cardSection1SelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3")); + cardSection1SelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3")); - List cardSection1SelectionInput1Items = new ArrayList(); + JsonArray cardSection1SelectionInput1Items = new JsonArray(); cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem1); cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem2); cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem3); cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem4); - Map cardSection1SelectionInput1 = new HashMap(); - cardSection1SelectionInput1.put("name", "priority"); - cardSection1SelectionInput1.put("label", "Priority"); - cardSection1SelectionInput1.put("type", "DROPDOWN"); - cardSection1SelectionInput1.put("items", cardSection1SelectionInput1Items); + JsonObject cardSection1SelectionInput1 = new JsonObject(); + cardSection1SelectionInput1.add("name", new JsonPrimitive("priority")); + cardSection1SelectionInput1.add("label", new JsonPrimitive("Priority")); + cardSection1SelectionInput1.add("type", new JsonPrimitive("DROPDOWN")); + cardSection1SelectionInput1.add("items", cardSection1SelectionInput1Items); - Map cardSection1SelectionInput1Widget = new HashMap(); - cardSection1SelectionInput1Widget.put("selectionInput", cardSection1SelectionInput1); + JsonObject cardSection1SelectionInput1Widget = new JsonObject(); + cardSection1SelectionInput1Widget.add("selectionInput", cardSection1SelectionInput1); - Map cardSection1SelectionInput2ItemsItem1 = new HashMap(); - cardSection1SelectionInput2ItemsItem1.put("text", "Blocks a critical customer operation"); - cardSection1SelectionInput2ItemsItem1.put("value", "Blocks a critical customer operation"); - - List cardSection1SelectionInput2Items = new ArrayList(); + JsonObject cardSection1SelectionInput2ItemsItem1 = new JsonObject(); + cardSection1SelectionInput2ItemsItem1.add("text", new JsonPrimitive("Blocks a critical customer operation")); + cardSection1SelectionInput2ItemsItem1.add("value", new JsonPrimitive("Blocks a critical customer operation")); + + JsonArray cardSection1SelectionInput2Items = new JsonArray(); cardSection1SelectionInput2Items.add(cardSection1SelectionInput2ItemsItem1); - Map cardSection1SelectionInput2 = new HashMap(); - cardSection1SelectionInput2.put("name", "impact"); - cardSection1SelectionInput2.put("label", "Impact"); - cardSection1SelectionInput2.put("items", cardSection1SelectionInput2Items); + JsonObject cardSection1SelectionInput2 = new JsonObject(); + cardSection1SelectionInput2.add("name", new JsonPrimitive("impact")); + cardSection1SelectionInput2.add("label", new JsonPrimitive("Impact")); + cardSection1SelectionInput2.add("items", cardSection1SelectionInput2Items); - Map cardSection1SelectionInput2Widget = new HashMap(); - cardSection1SelectionInput2Widget.put("selectionInput", cardSection1SelectionInput2); + JsonObject cardSection1SelectionInput2Widget = new JsonObject(); + cardSection1SelectionInput2Widget.add("selectionInput", cardSection1SelectionInput2); - Map cardSection1ButtonList1Button1Action1ParametersParameter1 = new HashMap(); - cardSection1ButtonList1Button1Action1ParametersParameter1.put("key", "submitCaseCreationForm"); - cardSection1ButtonList1Button1Action1ParametersParameter1.put("value", true); + JsonObject cardSection1ButtonList1Button1Action1ParametersParameter1 = new JsonObject(); + cardSection1ButtonList1Button1Action1ParametersParameter1.add("key", new JsonPrimitive("submitCaseCreationForm")); + cardSection1ButtonList1Button1Action1ParametersParameter1.add("value", new JsonPrimitive(true)); - List cardSection1ButtonList1Button1Action1Parameters = new ArrayList(); + JsonArray cardSection1ButtonList1Button1Action1Parameters = new JsonArray(); cardSection1ButtonList1Button1Action1Parameters.add(cardSection1ButtonList1Button1Action1ParametersParameter1); - Map cardSection1ButtonList1Button1Action1 = new HashMap(); - cardSection1ButtonList1Button1Action1.put("function", System.getenv().get("URL")); - cardSection1ButtonList1Button1Action1.put("parameters", cardSection1ButtonList1Button1Action1Parameters); - cardSection1ButtonList1Button1Action1.put("persistValues", true); + JsonObject cardSection1ButtonList1Button1Action1 = new JsonObject(); + cardSection1ButtonList1Button1Action1.add("function", new JsonPrimitive(System.getenv().get("URL"))); + cardSection1ButtonList1Button1Action1.add("parameters", cardSection1ButtonList1Button1Action1Parameters); + cardSection1ButtonList1Button1Action1.add("persistValues", new JsonPrimitive(true)); - Map cardSection1ButtonList1Button1OnCLick = new HashMap(); - cardSection1ButtonList1Button1OnCLick.put("action", cardSection1ButtonList1Button1Action1); + JsonObject cardSection1ButtonList1Button1OnCLick = new JsonObject(); + cardSection1ButtonList1Button1OnCLick.add("action", cardSection1ButtonList1Button1Action1); - Map cardSection1ButtonList1Button1 = new HashMap(); - cardSection1ButtonList1Button1.put("text", "Create"); - cardSection1ButtonList1Button1.put("onClick", cardSection1ButtonList1Button1OnCLick); + JsonObject cardSection1ButtonList1Button1 = new JsonObject(); + cardSection1ButtonList1Button1.add("text", new JsonPrimitive("Create")); + cardSection1ButtonList1Button1.add("onClick", cardSection1ButtonList1Button1OnCLick); - List cardSection1ButtonList1Buttons = new ArrayList(); + JsonArray cardSection1ButtonList1Buttons = new JsonArray(); cardSection1ButtonList1Buttons.add(cardSection1ButtonList1Button1); - Map cardSection1ButtonList1 = new HashMap(); - cardSection1ButtonList1.put("buttons", cardSection1ButtonList1Buttons); + JsonObject cardSection1ButtonList1 = new JsonObject(); + cardSection1ButtonList1.add("buttons", cardSection1ButtonList1Buttons); - Map cardSection1ButtonList1Widget = new HashMap(); - cardSection1ButtonList1Widget.put("buttonList", cardSection1ButtonList1); + JsonObject cardSection1ButtonList1Widget = new JsonObject(); + cardSection1ButtonList1Widget.add("buttonList", cardSection1ButtonList1); // Builds the creation form and adds error text for invalid inputs. - List cardSection1 = new ArrayList(); + JsonArray cardSection1 = new JsonArray(); if (errors.containsKey("name")) { cardSection1.add(createErrorTextParagraph(errors.get("name").toString())); } @@ -176,38 +176,38 @@ Map createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { cardSection1.add(cardSection1SelectionInput2Widget); cardSection1.add(cardSection1ButtonList1Widget); - Map cardSection1Widgets = new HashMap(); - cardSection1Widgets.put("widgets", cardSection1); + JsonObject cardSection1Widgets = new JsonObject(); + cardSection1Widgets.add("widgets", cardSection1); - List sections = new ArrayList(); + JsonArray sections = new JsonArray(); sections.add(cardSection1Widgets); - Map card = new HashMap(); - card.put("header", cardHeader1); - card.put("sections", sections); + JsonObject card = new JsonObject(); + card.add("header", cardHeader1); + card.add("sections", sections); - Map navigation = new HashMap(); + JsonObject navigation = new JsonObject(); if (isUpdate) { - navigation.put("updateCard", card); + navigation.add("updateCard", card); } else { - navigation.put("pushCard", card); + navigation.add("pushCard", card); } - List navigations = new ArrayList(); + JsonArray navigations = new JsonArray(); navigations.add(navigation); - Map action = new HashMap(); - action.put("navigations", navigations); + JsonObject action = new JsonObject(); + action.add("navigations", navigations); - Map renderActions = new HashMap(); - renderActions.put("action", action); + JsonObject renderActions = new JsonObject(); + renderActions.add("action", action); if (!isUpdate) { return renderActions; } - Map update = new HashMap(); - update.put("renderActions", renderActions); + JsonObject update = new JsonObject(); + update.add("renderActions", renderActions); return update; } @@ -223,9 +223,9 @@ Map createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { * @param event The event object containing form inputs. * @return The navigation action. */ - Map submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException{ + JsonObject submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException{ JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs"); - Map caseDetails = new HashMap(); + Map caseDetails = new HashMap(); if (formInputs != null) { if (formInputs.has("name")) { caseDetails.put("name", formInputs.getAsJsonObject("name").getAsJsonObject("stringInputs").getAsJsonArray("value").get(0).getAsString()); @@ -241,7 +241,7 @@ Map submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException } } - Map errors = validateFormInputs(caseDetails); + Map errors = validateFormInputs(caseDetails); if (errors.size() > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { @@ -261,8 +261,8 @@ Map submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException * @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(); + Map validateFormInputs(Map caseDetails) { + Map errors = new HashMap(); if (!caseDetails.containsKey("name")) { errors.put("name", "You must provide a name"); } @@ -285,12 +285,12 @@ Map validateFormInputs(Map caseDetails) { * @param errorMessage A description of the invalid input. * @return A text paragraph. */ - Map createErrorTextParagraph(String errorMessage) { - Map textParagraph = new HashMap(); - textParagraph.put("text", "Error: " + errorMessage + ""); + JsonObject createErrorTextParagraph(String errorMessage) { + JsonObject textParagraph = new JsonObject(); + textParagraph.add("text", new JsonPrimitive("Error: " + errorMessage + "")); - Map textParagraphWidget = new HashMap(); - textParagraphWidget.put("textParagraph", textParagraph); + JsonObject textParagraphWidget = new JsonObject(); + textParagraphWidget.add("textParagraph", textParagraph); return textParagraphWidget; } @@ -304,22 +304,22 @@ Map createErrorTextParagraph(String errorMessage) { * @param url The URL of the link to insert. * @return The render action */ - Map createLinkRenderAction(String title, String url) { - Map link1 = new HashMap(); - link1.put("title", title); - link1.put("url", url); + JsonObject createLinkRenderAction(String title, String url) { + JsonObject link1 = new JsonObject(); + link1.add("title", new JsonPrimitive(title)); + link1.add("url", new JsonPrimitive(url)); - List links = new ArrayList(); + JsonArray links = new JsonArray(); links.add(link1); - Map action = new HashMap(); - action.put("links", links); + JsonObject action = new JsonObject(); + action.add("links", links); - Map renderActions = new HashMap(); - renderActions.put("action", action); + JsonObject renderActions = new JsonObject(); + renderActions.add("action", action); - Map linkRenderAction = new HashMap(); - linkRenderAction.put("renderActions", renderActions); + JsonObject linkRenderAction = new JsonObject(); + linkRenderAction.add("renderActions", renderActions); return linkRenderAction; } diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index 6af9dfb..a73203b 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -19,14 +19,13 @@ 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.List; -import java.util.Map; public class CreateLinkPreview implements HttpFunction { private static final Gson gson = new Gson(); @@ -68,35 +67,43 @@ public void service(HttpRequest request, HttpResponse response) throws Exception * @param url A URL. * @return A case link preview card. */ - Map caseLinkPreview(String url) throws UnsupportedEncodingException { + JsonObject caseLinkPreview(String url) throws UnsupportedEncodingException { String[] segments = url.split("/"); JsonObject caseDetails = gson.fromJson(URLDecoder.decode(segments[segments.length - 1].replace("+", "%2B"), "UTF-8").replace("%2B", "+"), JsonObject.class); + String caseName = String.format("Case %s", caseDetails.get("name").getAsString()); + String caseDescription = caseDetails.get("description").getAsString(); - Map cardHeader = new HashMap(); - cardHeader.put("title", String.format("Case %s", caseDetails.get("name").getAsString())); + JsonObject cardHeader = new JsonObject(); + cardHeader.add("title", new JsonPrimitive(caseName)); - Map textParagraph = new HashMap(); - textParagraph.put("text", caseDetails.get("description").getAsString()); + JsonObject textParagraph = new JsonObject(); + textParagraph.add("text", new JsonPrimitive(caseDescription)); - Map widget = new HashMap(); - widget.put("textParagraph", textParagraph); + JsonObject widget = new JsonObject(); + widget.add("textParagraph", textParagraph); - Map section = new HashMap(); - section.put("widgets", List.of(widget)); + JsonArray widgets = new JsonArray(); + widgets.add(widget); - Map previewCard = new HashMap(); - previewCard.put("header", cardHeader); - previewCard.put("sections", List.of(section)); + JsonObject section = new JsonObject(); + section.add("widgets", widgets); - Map linkPreview = new HashMap(); - linkPreview.put("title", String.format("Case %s", caseDetails.get("name").getAsString())); - linkPreview.put("previewCard", previewCard); + JsonArray sections = new JsonArray(); + sections.add(section); - Map action = new HashMap(); - action.put("linkPreview", linkPreview); + JsonObject previewCard = new JsonObject(); + previewCard.add("header", cardHeader); + previewCard.add("sections", sections); - Map renderActions = new HashMap(); - renderActions.put("action", action); + 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; } @@ -109,43 +116,50 @@ Map caseLinkPreview(String url) throws UnsupportedEncodingException { * * @return A people link preview card. */ - Map peopleLinkPreview() { - Map cardHeader = new HashMap(); - cardHeader.put("title", "Rosario Cruz"); + JsonObject peopleLinkPreview() { + JsonObject cardHeader = new JsonObject(); + cardHeader.add("title", new JsonPrimitive("Rosario Cruz")); + + JsonObject image = new JsonObject(); + image.add("imageUrl", new JsonPrimitive("https://developers.google.com/workspace/add-ons/images/employee-profile.png")); + + JsonObject imageWidget = new JsonObject(); + imageWidget.add("image", image); - Map image = new HashMap(); - image.put("imageUrl", "https://developers.google.com/workspace/add-ons/images/employee-profile.png"); + JsonObject startIcon = new JsonObject(); + startIcon.add("knownIcon", new JsonPrimitive("EMAIL")); - Map imageWidget = new HashMap(); - imageWidget.put("image", image); + JsonObject decoratedText = new JsonObject(); + decoratedText.add("startIcon", startIcon); + decoratedText.add("text", new JsonPrimitive("rosario@example.com")); + decoratedText.add("bottomLabel", new JsonPrimitive("Case Manager")); - Map startIcon = new HashMap(); - startIcon.put("knownIcon", "EMAIL"); + JsonObject decoratedTextWidget = new JsonObject(); + decoratedTextWidget.add("decoratedText", decoratedText); - Map decoratedText = new HashMap(); - decoratedText.put("startIcon", startIcon); - decoratedText.put("text", "rosario@example.com"); - decoratedText.put("bottomLabel", "Case Manager"); + JsonArray widgets = new JsonArray(); + widgets.add(imageWidget); + widgets.add(decoratedTextWidget); - Map decoratedTextWidget = new HashMap(); - decoratedTextWidget.put("decoratedText", decoratedText); + JsonObject section = new JsonObject(); + section.add("widgets", widgets); - Map section = new HashMap(); - section.put("widgets", List.of(imageWidget, decoratedTextWidget)); + JsonArray sections = new JsonArray(); + sections.add(section); - Map previewCard = new HashMap(); - previewCard.put("header", cardHeader); - previewCard.put("sections", List.of(section)); + JsonObject previewCard = new JsonObject(); + previewCard.add("header", cardHeader); + previewCard.add("sections", sections); - Map linkPreview = new HashMap(); - linkPreview.put("title", "Rosario Cruz"); - linkPreview.put("previewCard", previewCard); + JsonObject linkPreview = new JsonObject(); + linkPreview.add("title", new JsonPrimitive("Rosario Cruz")); + linkPreview.add("previewCard", previewCard); - Map action = new HashMap(); - action.put("linkPreview", linkPreview); + JsonObject action = new JsonObject(); + action.add("linkPreview", linkPreview); - Map renderActions = new HashMap(); - renderActions.put("action", action); + JsonObject renderActions = new JsonObject(); + renderActions.add("action", action); return renderActions; } From 471a5d67d6c179af66febe65872ef0307fc61aa1 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 03:08:07 +0000 Subject: [PATCH 11/21] Upgrade from json encoded string to URL params in Java --- java/3p-resources/pom.xml | 5 +++++ .../src/main/java/Create3pResources.java | 13 ++++++++----- .../src/main/java/CreateLinkPreview.java | 17 ++++++++++------- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/java/3p-resources/pom.xml b/java/3p-resources/pom.xml index f8ac14b..251d5d3 100644 --- a/java/3p-resources/pom.xml +++ b/java/3p-resources/pom.xml @@ -40,6 +40,11 @@ limitations under the License. gson 2.9.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 index 65821dc..923b6a5 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -15,12 +15,12 @@ */ // [START add_ons_3p_resources] -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; 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; @@ -223,7 +223,7 @@ JsonObject createCaseInputCard(JsonObject event, Map errors, boo * @param event The event object containing form inputs. * @return The navigation action. */ - JsonObject submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingException{ + JsonObject submitCaseCreationForm(JsonObject event) throws Exception { JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs"); Map caseDetails = new HashMap(); if (formInputs != null) { @@ -246,8 +246,11 @@ JsonObject submitCaseCreationForm(JsonObject event) throws UnsupportedEncodingEx return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { String title = String.format("Case %s", caseDetails.get("name")); - String url = "https://example.com/support/cases/" + URLEncoder.encode(gson.toJson(caseDetails), "UTF-8").replaceAll("\\+", "%20").replaceAll("\\%21", "!").replaceAll("\\%27", "'").replaceAll("\\%28", "(").replaceAll("\\%29", ")").replaceAll("\\%7E", "~"); - return createLinkRenderAction(title, 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()); } } diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index a73203b..41b13d4 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -26,6 +26,8 @@ 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(); @@ -46,7 +48,7 @@ public void service(HttpRequest request, HttpResponse response) throws Exception URL parsedURL = new URL(url); if ("example.com".equals(parsedURL.getHost())) { if (parsedURL.getPath().startsWith("/support/cases/")) { - response.getWriter().write(gson.toJson(caseLinkPreview(url))); + response.getWriter().write(gson.toJson(caseLinkPreview(parsedURL))); return; } @@ -67,17 +69,18 @@ public void service(HttpRequest request, HttpResponse response) throws Exception * @param url A URL. * @return A case link preview card. */ - JsonObject caseLinkPreview(String url) throws UnsupportedEncodingException { - String[] segments = url.split("/"); - JsonObject caseDetails = gson.fromJson(URLDecoder.decode(segments[segments.length - 1].replace("+", "%2B"), "UTF-8").replace("%2B", "+"), JsonObject.class); - String caseName = String.format("Case %s", caseDetails.get("name").getAsString()); - String caseDescription = caseDetails.get("description").getAsString(); + JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException { + 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")); + } 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(caseDescription)); + textParagraph.add("text", new JsonPrimitive(caseDetails.get("description"))); JsonObject widget = new JsonObject(); widget.add("textParagraph", textParagraph); From 37f9a764690a79b6af5b3c01036bbcd0eeaeb169 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 03:43:12 +0000 Subject: [PATCH 12/21] Upgrade from json envoded string to URL params in Node and migrated to Node 20 --- node/3p-resources/README.md | 4 ++-- node/3p-resources/index.js | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/node/3p-resources/README.md b/node/3p-resources/README.md index a074a7f..78a78af 100644 --- a/node/3p-resources/README.md +++ b/node/3p-resources/README.md @@ -22,8 +22,8 @@ gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.goo ### Deploy the functions ```sh -gcloud functions deploy createLinkPreview --runtime nodejs16 --trigger-http -gcloud functions deploy create3pResources --runtime nodejs16 --trigger-http +gcloud functions deploy createLinkPreview --runtime nodejs20 --trigger-http +gcloud functions deploy create3pResources --runtime nodejs20 --trigger-http ``` ### Set the URL of the create3pResources function diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js index 2d5f49a..fa5173d 100644 --- a/node/3p-resources/index.js +++ b/node/3p-resources/index.js @@ -29,7 +29,7 @@ exports.createLinkPreview = (req, res) => { const parsedUrl = new URL(url); if (parsedUrl.hostname === 'example.com') { if (parsedUrl.pathname.startsWith('/support/cases/')) { - return res.json(caseLinkPreview(url)); + return res.json(caseLinkPreview(parsedUrl)); } if (parsedUrl.pathname.startsWith('/people/')) { @@ -45,29 +45,25 @@ exports.createLinkPreview = (req, res) => { * * A support case link preview. * - * @param {!string} url + * @param {!URL} url * @return {!Card} */ function caseLinkPreview(url) { - - // Parses the URL to identify the case details. - const segments = url.split('/'); - const caseDetails = JSON.parse(decodeURIComponent(segments[segments.length - 1])); - // Returns the card. // Uses the text from the card's header for the title of the smart chip. + const name = `Case ${url.searchParams.get("name")}`; return { action: { linkPreview: { - title: `Case ${caseDetails.name}`, + title: name, previewCard: { header: { - title: `Case ${caseDetails.name}` + title: name }, sections: [{ widgets: [{ textParagraph: { - text: caseDetails.description + text: url.searchParams.get("description") } }] }] @@ -300,8 +296,11 @@ function submitCaseCreationForm(event) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { const title = `Case ${caseDetails.name}`; - const url = 'https://example.com/support/cases/' + encodeURIComponent(JSON.stringify(caseDetails)); - return createLinkRenderAction(title, 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); } } From cb472454b9e526df0b957ff81ec3a57277d576a5 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 04:04:37 +0000 Subject: [PATCH 13/21] Upgrade from json envoded string to URL params in Python --- python/3p-resources/create_3p_resources/main.py | 7 +++---- python/3p-resources/create_link_preview/main.py | 17 +++++++---------- python/3p-resources/deployment.json | 2 +- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/python/3p-resources/create_3p_resources/main.py b/python/3p-resources/create_3p_resources/main.py index 7145c18..dcb72cc 100644 --- a/python/3p-resources/create_3p_resources/main.py +++ b/python/3p-resources/create_3p_resources/main.py @@ -14,10 +14,9 @@ # [START add_ons_3p_resources] from typing import Any, Mapping -from urllib.parse import urlparse +from urllib.parse import urlencode import os -import json import flask import functions_framework @@ -195,14 +194,14 @@ def submit_case_creation_form(event): 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 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"]}' - url = "https://example.com/support/cases/" + json.dumps(case_details, separators=(',', ':')) + url = "https://example.com/support/cases/?" + urlencode(case_details) return create_link_render_action(title, url) # [END add_ons_3p_resources_submit_create_case] diff --git a/python/3p-resources/create_link_preview/main.py b/python/3p-resources/create_link_preview/main.py index e115fb3..33e234d 100644 --- a/python/3p-resources/create_link_preview/main.py +++ b/python/3p-resources/create_link_preview/main.py @@ -14,9 +14,8 @@ # [START add_ons_preview_link] from typing import Any, Mapping -from urllib.parse import urlparse, unquote +from urllib.parse import urlparse, parse_qs -import json import flask import functions_framework @@ -35,7 +34,7 @@ def create_link_preview(req: flask.Request): parsed_url = urlparse(url) if parsed_url.hostname == "example.com": if parsed_url.path.startswith("/support/cases/"): - return case_link_preview(url) + return case_link_preview(parsed_url) if parsed_url.path.startswith("/people/"): return people_link_preview() @@ -54,24 +53,22 @@ def case_link_preview(url): A case link preview card. """ - # Parses the URL to identify the case details. - segments = url.split("/") - case_details = json.loads(unquote(segments[len(segments) - 1])); - # Returns the card. # Uses the text from the card's header for the title of the smart chip. + query_string = parse_qs(url.query) + name = f'Case {query_string["name"][0]}' return { "action": { "linkPreview": { - "title": f'Case {case_details["name"]}', + "title": name, "previewCard": { "header": { - "title": f'Case {case_details["name"]}' + "title": name }, "sections": [{ "widgets": [{ "textParagraph": { - "text": case_details["description"] + "text": query_string["description"][0] } }] }], diff --git a/python/3p-resources/deployment.json b/python/3p-resources/deployment.json index 3165cf1..a4757a4 100644 --- a/python/3p-resources/deployment.json +++ b/python/3p-resources/deployment.json @@ -48,7 +48,7 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } -], + ], "createActionTriggers": [ { "id": "createCase", From bab43d7abb768edbc0f6c5d2cd31e04e828823a7 Mon Sep 17 00:00:00 2001 From: pierrick Date: Sat, 20 Jan 2024 04:27:43 +0000 Subject: [PATCH 14/21] Upgrade from json envoded string to URL params in Apps Script --- apps-script/3p-resources/3p-resources.gs | 36 ++++++++++++++++++++---- java/3p-resources/deployment.json | 2 +- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 4155da5..f1e18a9 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -22,21 +22,19 @@ * @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 details. - const segments = event.docs.matchedUrl.url.split('/'); - const caseDetails = JSON.parse(decodeURIComponent(segments[segments.length - 1])); + 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}`); + .setTitle(`Case ${caseDetails["name"][0]}`); const caseDescription = CardService.newTextParagraph() - .setText(caseDetails.description); + .setText(caseDetails["description"][0]); // Returns the card. // Uses the text from the card's header for the title of the smart chip. @@ -47,6 +45,32 @@ function caseLinkPreview(event) { } } +/** +* Extract the URL parameters from the given URL. +* +* @param {!string} url +* @return {!Map} A map of URL parameters. +*/ +function parseQuery(url) { + var 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] // [START add_ons_people_preview_link] @@ -197,7 +221,7 @@ function submitCaseCreationForm(event) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { const title = `Case ${caseDetails.name}`; - const url = 'https://example.com/support/cases/' + encodeURIComponent(JSON.stringify(caseDetails)); + const url = 'https://example.com/support/cases/?' + Object.entries(caseDetails).flatMap(([k, v]) => Array.isArray(v) ? v.map(e => `${k}=${encodeURIComponent(e)}`) : `${k}=${encodeURIComponent(v)}`).join("&"); return createLinkRenderAction(title, url); } } diff --git a/java/3p-resources/deployment.json b/java/3p-resources/deployment.json index 3165cf1..a4757a4 100644 --- a/java/3p-resources/deployment.json +++ b/java/3p-resources/deployment.json @@ -48,7 +48,7 @@ }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } -], + ], "createActionTriggers": [ { "id": "createCase", From f09869d2cd9e3ee5ae62d5c5ef62bd07dd2a4ba8 Mon Sep 17 00:00:00 2001 From: pierrick Date: Mon, 22 Jan 2024 15:33:40 +0000 Subject: [PATCH 15/21] Remove .vscode files from change --- .gitignore | 1 + .vscode/settings.json | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 .gitignore delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file From 1a8dd9f0ec14531f448a96da069f779263e7984a Mon Sep 17 00:00:00 2001 From: pierrick Date: Mon, 22 Jan 2024 16:19:54 +0000 Subject: [PATCH 16/21] Reviewed Apps Script comments --- apps-script/3p-resources/3p-resources.gs | 43 ++++++++++++------------ 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index f1e18a9..fae0154 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -17,10 +17,10 @@ // [START add_ons_case_preview_link] /** -* Entry point for a support case link preview +* Entry point for a support case link preview. * -* @param {!Object} event -* @return {!Card} +* @param {!Object} event The event object. +* @return {!Card} The resulting preview link card. */ function caseLinkPreview(event) { @@ -46,10 +46,10 @@ function caseLinkPreview(event) { } /** -* Extract the URL parameters from the given URL. +* Extracts the URL parameters from the given URL. * -* @param {!string} url -* @return {!Map} A map of URL parameters. +* @param {!string} url The URL to parse. +* @return {!Map} A map with the extracted URL parameters. */ function parseQuery(url) { var query = url.split("?")[1]; @@ -112,12 +112,12 @@ function peopleLinkPreview(event) { // [START add_ons_3p_resources_create_case_card] /** - * Produces a support case creation form. + * 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 updateCard navigation. - * @return {!Card|!ActionResponse} + * @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) { @@ -162,7 +162,7 @@ function createCaseInputCard(event, errors, isUpdate) { const cardSection1ButtonList1 = CardService.newButtonSet() .addButton(cardSection1ButtonList1Button1); - // Builds the creation form and adds error text for invalid inputs. + // Builds the form inputs with error texts for invalid values. const cardSection1 = CardService.newCardSection(); if (errors?.name) { cardSection1.addWidget(createErrorTextParagraph(errors.name)); @@ -201,12 +201,12 @@ function createCaseInputCard(event, errors, isUpdate) { // [START add_ons_3p_resources_submit_create_case] /** - * Called when the creation form is submitted. If form input is valid, returns a render action - * that inserts a new link into the document. If invalid, returns an updateCard navigation that - * re-renders the creation form with error messages. + * 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 containing form inputs. - * @return {!Card|!RenderAction} + * @param {!Object} event The event object with form input values. + * @return {!ActionResponse|!SubmitFormResponse} The resulting response. */ function submitCaseCreationForm(event) { const caseDetails = { @@ -221,6 +221,7 @@ function submitCaseCreationForm(event) { 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/?' + Object.entries(caseDetails).flatMap(([k, v]) => Array.isArray(v) ? v.map(e => `${k}=${encodeURIComponent(e)}`) : `${k}=${encodeURIComponent(v)}`).join("&"); return createLinkRenderAction(title, url); } @@ -230,7 +231,7 @@ function submitCaseCreationForm(event) { // [START add_ons_3p_resources_validate_inputs] /** - * Validates form inputs for case creation. + * 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 @@ -255,10 +256,10 @@ function validateFormInputs(caseDetails) { } /** - * Returns a TextParagraph with red text indicating a form field validation error. + * Returns a text paragraph with red text indicating a form field validation error. * - * @param {string} errorMessage A description of the invalid input. - * @return {!TextParagraph} + * @param {string} errorMessage A description of input value error. + * @return {!TextParagraph} The resulting text paragraph. */ function createErrorTextParagraph(errorMessage) { return CardService.newTextParagraph() @@ -269,10 +270,10 @@ function createErrorTextParagraph(errorMessage) { // [START add_ons_3p_resources_link_render_action] /** - * Returns a RenderAction that inserts a link into the document. + * Returns asubmit 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 {!RenderAction} + * @return {!SubmitFormResponse} The resulting submit form response. */ function createLinkRenderAction(title, url) { return { From 10f5385e7cd73f2ca63fc1e5974fb61de865dda5 Mon Sep 17 00:00:00 2001 From: pierrick Date: Mon, 22 Jan 2024 17:44:22 +0000 Subject: [PATCH 17/21] Reviewed all sources specific to languages --- java/3p-resources/README.md | 2 +- java/3p-resources/src/main/java/Create3pResources.java | 2 +- java/3p-resources/src/main/java/CreateLinkPreview.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/java/3p-resources/README.md b/java/3p-resources/README.md index ae89023..47b94e7 100644 --- a/java/3p-resources/README.md +++ b/java/3p-resources/README.md @@ -23,7 +23,7 @@ gcloud services enable cloudfunctions cloudbuild.googleapis.com gsuiteaddons.goo ```sh gcloud functions deploy createLinkPreview --runtime java11 --trigger-http --entry-point CreateLinkPreview -gcloud functions deploy create3pResources --runtime java11 --trigger-http -entry-point Create3pResources +gcloud functions deploy create3pResources --runtime java11 --trigger-http --entry-point Create3pResources ``` ### Set the URL of the create3pResources function diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java index 923b6a5..2caad37 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -33,7 +33,7 @@ public class Create3pResources implements HttpFunction { private static final Gson gson = new Gson(); /** - * Responds to any HTTP request related to link previews. + * Responds to any HTTP request related to 3p resource creations. * * @param request An HTTP request context. * @param response An HTTP response context. diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index 41b13d4..f51c615 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -35,7 +35,7 @@ public class CreateLinkPreview implements HttpFunction { /** * Responds to any HTTP request related to link previews. * - * @param request An HTTP request context. + * @param request An HTTP request context. * @param response An HTTP response context. */ @Override From f4950269b9612c8f08cfd250d9ce6e404379e71c Mon Sep 17 00:00:00 2001 From: pierrick Date: Mon, 22 Jan 2024 18:15:21 +0000 Subject: [PATCH 18/21] Reviewed Python, NodeJS and Java comments --- apps-script/3p-resources/3p-resources.gs | 7 ++- .../src/main/java/Create3pResources.java | 30 +++++----- .../src/main/java/CreateLinkPreview.java | 12 +++- node/3p-resources/index.js | 55 ++++++++++--------- .../3p-resources/create_3p_resources/main.py | 36 +++++++----- .../3p-resources/create_link_preview/main.py | 15 ++--- 6 files changed, 87 insertions(+), 68 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index fae0154..1ecebb1 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -77,8 +77,8 @@ function parseQuery(url) { /** * Entry point for an employee profile link preview * -* @param {!Object} event -* @return {!Card} +* @param {!Object} event The event object. +* @return {!Card} The resulting preview link card. */ function peopleLinkPreview(event) { @@ -270,7 +270,8 @@ function createErrorTextParagraph(errorMessage) { // [START add_ons_3p_resources_link_render_action] /** - * Returns asubmit form response that inserts a link into the document. + * 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. diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java index 2caad37..a17c7a9 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -58,8 +58,8 @@ public void service(HttpRequest request, HttpResponse response) throws Exception * * @param event The event object. * @param errors A map of per-field error messages. - * @param isUpdate Whether to return the form as an updateCard navigation. - * @return A support case creation form card. + * @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) { @@ -155,7 +155,7 @@ JsonObject createCaseInputCard(JsonObject event, Map errors, boo JsonObject cardSection1ButtonList1Widget = new JsonObject(); cardSection1ButtonList1Widget.add("buttonList", cardSection1ButtonList1); - // Builds the creation form and adds error text for invalid inputs. + // Builds the form inputs with error texts for invalid values. JsonArray cardSection1 = new JsonArray(); if (errors.containsKey("name")) { cardSection1.add(createErrorTextParagraph(errors.get("name").toString())); @@ -216,12 +216,12 @@ JsonObject createCaseInputCard(JsonObject event, Map errors, boo // [START add_ons_3p_resources_submit_create_case] /** - * Called when the creation form is submitted. If form input is valid, returns a render action - * that inserts a new link into the document. If invalid, returns an updateCard navigation that - * re-renders the creation form with error messages. + * 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 containing form inputs. - * @return The navigation action. + * @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"); @@ -246,6 +246,7 @@ JsonObject submitCaseCreationForm(JsonObject event) throws Exception { 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)); @@ -258,7 +259,7 @@ JsonObject submitCaseCreationForm(JsonObject event) throws Exception { // [START add_ons_3p_resources_validate_inputs] /** - * Validates form inputs for case creation. + * 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 @@ -283,10 +284,10 @@ Map validateFormInputs(Map caseDetails) { } /** - * Returns a TextParagraph with red text indicating a form field validation error. + * Returns a text paragraph with red text indicating a form field validation error. * - * @param errorMessage A description of the invalid input. - * @return A text paragraph. + * @param errorMessage A description of input value error. + * @return The resulting text paragraph. */ JsonObject createErrorTextParagraph(String errorMessage) { JsonObject textParagraph = new JsonObject(); @@ -302,10 +303,11 @@ JsonObject createErrorTextParagraph(String errorMessage) { // [START add_ons_3p_resources_link_render_action] /** - * Returns a render action that inserts a link into the document. + * 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 render action + * @return The resulting submit form response. */ JsonObject createLinkRenderAction(String title, String url) { JsonObject link1 = new JsonObject(); diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index f51c615..d1e7c11 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -46,6 +46,7 @@ public void service(HttpRequest request, HttpResponse response) throws Exception .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))); @@ -66,15 +67,18 @@ public void service(HttpRequest request, HttpResponse response) throws Exception /** * A support case link preview. * - * @param url A URL. - * @return A case link preview card. + * @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)); @@ -117,9 +121,11 @@ JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException { /** * An employee profile link preview. * - * @return A people link preview card. + * @return The resulting preview link card. */ JsonObject peopleLinkPreview() { + // Builds a preview card with an employee's name, title, email, and profile photo. + // Uses the text from the card's header for the title of the smart chip. JsonObject cardHeader = new JsonObject(); cardHeader.add("title", new JsonPrimitive("Rosario Cruz")); diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js index fa5173d..6b6b9c3 100644 --- a/node/3p-resources/index.js +++ b/node/3p-resources/index.js @@ -16,17 +16,17 @@ // [START add_ons_preview_link] /** - * Responds to any HTTP request related to link previews for either a - * case link or people link. + * Responds to any HTTP request related to link previews. * - * @param {Object} req HTTP request context. - * @param {Object} res HTTP response context. + * @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)); @@ -45,12 +45,13 @@ exports.createLinkPreview = (req, res) => { * * A support case link preview. * - * @param {!URL} url - * @return {!Card} + * @param {!URL} url The event object. + * @return {!Card} The resulting preview link card. */ function caseLinkPreview(url) { - // Returns the card. + // 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: { @@ -79,12 +80,12 @@ function caseLinkPreview(url) { /** * An employee profile link preview. * - * @return {!Card} + * @param {!URL} url The event object. + * @return {!Card} The resulting preview link 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. + // Uses the text from the card's header for the title of the smart chip. return { action: { linkPreview: { @@ -124,8 +125,8 @@ function peopleLinkPreview() { /** * Responds to any HTTP request related to 3P resource creations. * - * @param {Object} req HTTP request context. - * @param {Object} res HTTP response context. + * @param {Object} req An HTTP request context. + * @param {Object} res An HTTP response context. */ exports.create3pResources = (req, res) => { const event = req.body; @@ -139,12 +140,12 @@ exports.create3pResources = (req, res) => { // [START add_ons_3p_resources_create_case_card] /** - * Produces a support case creation form. + * 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 updateCard navigation. - * @return {!Card|!ActionResponse} + * @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) { @@ -276,12 +277,12 @@ function createCaseInputCard(event, errors, isUpdate) { // [START add_ons_3p_resources_submit_create_case] /** - * Called when the creation form is submitted. If form input is valid, returns a render action - * that inserts a new link into the document. If invalid, returns an updateCard navigation that - * re-renders the creation form with error messages. + * 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 containing form inputs. - * @return {!Card|!RenderAction} + * @param {!Object} event The event object with form input values. + * @return {!ActionResponse|!SubmitFormResponse} The resulting response. */ function submitCaseCreationForm(event) { const caseDetails = { @@ -296,6 +297,7 @@ function submitCaseCreationForm(event) { 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); @@ -308,7 +310,7 @@ function submitCaseCreationForm(event) { // [START add_ons_3p_resources_validate_inputs] /** - * Validates form inputs for case creation. + * 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 @@ -333,10 +335,10 @@ function validateFormInputs(caseDetails) { } /** - * Returns a TextParagraph with red text indicating a form field validation error. + * Returns a text paragraph with red text indicating a form field validation error. * - * @param {string} errorMessage A description of the invalid input. - * @return {!TextParagraph} + * @param {string} errorMessage A description of input value error. + * @return {!TextParagraph} The resulting text paragraph. */ function createErrorTextParagraph(errorMessage) { return { @@ -350,10 +352,11 @@ function createErrorTextParagraph(errorMessage) { // [START add_ons_3p_resources_link_render_action] /** - * Returns a RenderAction that inserts a link into the document. + * 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 {!RenderAction} + * @return {!SubmitFormResponse} The resulting submit form response. */ function createLinkRenderAction(title, url) { return { diff --git a/python/3p-resources/create_3p_resources/main.py b/python/3p-resources/create_3p_resources/main.py index dcb72cc..e89ea9f 100644 --- a/python/3p-resources/create_3p_resources/main.py +++ b/python/3p-resources/create_3p_resources/main.py @@ -25,9 +25,9 @@ def create_3p_resources(req: flask.Request): """Responds to any HTTP request related to 3P resource creations. Args: - req: HTTP request context. + req: An HTTP request context. Returns: - The response object. + An HTTP response context. """ event = req.get_json(silent=True) parameters = event["commonEventObject"]["parameters"] if "parameters" in event["commonEventObject"] else None @@ -41,13 +41,13 @@ def create_3p_resources(req: flask.Request): def create_case_input_card(event, errors = {}, isUpdate = False): - """A support case link preview. + """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 updateCard navigation. + isUpdate: Whether to return the form as an update card navigation. Returns: - A card or an action reponse. + The resulting card or action response. """ card_header1 = { "title": "Create a support case" @@ -173,15 +173,15 @@ def create_case_input_card(event, errors = {}, isUpdate = False): def submit_case_creation_form(event): - """Called when the creation form is submitted. + """Submits the creation form. - If form input is valid, returns a render action that inserts a new link - into the document. If invalid, returns an updateCard navigation that + 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. + event: The event object with form input values. Returns: - A card or an action reponse. + The resulting response. """ formInputs = event["commonEventObject"]["formInputs"] if "formInputs" in event["commonEventObject"] else None case_details = { @@ -201,14 +201,17 @@ def submit_case_creation_form(event): 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 form inputs for case creation. + """Validates case creation form input values. Args: case_details: The values of each form input submitted by the user. Returns: @@ -225,12 +228,13 @@ def validate_form_inputs(case_details): 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 the invalid input. + error_essage: A description of input value error. Returns: - A text paragraph. + The resulting text paragraph. """ return { "textParagraph": { @@ -238,16 +242,18 @@ def create_error_text_paragraph(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 render action that inserts a link into the document. + """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: - A render action. + The resulting submit form response. """ return { "renderActions": { diff --git a/python/3p-resources/create_link_preview/main.py b/python/3p-resources/create_link_preview/main.py index 33e234d..32bb107 100644 --- a/python/3p-resources/create_link_preview/main.py +++ b/python/3p-resources/create_link_preview/main.py @@ -24,14 +24,15 @@ def create_link_preview(req: flask.Request): """Responds to any HTTP request related to link previews. Args: - req: HTTP request context. + req: An HTTP request context. Returns: - The response object. + 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) @@ -48,15 +49,15 @@ def create_link_preview(req: flask.Request): def case_link_preview(url): """A support case link preview. Args: - url: The case link. + url: A matching URL. Returns: - A case link preview card. + The resulting preview link card. """ - # Returns the card. - # Uses the text from the card's header for the title of the smart chip. + # 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": { @@ -89,7 +90,7 @@ def people_link_preview(): """ # 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. + # Uses the text from the card's header for the title of the smart chip. return { "action": { "linkPreview": { From 65bbbab5a22097c03052047f9086c730d05f132c Mon Sep 17 00:00:00 2001 From: pierrick Date: Wed, 24 Jan 2024 17:29:07 +0000 Subject: [PATCH 19/21] Remove people preview link implementation --- apps-script/3p-resources/3p-resources.gs | 34 ---------- apps-script/3p-resources/appsscript.json | 14 ----- java/3p-resources/README.md | 2 +- java/3p-resources/deployment.json | 14 ----- .../src/main/java/CreateLinkPreview.java | 63 ------------------- node/3p-resources/README.md | 2 +- node/3p-resources/deployment.json | 14 ----- node/3p-resources/index.js | 47 -------------- python/3p-resources/README.md | 2 +- .../3p-resources/create_link_preview/main.py | 47 -------------- python/3p-resources/deployment.json | 14 ----- 11 files changed, 3 insertions(+), 250 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 1ecebb1..9b25f25 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -72,40 +72,6 @@ function parseQuery(url) { } // [END add_ons_case_preview_link] -// [START add_ons_people_preview_link] - -/** -* Entry point for an employee profile link preview -* -* @param {!Object} event The event object. -* @return {!Card} The resulting preview link 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] // [START add_ons_3p_resources] diff --git a/apps-script/3p-resources/appsscript.json b/apps-script/3p-resources/appsscript.json index 6e1f897..c1752c0 100644 --- a/apps-script/3p-resources/appsscript.json +++ b/apps-script/3p-resources/appsscript.json @@ -36,20 +36,6 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, - { - "runFunction": "peopleLinkPreview", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", - "localizedLabelText": { - "es": "Personas" - }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } ], "createActionTriggers": [ diff --git a/java/3p-resources/README.md b/java/3p-resources/README.md index 47b94e7..1a2d8d3 100644 --- a/java/3p-resources/README.md +++ b/java/3p-resources/README.md @@ -1,6 +1,6 @@ # Third-Party Resources -The solution is made of two Cloud Functions, one for the two link preview triggers and +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. diff --git a/java/3p-resources/deployment.json b/java/3p-resources/deployment.json index a4757a4..7a7cc83 100644 --- a/java/3p-resources/deployment.json +++ b/java/3p-resources/deployment.json @@ -33,20 +33,6 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, - { - "runFunction": "$URL1", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", - "localizedLabelText": { - "es": "Personas" - }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } ], "createActionTriggers": [ diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index d1e7c11..d981e08 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -52,11 +52,6 @@ public void service(HttpRequest request, HttpResponse response) throws Exception response.getWriter().write(gson.toJson(caseLinkPreview(parsedURL))); return; } - - if (parsedURL.getPath().startsWith("/people/")) { - response.getWriter().write(gson.toJson(peopleLinkPreview())); - return; - } } response.getWriter().write("{}"); @@ -116,64 +111,6 @@ JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException { } // [END add_ons_case_preview_link] - // [START add_ons_people_preview_link] - - /** - * An employee profile link preview. - * - * @return The resulting preview link card. - */ - JsonObject peopleLinkPreview() { - // Builds a preview card with an employee's name, title, email, and profile photo. - // Uses the text from the card's header for the title of the smart chip. - JsonObject cardHeader = new JsonObject(); - cardHeader.add("title", new JsonPrimitive("Rosario Cruz")); - - JsonObject image = new JsonObject(); - image.add("imageUrl", new JsonPrimitive("https://developers.google.com/workspace/add-ons/images/employee-profile.png")); - - JsonObject imageWidget = new JsonObject(); - imageWidget.add("image", image); - - JsonObject startIcon = new JsonObject(); - startIcon.add("knownIcon", new JsonPrimitive("EMAIL")); - - JsonObject decoratedText = new JsonObject(); - decoratedText.add("startIcon", startIcon); - decoratedText.add("text", new JsonPrimitive("rosario@example.com")); - decoratedText.add("bottomLabel", new JsonPrimitive("Case Manager")); - - JsonObject decoratedTextWidget = new JsonObject(); - decoratedTextWidget.add("decoratedText", decoratedText); - - JsonArray widgets = new JsonArray(); - widgets.add(imageWidget); - widgets.add(decoratedTextWidget); - - 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("Rosario Cruz")); - 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_people_preview_link] } // [END add_ons_preview_link] diff --git a/node/3p-resources/README.md b/node/3p-resources/README.md index 78a78af..13870c9 100644 --- a/node/3p-resources/README.md +++ b/node/3p-resources/README.md @@ -1,6 +1,6 @@ # Third-Party Resources -The solution is made of two Cloud Functions, one for the two link preview triggers and +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. diff --git a/node/3p-resources/deployment.json b/node/3p-resources/deployment.json index a4757a4..7a7cc83 100644 --- a/node/3p-resources/deployment.json +++ b/node/3p-resources/deployment.json @@ -33,20 +33,6 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, - { - "runFunction": "$URL1", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", - "localizedLabelText": { - "es": "Personas" - }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } ], "createActionTriggers": [ diff --git a/node/3p-resources/index.js b/node/3p-resources/index.js index 6b6b9c3..000c2ba 100644 --- a/node/3p-resources/index.js +++ b/node/3p-resources/index.js @@ -31,10 +31,6 @@ exports.createLinkPreview = (req, res) => { if (parsedUrl.pathname.startsWith('/support/cases/')) { return res.json(caseLinkPreview(parsedUrl)); } - - if (parsedUrl.pathname.startsWith('/people/')) { - return res.json(peopleLinkPreview()); - } } } }; @@ -75,49 +71,6 @@ function caseLinkPreview(url) { } // [END add_ons_case_preview_link] -// [START add_ons_people_preview_link] - -/** - * An employee profile link preview. - * - * @param {!URL} url The event object. - * @return {!Card} The resulting preview link card. - */ -function peopleLinkPreview() { - // Builds a preview card with an employee's name, title, email, and profile photo. - // Uses the text from the card's header for the title of the smart chip. - return { - action: { - linkPreview: { - title: "Rosario Cruz", - previewCard: { - header: { - title: "Rosario Cruz" - }, - sections: [{ - widgets: [ - { - image: { - imageUrl: 'https://developers.google.com/workspace/add-ons/images/employee-profile.png' - } - }, { - decoratedText: { - startIcon: { - knownIcon: "EMAIL" - }, - text: "rosario@example.com", - bottomLabel: "Case Manager" - } - } - ] - }] - } - } - } - }; -} - -// [END add_ons_people_preview_link] // [END add_ons_preview_link] // [START add_ons_3p_resources] diff --git a/python/3p-resources/README.md b/python/3p-resources/README.md index 052a0b4..734342f 100644 --- a/python/3p-resources/README.md +++ b/python/3p-resources/README.md @@ -1,6 +1,6 @@ # Third-Party Resources -The solution is made of two Cloud Functions, one for the two link preview triggers and +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. diff --git a/python/3p-resources/create_link_preview/main.py b/python/3p-resources/create_link_preview/main.py index 32bb107..2b970de 100644 --- a/python/3p-resources/create_link_preview/main.py +++ b/python/3p-resources/create_link_preview/main.py @@ -37,9 +37,6 @@ def create_link_preview(req: flask.Request): if parsed_url.path.startswith("/support/cases/"): return case_link_preview(parsed_url) - if parsed_url.path.startswith("/people/"): - return people_link_preview() - return {} @@ -80,48 +77,4 @@ def case_link_preview(url): # [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. - # Uses the text from the card's header for the title of the smart chip. - return { - "action": { - "linkPreview": { - "title": "Rosario Cruz", - "previewCard": { - "header": { - "title": "Rosario Cruz" - }, - "sections": [{ - "widgets": [ - { - "image": { - "imageUrl": "https://developers.google.com/workspace/add-ons/images/employee-profile.png" - } - }, - { - "decoratedText": { - "startIcon": { - "knownIcon": "EMAIL" - }, - "text": "rosario@example.com", - "bottomLabel": "Case Manager", - } - }, - ] - }], - } - } - } - } - - -# [END add_ons_people_preview_link] # [END add_ons_preview_link] diff --git a/python/3p-resources/deployment.json b/python/3p-resources/deployment.json index a4757a4..7a7cc83 100644 --- a/python/3p-resources/deployment.json +++ b/python/3p-resources/deployment.json @@ -33,20 +33,6 @@ "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png" - }, - { - "runFunction": "$URL1", - "patterns": [ - { - "hostPattern": "example.com", - "pathPrefix": "people" - } - ], - "labelText": "People", - "localizedLabelText": { - "es": "Personas" - }, - "logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png" } ], "createActionTriggers": [ From 85c9c43ffbf3317c864ba7ee7df979b5d4423209 Mon Sep 17 00:00:00 2001 From: pierrick Date: Wed, 24 Jan 2024 17:50:20 +0000 Subject: [PATCH 20/21] Fix copyright year --- java/3p-resources/src/main/java/CreateLinkPreview.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/3p-resources/src/main/java/CreateLinkPreview.java b/java/3p-resources/src/main/java/CreateLinkPreview.java index d981e08..e3d4372 100644 --- a/java/3p-resources/src/main/java/CreateLinkPreview.java +++ b/java/3p-resources/src/main/java/CreateLinkPreview.java @@ -1,5 +1,5 @@ /** - * Copyright 2023 Google LLC + * 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. From 70f86c7e49b587d665bfe347ace0a7f92f5a4b44 Mon Sep 17 00:00:00 2001 From: pierrick Date: Thu, 25 Jan 2024 20:34:19 +0000 Subject: [PATCH 21/21] Review fixes --- apps-script/3p-resources/3p-resources.gs | 68 +++--- .../src/main/java/Create3pResources.java | 199 +++++++++--------- 2 files changed, 138 insertions(+), 129 deletions(-) diff --git a/apps-script/3p-resources/3p-resources.gs b/apps-script/3p-resources/3p-resources.gs index 9b25f25..ea2d159 100644 --- a/apps-script/3p-resources/3p-resources.gs +++ b/apps-script/3p-resources/3p-resources.gs @@ -52,7 +52,7 @@ function caseLinkPreview(event) { * @return {!Map} A map with the extracted URL parameters. */ function parseQuery(url) { - var query = url.split("?")[1]; + const query = url.split("?")[1]; if (query) { return query.split("&") .reduce(function(o, e) { @@ -87,20 +87,20 @@ function parseQuery(url) { */ function createCaseInputCard(event, errors, isUpdate) { - const cardHeader1 = CardService.newCardHeader() + const cardHeader = CardService.newCardHeader() .setTitle('Create a support case') - const cardSection1TextInput1 = CardService.newTextInput() + const cardSectionTextInput1 = CardService.newTextInput() .setFieldName('name') .setTitle('Name') .setMultiline(false); - const cardSection1TextInput2 = CardService.newTextInput() + const cardSectionTextInput2 = CardService.newTextInput() .setFieldName('description') .setTitle('Description') .setMultiline(true); - const cardSection1SelectionInput1 = CardService.newSelectionInput() + const cardSectionSelectionInput1 = CardService.newSelectionInput() .setFieldName('priority') .setTitle('Priority') .setType(CardService.SelectionInputType.DROPDOWN) @@ -109,49 +109,49 @@ function createCaseInputCard(event, errors, isUpdate) { .addItem('P2', 'P2', false) .addItem('P3', 'P3', false); - const cardSection1SelectionInput2 = CardService.newSelectionInput() + 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 cardSection1ButtonList1Button1Action1 = CardService.newAction() + const cardSectionButtonListButtonAction = CardService.newAction() .setPersistValues(true) .setFunctionName('submitCaseCreationForm') .setParameters({}); - const cardSection1ButtonList1Button1 = CardService.newTextButton() + const cardSectionButtonListButton = CardService.newTextButton() .setText('Create') .setTextButtonStyle(CardService.TextButtonStyle.TEXT) - .setOnClickAction(cardSection1ButtonList1Button1Action1); + .setOnClickAction(cardSectionButtonListButtonAction); - const cardSection1ButtonList1 = CardService.newButtonSet() - .addButton(cardSection1ButtonList1Button1); + const cardSectionButtonList = CardService.newButtonSet() + .addButton(cardSectionButtonListButton); // Builds the form inputs with error texts for invalid values. - const cardSection1 = CardService.newCardSection(); + const cardSection = CardService.newCardSection(); if (errors?.name) { - cardSection1.addWidget(createErrorTextParagraph(errors.name)); + cardSection.addWidget(createErrorTextParagraph(errors.name)); } - cardSection1.addWidget(cardSection1TextInput1); + cardSection.addWidget(cardSectionTextInput1); if (errors?.description) { - cardSection1.addWidget(createErrorTextParagraph(errors.description)); + cardSection.addWidget(createErrorTextParagraph(errors.description)); } - cardSection1.addWidget(cardSection1TextInput2); + cardSection.addWidget(cardSectionTextInput2); if (errors?.priority) { - cardSection1.addWidget(createErrorTextParagraph(errors.priority)); + cardSection.addWidget(createErrorTextParagraph(errors.priority)); } - cardSection1.addWidget(cardSection1SelectionInput1); + cardSection.addWidget(cardSectionSelectionInput1); if (errors?.impact) { - cardSection1.addWidget(createErrorTextParagraph(errors.impact)); + cardSection.addWidget(createErrorTextParagraph(errors.impact)); } - cardSection1.addWidget(cardSection1SelectionInput2); - cardSection1.addWidget(cardSection1ButtonList1); + cardSection.addWidget(cardSectionSelectionInput2); + cardSection.addWidget(cardSectionButtonList); const card = CardService.newCardBuilder() - .setHeader(cardHeader1) - .addSection(cardSection1) + .setHeader(cardHeader) + .addSection(cardSection) .build(); if (isUpdate) { @@ -188,11 +188,23 @@ function submitCaseCreationForm(event) { } else { const title = `Case ${caseDetails.name}`; // Adds the case details as parameters to the generated link URL. - const url = 'https://example.com/support/cases/?' + Object.entries(caseDetails).flatMap(([k, v]) => Array.isArray(v) ? v.map(e => `${k}=${encodeURIComponent(e)}`) : `${k}=${encodeURIComponent(v)}`).join("&"); + 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] @@ -205,16 +217,16 @@ function submitCaseCreationForm(event) { */ function validateFormInputs(caseDetails) { const errors = {}; - if (caseDetails.name === undefined) { + if (!caseDetails.name) { errors.name = 'You must provide a name'; } - if (caseDetails.description === undefined) { + if (!caseDetails.description) { errors.description = 'You must provide a description'; } - if (caseDetails.priority === undefined) { + if (!caseDetails.priority) { errors.priority = 'You must provide a priority'; } - if (caseDetails.impact && !(['P0', 'P1']).includes(caseDetails.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'; } diff --git a/java/3p-resources/src/main/java/Create3pResources.java b/java/3p-resources/src/main/java/Create3pResources.java index a17c7a9..2fc05b2 100644 --- a/java/3p-resources/src/main/java/Create3pResources.java +++ b/java/3p-resources/src/main/java/Create3pResources.java @@ -44,10 +44,8 @@ public void service(HttpRequest request, HttpResponse response) throws Exception JsonObject parameters = event.getAsJsonObject("commonEventObject").getAsJsonObject("parameters"); if (parameters != null && parameters.has("submitCaseCreationForm") && parameters.get("submitCaseCreationForm").getAsBoolean()) { response.getWriter().write(gson.toJson(submitCaseCreationForm(event))); - return; } else { response.getWriter().write(gson.toJson(createCaseInputCard(event, new HashMap(), false))); - return; } } @@ -62,128 +60,127 @@ public void service(HttpRequest request, HttpResponse response) throws Exception * @return The resulting card or action response. */ JsonObject createCaseInputCard(JsonObject event, Map errors, boolean isUpdate) { - - JsonObject cardHeader1 = new JsonObject(); - cardHeader1.add("title", new JsonPrimitive("Create a support case")); - - JsonObject cardSection1TextInput1 = new JsonObject(); - cardSection1TextInput1.add("name", new JsonPrimitive("name")); - cardSection1TextInput1.add("label", new JsonPrimitive("Name")); - - JsonObject cardSection1TextInput1Widget = new JsonObject(); - cardSection1TextInput1Widget.add("textInput", cardSection1TextInput1); - - JsonObject cardSection1TextInput2 = new JsonObject(); - cardSection1TextInput2.add("name", new JsonPrimitive("description")); - cardSection1TextInput2.add("label", new JsonPrimitive("Description")); - cardSection1TextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE")); - - JsonObject cardSection1TextInput2Widget = new JsonObject(); - cardSection1TextInput2Widget.add("textInput", cardSection1TextInput2); - - JsonObject cardSection1SelectionInput1ItemsItem1 = new JsonObject(); - cardSection1SelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0")); - cardSection1SelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0")); - - JsonObject cardSection1SelectionInput1ItemsItem2 = new JsonObject(); - cardSection1SelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1")); - cardSection1SelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1")); - - JsonObject cardSection1SelectionInput1ItemsItem3 = new JsonObject(); - cardSection1SelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2")); - cardSection1SelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2")); - - JsonObject cardSection1SelectionInput1ItemsItem4 = new JsonObject(); - cardSection1SelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3")); - cardSection1SelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3")); - - JsonArray cardSection1SelectionInput1Items = new JsonArray(); - cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem1); - cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem2); - cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem3); - cardSection1SelectionInput1Items.add(cardSection1SelectionInput1ItemsItem4); - - JsonObject cardSection1SelectionInput1 = new JsonObject(); - cardSection1SelectionInput1.add("name", new JsonPrimitive("priority")); - cardSection1SelectionInput1.add("label", new JsonPrimitive("Priority")); - cardSection1SelectionInput1.add("type", new JsonPrimitive("DROPDOWN")); - cardSection1SelectionInput1.add("items", cardSection1SelectionInput1Items); - - JsonObject cardSection1SelectionInput1Widget = new JsonObject(); - cardSection1SelectionInput1Widget.add("selectionInput", cardSection1SelectionInput1); - - JsonObject cardSection1SelectionInput2ItemsItem1 = new JsonObject(); - cardSection1SelectionInput2ItemsItem1.add("text", new JsonPrimitive("Blocks a critical customer operation")); - cardSection1SelectionInput2ItemsItem1.add("value", new JsonPrimitive("Blocks a critical customer operation")); + 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 cardSection1SelectionInput2Items = new JsonArray(); - cardSection1SelectionInput2Items.add(cardSection1SelectionInput2ItemsItem1); + JsonArray cardSectionSelectionInput2Items = new JsonArray(); + cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem); - JsonObject cardSection1SelectionInput2 = new JsonObject(); - cardSection1SelectionInput2.add("name", new JsonPrimitive("impact")); - cardSection1SelectionInput2.add("label", new JsonPrimitive("Impact")); - cardSection1SelectionInput2.add("items", cardSection1SelectionInput2Items); + JsonObject cardSectionSelectionInput2 = new JsonObject(); + cardSectionSelectionInput2.add("name", new JsonPrimitive("impact")); + cardSectionSelectionInput2.add("label", new JsonPrimitive("Impact")); + cardSectionSelectionInput2.add("items", cardSectionSelectionInput2Items); - JsonObject cardSection1SelectionInput2Widget = new JsonObject(); - cardSection1SelectionInput2Widget.add("selectionInput", cardSection1SelectionInput2); + JsonObject cardSectionSelectionInput2Widget = new JsonObject(); + cardSectionSelectionInput2Widget.add("selectionInput", cardSectionSelectionInput2); - JsonObject cardSection1ButtonList1Button1Action1ParametersParameter1 = new JsonObject(); - cardSection1ButtonList1Button1Action1ParametersParameter1.add("key", new JsonPrimitive("submitCaseCreationForm")); - cardSection1ButtonList1Button1Action1ParametersParameter1.add("value", new JsonPrimitive(true)); + JsonObject cardSectionButtonListButtonActionParametersParameter = new JsonObject(); + cardSectionButtonListButtonActionParametersParameter.add("key", new JsonPrimitive("submitCaseCreationForm")); + cardSectionButtonListButtonActionParametersParameter.add("value", new JsonPrimitive(true)); - JsonArray cardSection1ButtonList1Button1Action1Parameters = new JsonArray(); - cardSection1ButtonList1Button1Action1Parameters.add(cardSection1ButtonList1Button1Action1ParametersParameter1); + JsonArray cardSectionButtonListButtonActionParameters = new JsonArray(); + cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter); - JsonObject cardSection1ButtonList1Button1Action1 = new JsonObject(); - cardSection1ButtonList1Button1Action1.add("function", new JsonPrimitive(System.getenv().get("URL"))); - cardSection1ButtonList1Button1Action1.add("parameters", cardSection1ButtonList1Button1Action1Parameters); - cardSection1ButtonList1Button1Action1.add("persistValues", new JsonPrimitive(true)); + JsonObject cardSectionButtonListButtonAction = new JsonObject(); + cardSectionButtonListButtonAction.add("function", new JsonPrimitive(System.getenv().get("URL"))); + cardSectionButtonListButtonAction.add("parameters", cardSectionButtonListButtonActionParameters); + cardSectionButtonListButtonAction.add("persistValues", new JsonPrimitive(true)); - JsonObject cardSection1ButtonList1Button1OnCLick = new JsonObject(); - cardSection1ButtonList1Button1OnCLick.add("action", cardSection1ButtonList1Button1Action1); + JsonObject cardSectionButtonListButtonOnCLick = new JsonObject(); + cardSectionButtonListButtonOnCLick.add("action", cardSectionButtonListButtonAction); - JsonObject cardSection1ButtonList1Button1 = new JsonObject(); - cardSection1ButtonList1Button1.add("text", new JsonPrimitive("Create")); - cardSection1ButtonList1Button1.add("onClick", cardSection1ButtonList1Button1OnCLick); + JsonObject cardSectionButtonListButton = new JsonObject(); + cardSectionButtonListButton.add("text", new JsonPrimitive("Create")); + cardSectionButtonListButton.add("onClick", cardSectionButtonListButtonOnCLick); - JsonArray cardSection1ButtonList1Buttons = new JsonArray(); - cardSection1ButtonList1Buttons.add(cardSection1ButtonList1Button1); + JsonArray cardSectionButtonListButtons = new JsonArray(); + cardSectionButtonListButtons.add(cardSectionButtonListButton); - JsonObject cardSection1ButtonList1 = new JsonObject(); - cardSection1ButtonList1.add("buttons", cardSection1ButtonList1Buttons); + JsonObject cardSectionButtonList = new JsonObject(); + cardSectionButtonList.add("buttons", cardSectionButtonListButtons); - JsonObject cardSection1ButtonList1Widget = new JsonObject(); - cardSection1ButtonList1Widget.add("buttonList", cardSection1ButtonList1); + JsonObject cardSectionButtonListWidget = new JsonObject(); + cardSectionButtonListWidget.add("buttonList", cardSectionButtonList); // Builds the form inputs with error texts for invalid values. - JsonArray cardSection1 = new JsonArray(); + JsonArray cardSection = new JsonArray(); if (errors.containsKey("name")) { - cardSection1.add(createErrorTextParagraph(errors.get("name").toString())); + cardSection.add(createErrorTextParagraph(errors.get("name").toString())); } - cardSection1.add(cardSection1TextInput1Widget); + cardSection.add(cardSectionTextInput1Widget); if (errors.containsKey("description")) { - cardSection1.add(createErrorTextParagraph(errors.get("description").toString())); + cardSection.add(createErrorTextParagraph(errors.get("description").toString())); } - cardSection1.add(cardSection1TextInput2Widget); + cardSection.add(cardSectionTextInput2Widget); if (errors.containsKey("priority")) { - cardSection1.add(createErrorTextParagraph(errors.get("priority").toString())); + cardSection.add(createErrorTextParagraph(errors.get("priority").toString())); } - cardSection1.add(cardSection1SelectionInput1Widget); + cardSection.add(cardSectionSelectionInput1Widget); if (errors.containsKey("impact")) { - cardSection1.add(createErrorTextParagraph(errors.get("impact").toString())); + cardSection.add(createErrorTextParagraph(errors.get("impact").toString())); } - cardSection1.add(cardSection1SelectionInput2Widget); - cardSection1.add(cardSection1ButtonList1Widget); + cardSection.add(cardSectionSelectionInput2Widget); + cardSection.add(cardSectionButtonListWidget); - JsonObject cardSection1Widgets = new JsonObject(); - cardSection1Widgets.add("widgets", cardSection1); + JsonObject cardSectionWidgets = new JsonObject(); + cardSectionWidgets.add("widgets", cardSection); JsonArray sections = new JsonArray(); - sections.add(cardSection1Widgets); + sections.add(cardSectionWidgets); JsonObject card = new JsonObject(); - card.add("header", cardHeader1); + card.add("header", cardHeader); card.add("sections", sections); JsonObject navigation = new JsonObject(); @@ -310,12 +307,12 @@ JsonObject createErrorTextParagraph(String errorMessage) { * @return The resulting submit form response. */ JsonObject createLinkRenderAction(String title, String url) { - JsonObject link1 = new JsonObject(); - link1.add("title", new JsonPrimitive(title)); - link1.add("url", new JsonPrimitive(url)); + JsonObject link = new JsonObject(); + link.add("title", new JsonPrimitive(title)); + link.add("url", new JsonPrimitive(url)); JsonArray links = new JsonArray(); - links.add(link1); + links.add(link); JsonObject action = new JsonObject(); action.add("links", links);