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]