Skip to content

Commit ece111d

Browse files
PierrickVouletpierrick
and
pierrick
authored
feat: Upgrade preview-link code sample to 3p-resources (#186)
* Upgrade preview-link code sample for Apps Script to include create 3P resources * Fix preview link incompatability with 3p resource creation * Nit documentation edits * Upgrade preview-link code sample for Node to include create 3P resources * Fixed small typos in title texts * Upgrade and fix preview-link code sample for Python to enable create 3P resources * Implemented 3P resources creation function for Python * Upgrade preview-link code sample for Java and initatie create 3P resources * Fix preview-link code sample for Java and implement create 3P resources * Upgraded Java impl of 3p-resources to use JsonObjects * Upgrade from json encoded string to URL params in Java * Upgrade from json envoded string to URL params in Node and migrated to Node 20 * Upgrade from json envoded string to URL params in Python * Upgrade from json envoded string to URL params in Apps Script * Remove .vscode files from change * Reviewed Apps Script comments * Reviewed all sources specific to languages * Reviewed Python, NodeJS and Java comments * Remove people preview link implementation * Fix copyright year * Review fixes --------- Co-authored-by: pierrick <pierrick@google.com>
1 parent 17bfd65 commit ece111d

File tree

28 files changed

+1719
-731
lines changed

28 files changed

+1719
-731
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vscode/
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* Copyright 2024 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
// [START add_ons_preview_link]
17+
// [START add_ons_case_preview_link]
18+
19+
/**
20+
* Entry point for a support case link preview.
21+
*
22+
* @param {!Object} event The event object.
23+
* @return {!Card} The resulting preview link card.
24+
*/
25+
function caseLinkPreview(event) {
26+
27+
// If the event object URL matches a specified pattern for support case links.
28+
if (event.docs.matchedUrl.url) {
29+
30+
// Uses the event object to parse the URL and identify the case details.
31+
const caseDetails = parseQuery(event.docs.matchedUrl.url);
32+
33+
// Builds a preview card with the case name, and description
34+
const caseHeader = CardService.newCardHeader()
35+
.setTitle(`Case ${caseDetails["name"][0]}`);
36+
const caseDescription = CardService.newTextParagraph()
37+
.setText(caseDetails["description"][0]);
38+
39+
// Returns the card.
40+
// Uses the text from the card's header for the title of the smart chip.
41+
return CardService.newCardBuilder()
42+
.setHeader(caseHeader)
43+
.addSection(CardService.newCardSection().addWidget(caseDescription))
44+
.build();
45+
}
46+
}
47+
48+
/**
49+
* Extracts the URL parameters from the given URL.
50+
*
51+
* @param {!string} url The URL to parse.
52+
* @return {!Map} A map with the extracted URL parameters.
53+
*/
54+
function parseQuery(url) {
55+
const query = url.split("?")[1];
56+
if (query) {
57+
return query.split("&")
58+
.reduce(function(o, e) {
59+
var temp = e.split("=");
60+
var key = temp[0].trim();
61+
var value = temp[1].trim();
62+
value = isNaN(value) ? value : Number(value);
63+
if (o[key]) {
64+
o[key].push(value);
65+
} else {
66+
o[key] = [value];
67+
}
68+
return o;
69+
}, {});
70+
}
71+
return null;
72+
}
73+
74+
// [END add_ons_case_preview_link]
75+
// [END add_ons_preview_link]
76+
77+
// [START add_ons_3p_resources]
78+
// [START add_ons_3p_resources_create_case_card]
79+
80+
/**
81+
* Produces a support case creation form card.
82+
*
83+
* @param {!Object} event The event object.
84+
* @param {!Object=} errors An optional map of per-field error messages.
85+
* @param {boolean} isUpdate Whether to return the form as an update card navigation.
86+
* @return {!Card|!ActionResponse} The resulting card or action response.
87+
*/
88+
function createCaseInputCard(event, errors, isUpdate) {
89+
90+
const cardHeader = CardService.newCardHeader()
91+
.setTitle('Create a support case')
92+
93+
const cardSectionTextInput1 = CardService.newTextInput()
94+
.setFieldName('name')
95+
.setTitle('Name')
96+
.setMultiline(false);
97+
98+
const cardSectionTextInput2 = CardService.newTextInput()
99+
.setFieldName('description')
100+
.setTitle('Description')
101+
.setMultiline(true);
102+
103+
const cardSectionSelectionInput1 = CardService.newSelectionInput()
104+
.setFieldName('priority')
105+
.setTitle('Priority')
106+
.setType(CardService.SelectionInputType.DROPDOWN)
107+
.addItem('P0', 'P0', false)
108+
.addItem('P1', 'P1', false)
109+
.addItem('P2', 'P2', false)
110+
.addItem('P3', 'P3', false);
111+
112+
const cardSectionSelectionInput2 = CardService.newSelectionInput()
113+
.setFieldName('impact')
114+
.setTitle('Impact')
115+
.setType(CardService.SelectionInputType.CHECK_BOX)
116+
.addItem('Blocks a critical customer operation', 'Blocks a critical customer operation', false);
117+
118+
const cardSectionButtonListButtonAction = CardService.newAction()
119+
.setPersistValues(true)
120+
.setFunctionName('submitCaseCreationForm')
121+
.setParameters({});
122+
123+
const cardSectionButtonListButton = CardService.newTextButton()
124+
.setText('Create')
125+
.setTextButtonStyle(CardService.TextButtonStyle.TEXT)
126+
.setOnClickAction(cardSectionButtonListButtonAction);
127+
128+
const cardSectionButtonList = CardService.newButtonSet()
129+
.addButton(cardSectionButtonListButton);
130+
131+
// Builds the form inputs with error texts for invalid values.
132+
const cardSection = CardService.newCardSection();
133+
if (errors?.name) {
134+
cardSection.addWidget(createErrorTextParagraph(errors.name));
135+
}
136+
cardSection.addWidget(cardSectionTextInput1);
137+
if (errors?.description) {
138+
cardSection.addWidget(createErrorTextParagraph(errors.description));
139+
}
140+
cardSection.addWidget(cardSectionTextInput2);
141+
if (errors?.priority) {
142+
cardSection.addWidget(createErrorTextParagraph(errors.priority));
143+
}
144+
cardSection.addWidget(cardSectionSelectionInput1);
145+
if (errors?.impact) {
146+
cardSection.addWidget(createErrorTextParagraph(errors.impact));
147+
}
148+
149+
cardSection.addWidget(cardSectionSelectionInput2);
150+
cardSection.addWidget(cardSectionButtonList);
151+
152+
const card = CardService.newCardBuilder()
153+
.setHeader(cardHeader)
154+
.addSection(cardSection)
155+
.build();
156+
157+
if (isUpdate) {
158+
return CardService.newActionResponseBuilder()
159+
.setNavigation(CardService.newNavigation().updateCard(card))
160+
.build();
161+
} else {
162+
return card;
163+
}
164+
}
165+
166+
// [END add_ons_3p_resources_create_case_card]
167+
// [START add_ons_3p_resources_submit_create_case]
168+
169+
/**
170+
* Submits the creation form. If valid, returns a render action
171+
* that inserts a new link into the document. If invalid, returns an
172+
* update card navigation that re-renders the creation form with error messages.
173+
*
174+
* @param {!Object} event The event object with form input values.
175+
* @return {!ActionResponse|!SubmitFormResponse} The resulting response.
176+
*/
177+
function submitCaseCreationForm(event) {
178+
const caseDetails = {
179+
name: event.formInput.name,
180+
description: event.formInput.description,
181+
priority: event.formInput.priority,
182+
impact: !!event.formInput.impact,
183+
};
184+
185+
const errors = validateFormInputs(caseDetails);
186+
if (Object.keys(errors).length > 0) {
187+
return createCaseInputCard(event, errors, /* isUpdate= */ true);
188+
} else {
189+
const title = `Case ${caseDetails.name}`;
190+
// Adds the case details as parameters to the generated link URL.
191+
const url = 'https://example.com/support/cases/?' + generateQuery(caseDetails);
192+
return createLinkRenderAction(title, url);
193+
}
194+
}
195+
196+
/**
197+
* Build a query path with URL parameters.
198+
*
199+
* @param {!Map} parameters A map with the URL parameters.
200+
* @return {!string} The resulting query path.
201+
*/
202+
function generateQuery(parameters) {
203+
return Object.entries(parameters).flatMap(([k, v]) =>
204+
Array.isArray(v) ? v.map(e => `${k}=${encodeURIComponent(e)}`) : `${k}=${encodeURIComponent(v)}`
205+
).join("&");
206+
}
207+
208+
// [END add_ons_3p_resources_submit_create_case]
209+
// [START add_ons_3p_resources_validate_inputs]
210+
211+
/**
212+
* Validates case creation form input values.
213+
*
214+
* @param {!Object} caseDetails The values of each form input submitted by the user.
215+
* @return {!Object} A map from field name to error message. An empty object
216+
* represents a valid form submission.
217+
*/
218+
function validateFormInputs(caseDetails) {
219+
const errors = {};
220+
if (!caseDetails.name) {
221+
errors.name = 'You must provide a name';
222+
}
223+
if (!caseDetails.description) {
224+
errors.description = 'You must provide a description';
225+
}
226+
if (!caseDetails.priority) {
227+
errors.priority = 'You must provide a priority';
228+
}
229+
if (caseDetails.impact && caseDetails.priority !== 'P0' && caseDetails.priority !== 'P1') {
230+
errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1';
231+
}
232+
233+
return errors;
234+
}
235+
236+
/**
237+
* Returns a text paragraph with red text indicating a form field validation error.
238+
*
239+
* @param {string} errorMessage A description of input value error.
240+
* @return {!TextParagraph} The resulting text paragraph.
241+
*/
242+
function createErrorTextParagraph(errorMessage) {
243+
return CardService.newTextParagraph()
244+
.setText('<font color=\"#BA0300\"><b>Error:</b> ' + errorMessage + '</font>');
245+
}
246+
247+
// [END add_ons_3p_resources_validate_inputs]
248+
// [START add_ons_3p_resources_link_render_action]
249+
250+
/**
251+
* Returns a submit form response that inserts a link into the document.
252+
*
253+
* @param {string} title The title of the link to insert.
254+
* @param {string} url The URL of the link to insert.
255+
* @return {!SubmitFormResponse} The resulting submit form response.
256+
*/
257+
function createLinkRenderAction(title, url) {
258+
return {
259+
renderActions: {
260+
action: {
261+
links: [{
262+
title: title,
263+
url: url
264+
}]
265+
}
266+
}
267+
};
268+
}
269+
270+
// [END add_ons_3p_resources_link_render_action]
271+
// [END add_ons_3p_resources]

apps-script/3p-resources/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Third-Party Resources
2+
3+
## Preview Links with Smart Chips
4+
5+
For more information on preview link with Smart Chips, please read the
6+
[guide](https://developers.google.com/apps-script/add-ons/editors/gsao/preview-links).
7+
8+
## Create Third-Party Resources from the @ Menu
9+
10+
For more information on creating third-party resources from the @ menu, please read the
11+
[guide](https://developers.devsite.corp.google.com/workspace/add-ons/guides/create-insert-resource-smart-chip).

apps-script/preview-links/appsscript.json renamed to apps-script/3p-resources/appsscript.json

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
"exceptionLogging": "STACKDRIVER",
44
"runtimeVersion": "V8",
55
"oauthScopes": [
6-
"https://www.googleapis.com/auth/workspace.linkpreview"
6+
"https://www.googleapis.com/auth/workspace.linkpreview",
7+
"https://www.googleapis.com/auth/workspace.linkcreate"
78
],
89
"addOns": {
910
"common": {
10-
"name": "Preview support cases",
11-
"logoUrl": "https://developers.google.com/workspace/add-ons/images/link-icon.png",
11+
"name": "Manage support cases",
12+
"logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png",
1213
"layoutProperties": {
1314
"primaryColor": "#dd4b39"
1415
}
@@ -35,20 +36,17 @@
3536
"es": "Caso de soporte"
3637
},
3738
"logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
38-
},
39+
}
40+
],
41+
"createActionTriggers": [
3942
{
40-
"runFunction": "peopleLinkPreview",
41-
"patterns": [
42-
{
43-
"hostPattern": "example.com",
44-
"pathPrefix": "people"
45-
}
46-
],
47-
"labelText": "People",
43+
"id": "createCase",
44+
"labelText": "Create support case",
4845
"localizedLabelText": {
49-
"es": "Personas"
46+
"es": "Crear caso de soporte"
5047
},
51-
"logoUrl": "https://developers.google.com/workspace/add-ons/images/person-icon.png"
48+
"runFunction": "createCaseInputCard",
49+
"logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
5250
}
5351
]
5452
}

apps-script/preview-links/README.md

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)