Skip to content

Commit

Permalink
feat: Upgrade preview-link code sample to 3p-resources (#186)
Browse files Browse the repository at this point in the history
* Upgrade preview-link code sample for Apps Script to include create 3P resources

* Fix preview link incompatability with 3p resource creation

* Nit documentation edits

* Upgrade preview-link code sample for Node to include create 3P resources

* Fixed small typos in title texts

* Upgrade and fix preview-link code sample for Python to enable create 3P resources

* Implemented 3P resources creation function for Python

* Upgrade preview-link code sample for Java and initatie create 3P resources

* Fix preview-link code sample for Java and implement create 3P resources

* Upgraded Java impl of 3p-resources to use JsonObjects

* Upgrade from json encoded string to URL params in Java

* Upgrade from json envoded string to URL params in Node and migrated to Node 20

* Upgrade from json envoded string to URL params in Python

* Upgrade from json envoded string to URL params in Apps Script

* Remove .vscode files from change

* Reviewed Apps Script comments

* Reviewed all sources specific to languages

* Reviewed Python, NodeJS and Java comments

* Remove people preview link implementation

* Fix copyright year

* Review fixes

---------

Co-authored-by: pierrick <[email protected]>
  • Loading branch information
PierrickVoulet and pierrick authored Jan 25, 2024
1 parent 17bfd65 commit ece111d
Show file tree
Hide file tree
Showing 28 changed files with 1,719 additions and 731 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode/
271 changes: 271 additions & 0 deletions apps-script/3p-resources/3p-resources.gs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// [START add_ons_preview_link]
// [START add_ons_case_preview_link]

/**
* Entry point for a support case link preview.
*
* @param {!Object} event The event object.
* @return {!Card} The resulting preview link card.
*/
function caseLinkPreview(event) {

// If the event object URL matches a specified pattern for support case links.
if (event.docs.matchedUrl.url) {

// Uses the event object to parse the URL and identify the case details.
const caseDetails = parseQuery(event.docs.matchedUrl.url);

// Builds a preview card with the case name, and description
const caseHeader = CardService.newCardHeader()
.setTitle(`Case ${caseDetails["name"][0]}`);
const caseDescription = CardService.newTextParagraph()
.setText(caseDetails["description"][0]);

// Returns the card.
// Uses the text from the card's header for the title of the smart chip.
return CardService.newCardBuilder()
.setHeader(caseHeader)
.addSection(CardService.newCardSection().addWidget(caseDescription))
.build();
}
}

/**
* Extracts the URL parameters from the given URL.
*
* @param {!string} url The URL to parse.
* @return {!Map} A map with the extracted URL parameters.
*/
function parseQuery(url) {
const query = url.split("?")[1];
if (query) {
return query.split("&")
.reduce(function(o, e) {
var temp = e.split("=");
var key = temp[0].trim();
var value = temp[1].trim();
value = isNaN(value) ? value : Number(value);
if (o[key]) {
o[key].push(value);
} else {
o[key] = [value];
}
return o;
}, {});
}
return null;
}

// [END add_ons_case_preview_link]
// [END add_ons_preview_link]

// [START add_ons_3p_resources]
// [START add_ons_3p_resources_create_case_card]

/**
* Produces a support case creation form card.
*
* @param {!Object} event The event object.
* @param {!Object=} errors An optional map of per-field error messages.
* @param {boolean} isUpdate Whether to return the form as an update card navigation.
* @return {!Card|!ActionResponse} The resulting card or action response.
*/
function createCaseInputCard(event, errors, isUpdate) {

const cardHeader = CardService.newCardHeader()
.setTitle('Create a support case')

const cardSectionTextInput1 = CardService.newTextInput()
.setFieldName('name')
.setTitle('Name')
.setMultiline(false);

const cardSectionTextInput2 = CardService.newTextInput()
.setFieldName('description')
.setTitle('Description')
.setMultiline(true);

const cardSectionSelectionInput1 = CardService.newSelectionInput()
.setFieldName('priority')
.setTitle('Priority')
.setType(CardService.SelectionInputType.DROPDOWN)
.addItem('P0', 'P0', false)
.addItem('P1', 'P1', false)
.addItem('P2', 'P2', false)
.addItem('P3', 'P3', false);

const cardSectionSelectionInput2 = CardService.newSelectionInput()
.setFieldName('impact')
.setTitle('Impact')
.setType(CardService.SelectionInputType.CHECK_BOX)
.addItem('Blocks a critical customer operation', 'Blocks a critical customer operation', false);

const cardSectionButtonListButtonAction = CardService.newAction()
.setPersistValues(true)
.setFunctionName('submitCaseCreationForm')
.setParameters({});

const cardSectionButtonListButton = CardService.newTextButton()
.setText('Create')
.setTextButtonStyle(CardService.TextButtonStyle.TEXT)
.setOnClickAction(cardSectionButtonListButtonAction);

const cardSectionButtonList = CardService.newButtonSet()
.addButton(cardSectionButtonListButton);

// Builds the form inputs with error texts for invalid values.
const cardSection = CardService.newCardSection();
if (errors?.name) {
cardSection.addWidget(createErrorTextParagraph(errors.name));
}
cardSection.addWidget(cardSectionTextInput1);
if (errors?.description) {
cardSection.addWidget(createErrorTextParagraph(errors.description));
}
cardSection.addWidget(cardSectionTextInput2);
if (errors?.priority) {
cardSection.addWidget(createErrorTextParagraph(errors.priority));
}
cardSection.addWidget(cardSectionSelectionInput1);
if (errors?.impact) {
cardSection.addWidget(createErrorTextParagraph(errors.impact));
}

cardSection.addWidget(cardSectionSelectionInput2);
cardSection.addWidget(cardSectionButtonList);

const card = CardService.newCardBuilder()
.setHeader(cardHeader)
.addSection(cardSection)
.build();

if (isUpdate) {
return CardService.newActionResponseBuilder()
.setNavigation(CardService.newNavigation().updateCard(card))
.build();
} else {
return card;
}
}

// [END add_ons_3p_resources_create_case_card]
// [START add_ons_3p_resources_submit_create_case]

/**
* Submits the creation form. If valid, returns a render action
* that inserts a new link into the document. If invalid, returns an
* update card navigation that re-renders the creation form with error messages.
*
* @param {!Object} event The event object with form input values.
* @return {!ActionResponse|!SubmitFormResponse} The resulting response.
*/
function submitCaseCreationForm(event) {
const caseDetails = {
name: event.formInput.name,
description: event.formInput.description,
priority: event.formInput.priority,
impact: !!event.formInput.impact,
};

const errors = validateFormInputs(caseDetails);
if (Object.keys(errors).length > 0) {
return createCaseInputCard(event, errors, /* isUpdate= */ true);
} else {
const title = `Case ${caseDetails.name}`;
// Adds the case details as parameters to the generated link URL.
const url = 'https://example.com/support/cases/?' + generateQuery(caseDetails);
return createLinkRenderAction(title, url);
}
}

/**
* Build a query path with URL parameters.
*
* @param {!Map} parameters A map with the URL parameters.
* @return {!string} The resulting query path.
*/
function generateQuery(parameters) {
return Object.entries(parameters).flatMap(([k, v]) =>
Array.isArray(v) ? v.map(e => `${k}=${encodeURIComponent(e)}`) : `${k}=${encodeURIComponent(v)}`
).join("&");
}

// [END add_ons_3p_resources_submit_create_case]
// [START add_ons_3p_resources_validate_inputs]

/**
* Validates case creation form input values.
*
* @param {!Object} caseDetails The values of each form input submitted by the user.
* @return {!Object} A map from field name to error message. An empty object
* represents a valid form submission.
*/
function validateFormInputs(caseDetails) {
const errors = {};
if (!caseDetails.name) {
errors.name = 'You must provide a name';
}
if (!caseDetails.description) {
errors.description = 'You must provide a description';
}
if (!caseDetails.priority) {
errors.priority = 'You must provide a priority';
}
if (caseDetails.impact && caseDetails.priority !== 'P0' && caseDetails.priority !== 'P1') {
errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1';
}

return errors;
}

/**
* Returns a text paragraph with red text indicating a form field validation error.
*
* @param {string} errorMessage A description of input value error.
* @return {!TextParagraph} The resulting text paragraph.
*/
function createErrorTextParagraph(errorMessage) {
return CardService.newTextParagraph()
.setText('<font color=\"#BA0300\"><b>Error:</b> ' + errorMessage + '</font>');
}

// [END add_ons_3p_resources_validate_inputs]
// [START add_ons_3p_resources_link_render_action]

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

// [END add_ons_3p_resources_link_render_action]
// [END add_ons_3p_resources]
11 changes: 11 additions & 0 deletions apps-script/3p-resources/README.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand All @@ -35,20 +36,17 @@
"es": "Caso de soporte"
},
"logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
},
}
],
"createActionTriggers": [
{
"runFunction": "peopleLinkPreview",
"patterns": [
{
"hostPattern": "example.com",
"pathPrefix": "people"
}
],
"labelText": "People",
"id": "createCase",
"labelText": "Create support case",
"localizedLabelText": {
"es": "Personas"
"es": "Crear caso de soporte"
},
"logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png"
"runFunction": "createCaseInputCard",
"logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
}
]
}
Expand Down
4 changes: 0 additions & 4 deletions apps-script/preview-links/README.md

This file was deleted.

Loading

0 comments on commit ece111d

Please sign in to comment.