Halaman ini menjelaskan cara mem-build add-on Google Workspace yang memungkinkan pengguna Google Dokumen membuat referensi, seperti kasus dukungan atau tugas project, di layanan pihak ketiga dari dalam Google Dokumen.
Dengan add-on Google Workspace, Anda dapat menambahkan layanan ke menu @ di Dokumen. Add-on ini menambahkan item menu yang memungkinkan pengguna membuat resource di layanan Anda melalui dialog formulir di Dokumen.
Cara pengguna membuat resource
Untuk membuat resource di layanan Anda dari dalam dokumen Google Dokumen, pengguna
mengetik @
dalam dokumen dan memilih layanan Anda dari menu @:
Saat pengguna mengetik @
dalam dokumen dan memilih layanan Anda, Anda akan menampilkan
kartu yang menyertakan input formulir yang diperlukan pengguna untuk membuat
resource. Setelah pengguna mengirimkan formulir pembuatan resource, add-on Anda harus membuat resource di layanan dan membuat URL yang mengarah ke resource tersebut.
Add-on menyisipkan chip ke dalam dokumen untuk resource yang dibuat. Saat pengguna mengarahkan kursor ke chip ini, tindakan tersebut akan memanggil pemicu pratinjau link terkait add-on. Pastikan add-on Anda menyisipkan chip dengan pola link yang didukung oleh pemicu pratinjau link Anda.
Prasyarat
Apps Script
- Add-on Google Workspace yang mendukung pratinjau link untuk pola link referensi yang dibuat pengguna. Untuk mem-build add-on dengan pratinjau link, lihat Melihat pratinjau link dengan smart chip.
Node.js
- Add-on Google Workspace yang mendukung pratinjau link untuk pola link referensi yang dibuat pengguna. Untuk mem-build add-on dengan pratinjau link, lihat Melihat pratinjau link dengan smart chip.
Python
- Add-on Google Workspace yang mendukung pratinjau link untuk pola link referensi yang dibuat pengguna. Untuk mem-build add-on dengan pratinjau link, lihat Melihat pratinjau link dengan smart chip.
Java
- Add-on Google Workspace yang mendukung pratinjau link untuk pola link referensi yang dibuat pengguna. Untuk mem-build add-on dengan pratinjau link, lihat Melihat pratinjau link dengan smart chip.
Menyiapkan pembuatan resource untuk add-on Anda
Bagian ini menjelaskan cara menyiapkan pembuatan resource untuk add-on Anda, yang mencakup langkah-langkah berikut:
- Konfigurasi pembuatan resource di manifes add-on Anda.
- Buat kartu formulir yang diperlukan pengguna untuk membuat resource dalam layanan Anda.
- Kelola pengiriman formulir sehingga fungsi yang membuat resource berjalan saat pengguna mengirimkan formulir.
Mengonfigurasi pembuatan resource
Untuk mengonfigurasi pembuatan resource, tentukan bagian dan kolom berikut dalam manifes add-on Anda:
Di bagian
addOns
di kolomdocs
, terapkan pemicucreateActionTriggers
yang menyertakanrunFunction
. (Anda menentukan fungsi ini di bagian berikut, Mem-build kartu formulir.)Untuk mempelajari kolom yang dapat Anda tentukan di pemicu
createActionTriggers
, lihat dokumentasi referensi untuk manifes Apps Script atau resource deployment untuk runtime lain.Di kolom
oauthScopes
, tambahkan cakupanhttps://www.googleapis.com/auth/workspace.linkcreate
sehingga pengguna dapat memberi otorisasi add-on untuk membuat resource. Secara khusus, cakupan ini memungkinkan add-on membaca informasi yang dikirimkan pengguna ke formulir pembuatan resource dan menyisipkan smart chip ke dalam dokumen berdasarkan informasi tersebut.
Sebagai contoh, lihat bagian addons
dari manifes yang mengonfigurasi
pembuatan resource untuk layanan kasus dukungan berikut:
{
"oauthScopes": [
"https://www.googleapis.com/auth/workspace.linkpreview",
"https://www.googleapis.com/auth/workspace.linkcreate"
],
"addOns": {
"docs": {
"linkPreviewTriggers": [
...
],
"createActionTriggers": [
{
"id": "createCase",
"labelText": "Create support case",
"localizedLabelText": {
"es": "Crear caso de soporte"
},
"runFunction": "createCaseInputCard",
"logoUrl": "https://www.example.com/images/case.png"
}
]
}
}
}
Dalam contoh ini, add-on Google Workspace memungkinkan pengguna membuat kasus dukungan.
Setiap pemicu createActionTriggers
harus memiliki kolom berikut:
- ID unik
- Label teks yang muncul di menu @ Dokumen
- URL logo yang mengarah ke ikon yang muncul di samping teks label di menu @
- Fungsi callback yang mereferensikan fungsi Apps Script atau endpoint HTTP yang menampilkan kartu
Membuat kartu formulir
Untuk membuat resource di layanan dari menu @Doc, Anda harus menerapkan fungsi apa pun yang ditentukan dalam objek createActionTriggers
.
Saat pengguna berinteraksi dengan salah satu item menu Anda, pemicu createActionTriggers
yang sesuai akan diaktifkan dan fungsi callback-nya akan menampilkan kartu dengan input formulir untuk membuat resource.
Elemen dan tindakan yang didukung
Untuk membuat antarmuka kartu, Anda menggunakan widget untuk menampilkan informasi dan input yang diperlukan pengguna untuk membuat resource. Sebagian besar widget dan tindakan add-on Google Workspace didukung dengan pengecualian berikut:
- Footer kartu tidak didukung.
- Notifikasi tidak didukung.
- Untuk navigasi, hanya navigasi
updateCard
yang didukung.
Contoh kartu dengan input formulir
Contoh berikut menunjukkan fungsi callback Apps Script yang menampilkan kartu saat pengguna memilih Buat kasus dukungan dari menu @:
Apps Script
/** * 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; } }
Node.js
/** * Produces a support case creation form card. * * @param {!Object} event The event object. * @param {!Object=} errors An optional map of per-field error messages. * @param {boolean} isUpdate Whether to return the form as an update card navigation. * @return {!Card|!ActionResponse} The resulting card or action response. */ function createCaseInputCard(event, errors, isUpdate) { const cardHeader1 = { title: "Create a support case" }; const cardSection1TextInput1 = { textInput: { name: "name", label: "Name" } }; const cardSection1TextInput2 = { textInput: { name: "description", label: "Description", type: "MULTIPLE_LINE" } }; const cardSection1SelectionInput1 = { selectionInput: { name: "priority", label: "Priority", type: "DROPDOWN", items: [{ text: "P0", value: "P0" }, { text: "P1", value: "P1" }, { text: "P2", value: "P2" }, { text: "P3", value: "P3" }] } }; const cardSection1SelectionInput2 = { selectionInput: { name: "impact", label: "Impact", items: [{ text: "Blocks a critical customer operation", value: "Blocks a critical customer operation" }] } }; const cardSection1ButtonList1Button1Action1 = { function: process.env.URL, parameters: [ { key: "submitCaseCreationForm", value: true } ], persistValues: true }; const cardSection1ButtonList1Button1 = { text: "Create", onClick: { action: cardSection1ButtonList1Button1Action1 } }; const cardSection1ButtonList1 = { buttonList: { buttons: [cardSection1ButtonList1Button1] } }; // Builds the creation form and adds error text for invalid inputs. const cardSection1 = []; if (errors?.name) { cardSection1.push(createErrorTextParagraph(errors.name)); } cardSection1.push(cardSection1TextInput1); if (errors?.description) { cardSection1.push(createErrorTextParagraph(errors.description)); } cardSection1.push(cardSection1TextInput2); if (errors?.priority) { cardSection1.push(createErrorTextParagraph(errors.priority)); } cardSection1.push(cardSection1SelectionInput1); if (errors?.impact) { cardSection1.push(createErrorTextParagraph(errors.impact)); } cardSection1.push(cardSection1SelectionInput2); cardSection1.push(cardSection1ButtonList1); const card = { header: cardHeader1, sections: [{ widgets: cardSection1 }] }; if (isUpdate) { return { renderActions: { action: { navigations: [{ updateCard: card }] } } }; } else { return { action: { navigations: [{ pushCard: card }] } }; } }
Python
def create_case_input_card(event, errors = {}, isUpdate = False): """Produces a support case creation form card. Args: event: The event object. errors: An optional dict of per-field error messages. isUpdate: Whether to return the form as an update card navigation. Returns: The resulting card or action response. """ card_header1 = { "title": "Create a support case" } card_section1_text_input1 = { "textInput": { "name": "name", "label": "Name" } } card_section1_text_input2 = { "textInput": { "name": "description", "label": "Description", "type": "MULTIPLE_LINE" } } card_section1_selection_input1 = { "selectionInput": { "name": "priority", "label": "Priority", "type": "DROPDOWN", "items": [{ "text": "P0", "value": "P0" }, { "text": "P1", "value": "P1" }, { "text": "P2", "value": "P2" }, { "text": "P3", "value": "P3" }] } } card_section1_selection_input2 = { "selectionInput": { "name": "impact", "label": "Impact", "items": [{ "text": "Blocks a critical customer operation", "value": "Blocks a critical customer operation" }] } } card_section1_button_list1_button1_action1 = { "function": os.environ["URL"], "parameters": [ { "key": "submitCaseCreationForm", "value": True } ], "persistValues": True } card_section1_button_list1_button1 = { "text": "Create", "onClick": { "action": card_section1_button_list1_button1_action1 } } card_section1_button_list1 = { "buttonList": { "buttons": [card_section1_button_list1_button1] } } # Builds the creation form and adds error text for invalid inputs. card_section1 = [] if "name" in errors: card_section1.append(create_error_text_paragraph(errors["name"])) card_section1.append(card_section1_text_input1) if "description" in errors: card_section1.append(create_error_text_paragraph(errors["description"])) card_section1.append(card_section1_text_input2) if "priority" in errors: card_section1.append(create_error_text_paragraph(errors["priority"])) card_section1.append(card_section1_selection_input1) if "impact" in errors: card_section1.append(create_error_text_paragraph(errors["impact"])) card_section1.append(card_section1_selection_input2) card_section1.append(card_section1_button_list1) card = { "header": card_header1, "sections": [{ "widgets": card_section1 }] } if isUpdate: return { "renderActions": { "action": { "navigations": [{ "updateCard": card }] } } } else: return { "action": { "navigations": [{ "pushCard": card }] } }
Java
/** * Produces a support case creation form. * * @param event The event object. * @param errors A map of per-field error messages. * @param isUpdate Whether to return the form as an update card navigation. * @return The resulting card or action response. */ JsonObject createCaseInputCard(JsonObject event, Map<String, String> errors, boolean isUpdate) { JsonObject cardHeader = new JsonObject(); cardHeader.add("title", new JsonPrimitive("Create a support case")); JsonObject cardSectionTextInput1 = new JsonObject(); cardSectionTextInput1.add("name", new JsonPrimitive("name")); cardSectionTextInput1.add("label", new JsonPrimitive("Name")); JsonObject cardSectionTextInput1Widget = new JsonObject(); cardSectionTextInput1Widget.add("textInput", cardSectionTextInput1); JsonObject cardSectionTextInput2 = new JsonObject(); cardSectionTextInput2.add("name", new JsonPrimitive("description")); cardSectionTextInput2.add("label", new JsonPrimitive("Description")); cardSectionTextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE")); JsonObject cardSectionTextInput2Widget = new JsonObject(); cardSectionTextInput2Widget.add("textInput", cardSectionTextInput2); JsonObject cardSectionSelectionInput1ItemsItem1 = new JsonObject(); cardSectionSelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0")); cardSectionSelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0")); JsonObject cardSectionSelectionInput1ItemsItem2 = new JsonObject(); cardSectionSelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1")); cardSectionSelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1")); JsonObject cardSectionSelectionInput1ItemsItem3 = new JsonObject(); cardSectionSelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2")); cardSectionSelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2")); JsonObject cardSectionSelectionInput1ItemsItem4 = new JsonObject(); cardSectionSelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3")); cardSectionSelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3")); JsonArray cardSectionSelectionInput1Items = new JsonArray(); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4); JsonObject cardSectionSelectionInput1 = new JsonObject(); cardSectionSelectionInput1.add("name", new JsonPrimitive("priority")); cardSectionSelectionInput1.add("label", new JsonPrimitive("Priority")); cardSectionSelectionInput1.add("type", new JsonPrimitive("DROPDOWN")); cardSectionSelectionInput1.add("items", cardSectionSelectionInput1Items); JsonObject cardSectionSelectionInput1Widget = new JsonObject(); cardSectionSelectionInput1Widget.add("selectionInput", cardSectionSelectionInput1); JsonObject cardSectionSelectionInput2ItemsItem = new JsonObject(); cardSectionSelectionInput2ItemsItem.add("text", new JsonPrimitive("Blocks a critical customer operation")); cardSectionSelectionInput2ItemsItem.add("value", new JsonPrimitive("Blocks a critical customer operation")); JsonArray cardSectionSelectionInput2Items = new JsonArray(); cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem); JsonObject cardSectionSelectionInput2 = new JsonObject(); cardSectionSelectionInput2.add("name", new JsonPrimitive("impact")); cardSectionSelectionInput2.add("label", new JsonPrimitive("Impact")); cardSectionSelectionInput2.add("items", cardSectionSelectionInput2Items); JsonObject cardSectionSelectionInput2Widget = new JsonObject(); cardSectionSelectionInput2Widget.add("selectionInput", cardSectionSelectionInput2); JsonObject cardSectionButtonListButtonActionParametersParameter = new JsonObject(); cardSectionButtonListButtonActionParametersParameter.add("key", new JsonPrimitive("submitCaseCreationForm")); cardSectionButtonListButtonActionParametersParameter.add("value", new JsonPrimitive(true)); JsonArray cardSectionButtonListButtonActionParameters = new JsonArray(); cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter); JsonObject cardSectionButtonListButtonAction = new JsonObject(); cardSectionButtonListButtonAction.add("function", new JsonPrimitive(System.getenv().get("URL"))); cardSectionButtonListButtonAction.add("parameters", cardSectionButtonListButtonActionParameters); cardSectionButtonListButtonAction.add("persistValues", new JsonPrimitive(true)); JsonObject cardSectionButtonListButtonOnCLick = new JsonObject(); cardSectionButtonListButtonOnCLick.add("action", cardSectionButtonListButtonAction); JsonObject cardSectionButtonListButton = new JsonObject(); cardSectionButtonListButton.add("text", new JsonPrimitive("Create")); cardSectionButtonListButton.add("onClick", cardSectionButtonListButtonOnCLick); JsonArray cardSectionButtonListButtons = new JsonArray(); cardSectionButtonListButtons.add(cardSectionButtonListButton); JsonObject cardSectionButtonList = new JsonObject(); cardSectionButtonList.add("buttons", cardSectionButtonListButtons); JsonObject cardSectionButtonListWidget = new JsonObject(); cardSectionButtonListWidget.add("buttonList", cardSectionButtonList); // Builds the form inputs with error texts for invalid values. JsonArray cardSection = new JsonArray(); if (errors.containsKey("name")) { cardSection.add(createErrorTextParagraph(errors.get("name").toString())); } cardSection.add(cardSectionTextInput1Widget); if (errors.containsKey("description")) { cardSection.add(createErrorTextParagraph(errors.get("description").toString())); } cardSection.add(cardSectionTextInput2Widget); if (errors.containsKey("priority")) { cardSection.add(createErrorTextParagraph(errors.get("priority").toString())); } cardSection.add(cardSectionSelectionInput1Widget); if (errors.containsKey("impact")) { cardSection.add(createErrorTextParagraph(errors.get("impact").toString())); } cardSection.add(cardSectionSelectionInput2Widget); cardSection.add(cardSectionButtonListWidget); JsonObject cardSectionWidgets = new JsonObject(); cardSectionWidgets.add("widgets", cardSection); JsonArray sections = new JsonArray(); sections.add(cardSectionWidgets); JsonObject card = new JsonObject(); card.add("header", cardHeader); card.add("sections", sections); JsonObject navigation = new JsonObject(); if (isUpdate) { navigation.add("updateCard", card); } else { navigation.add("pushCard", card); } JsonArray navigations = new JsonArray(); navigations.add(navigation); JsonObject action = new JsonObject(); action.add("navigations", navigations); JsonObject renderActions = new JsonObject(); renderActions.add("action", action); if (!isUpdate) { return renderActions; } JsonObject update = new JsonObject(); update.add("renderActions", renderActions); return update; }
Fungsi createCaseInputCard
merender kartu berikut:
Kartu ini mencakup input teks, menu drop-down, dan kotak centang. Fungsi ini juga memiliki
tombol teks dengan tindakanonClick
yang menjalankan fungsi lain untuk
menangani
pengiriman formulir pembuatan.
Setelah pengguna mengisi formulir dan mengklik
Buat, add-on akan mengirimkan input formulir ke fungsi tindakan
onClick
–disebut submitCaseCreationForm
dalam contoh kami–pada saat itulah
add-on dapat memvalidasi input dan menggunakannya untuk
membuat resource di layanan pihak ketiga.
Menangani pengiriman formulir
Setelah pengguna mengirimkan formulir pembuatan, fungsi yang terkait dengan
tindakan onClick
akan berjalan. Untuk pengalaman pengguna yang ideal, add-on Anda harus menangani pengiriman formulir yang berhasil dan salah.
Menangani pembuatan resource yang berhasil
Fungsi onClick
add-on Anda akan membuat
resource di layanan pihak ketiga dan membuat URL yang mengarah ke resource tersebut.
Untuk menyampaikan URL resource kembali ke Dokumen
untuk pembuatan chip, fungsi onClick
harus menampilkan SubmitFormResponse
dengan array satu elemen di renderActions.action.links
yang mengarah ke
link. Judul link harus mewakili judul resource yang dibuat dan
URL harus mengarah ke resource tersebut.
Contoh berikut menunjukkan SubmitFormResponse
untuk resource yang dibuat:
Apps Script
/** * 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 }] } } }; }
Node.js
/** * 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 }] } } }; }
Python
def create_link_render_action(title, url): """Returns a submit form response that inserts a link into the document. Args: title: The title of the link to insert. url: The URL of the link to insert. Returns: The resulting submit form response. """ return { "renderActions": { "action": { "links": [{ "title": title, "url": url }] } } }
Java
/** * Returns a submit form response that inserts a link into the document. * * @param title The title of the link to insert. * @param url The URL of the link to insert. * @return The resulting submit form response. */ JsonObject createLinkRenderAction(String title, String url) { JsonObject link = new JsonObject(); link.add("title", new JsonPrimitive(title)); link.add("url", new JsonPrimitive(url)); JsonArray links = new JsonArray(); links.add(link); JsonObject action = new JsonObject(); action.add("links", links); JsonObject renderActions = new JsonObject(); renderActions.add("action", action); JsonObject linkRenderAction = new JsonObject(); linkRenderAction.add("renderActions", renderActions); return linkRenderAction; }
Setelah SubmitFormResponse
ditampilkan, dialog modal akan ditutup dan
add-on akan menyisipkan chip ke dalam dokumen.
Saat pengguna menahan kursor di atas chip ini, tindakan tersebut akan memanggil pemicu pratinjau
link yang terkait. Pastikan add-on Anda tidak menyisipkan chip dengan pola link yang tidak didukung oleh pemicu pratinjau link Anda.
Menangani error
Jika pengguna mencoba mengirimkan formulir dengan kolom yang tidak valid, add-on harus menampilkan tindakan render yang menampilkan error menggunakan navigasi updateCard
, bukan menampilkan
SubmitFormResponse
dengan link.
Hal ini memungkinkan pengguna melihat kesalahan
yang mereka lakukan dan mencoba lagi. Lihat
updateCard(card)
untuk
Apps Script dan updateCard
untuk runtime lainnya. Notifikasi dan navigasi pushCard
tidak didukung.
Contoh penanganan error
Contoh berikut menunjukkan kode yang dipanggil saat pengguna mengirimkan
formulir. Jika input tidak valid, kartu akan diperbarui dan menampilkan pesan error. Jika input valid,
add-on akan menampilkan SubmitFormResponse
dengan
link ke resource yang dibuat.
Apps Script
/** * 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("&"); }
Node.js
/** * Submits the creation form. If valid, returns a render action * that inserts a new link into the document. If invalid, returns an * update card navigation that re-renders the creation form with error messages. * * @param {!Object} event The event object with form input values. * @return {!ActionResponse|!SubmitFormResponse} The resulting response. */ function submitCaseCreationForm(event) { const caseDetails = { name: event.commonEventObject.formInputs?.name?.stringInputs?.value[0], description: event.commonEventObject.formInputs?.description?.stringInputs?.value[0], priority: event.commonEventObject.formInputs?.priority?.stringInputs?.value[0], impact: !!event.commonEventObject.formInputs?.impact?.stringInputs?.value[0], }; const errors = validateFormInputs(caseDetails); if (Object.keys(errors).length > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { const title = `Case ${caseDetails.name}`; // Adds the case details as parameters to the generated link URL. const url = new URL('https://example.com/support/cases/'); for (const [key, value] of Object.entries(caseDetails)) { url.searchParams.append(key, value); } return createLinkRenderAction(title, url.href); } }
Python
def submit_case_creation_form(event): """Submits the creation form. If valid, returns a render action that inserts a new link into the document. If invalid, returns an update card navigation that re-renders the creation form with error messages. Args: event: The event object with form input values. Returns: The resulting response. """ formInputs = event["commonEventObject"]["formInputs"] if "formInputs" in event["commonEventObject"] else None case_details = { "name": None, "description": None, "priority": None, "impact": None, } if formInputs is not None: case_details["name"] = formInputs["name"]["stringInputs"]["value"][0] if "name" in formInputs else None case_details["description"] = formInputs["description"]["stringInputs"]["value"][0] if "description" in formInputs else None case_details["priority"] = formInputs["priority"]["stringInputs"]["value"][0] if "priority" in formInputs else None case_details["impact"] = formInputs["impact"]["stringInputs"]["value"][0] if "impact" in formInputs else False errors = validate_form_inputs(case_details) if len(errors) > 0: return create_case_input_card(event, errors, True) # Update mode else: title = f'Case {case_details["name"]}' # Adds the case details as parameters to the generated link URL. url = "https://example.com/support/cases/?" + urlencode(case_details) return create_link_render_action(title, url)
Java
/** * Submits the creation form. If valid, returns a render action * that inserts a new link into the document. If invalid, returns an * update card navigation that re-renders the creation form with error messages. * * @param event The event object with form input values. * @return The resulting response. */ JsonObject submitCaseCreationForm(JsonObject event) throws Exception { JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs"); Map<String, String> caseDetails = new HashMap<String, String>(); 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<String, String> errors = validateFormInputs(caseDetails); if (errors.size() > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { String title = String.format("Case %s", caseDetails.get("name")); // Adds the case details as parameters to the generated link URL. URIBuilder uriBuilder = new URIBuilder("https://example.com/support/cases/"); for (String caseDetailKey : caseDetails.keySet()) { uriBuilder.addParameter(caseDetailKey, caseDetails.get(caseDetailKey)); } return createLinkRenderAction(title, uriBuilder.build().toURL().toString()); } }
Contoh kode berikut memvalidasi input formulir dan membuat pesan error untuk input yang tidak valid:
Apps Script
/** * 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>'); }
Node.js
/** * Validates case creation form input values. * * @param {!Object} caseDetails The values of each form input submitted by the user. * @return {!Object} A map from field name to error message. An empty object * represents a valid form submission. */ function validateFormInputs(caseDetails) { const errors = {}; if (caseDetails.name === undefined) { errors.name = 'You must provide a name'; } if (caseDetails.description === undefined) { errors.description = 'You must provide a description'; } if (caseDetails.priority === undefined) { errors.priority = 'You must provide a priority'; } if (caseDetails.impact && !(['P0', 'P1']).includes(caseDetails.priority)) { errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; } return errors; } /** * Returns a text paragraph with red text indicating a form field validation error. * * @param {string} errorMessage A description of input value error. * @return {!TextParagraph} The resulting text paragraph. */ function createErrorTextParagraph(errorMessage) { return { textParagraph: { text: '<font color=\"#BA0300\"><b>Error:</b> ' + errorMessage + '</font>' } } }
Python
def validate_form_inputs(case_details): """Validates case creation form input values. Args: case_details: The values of each form input submitted by the user. Returns: A dict from field name to error message. An empty object represents a valid form submission. """ errors = {} if case_details["name"] is None: errors["name"] = "You must provide a name" if case_details["description"] is None: errors["description"] = "You must provide a description" if case_details["priority"] is None: errors["priority"] = "You must provide a priority" if case_details["impact"] is not None and case_details["priority"] not in ['P0', 'P1']: errors["impact"] = "If an issue blocks a critical customer operation, priority must be P0 or P1" return errors def create_error_text_paragraph(error_message): """Returns a text paragraph with red text indicating a form field validation error. Args: error_essage: A description of input value error. Returns: The resulting text paragraph. """ return { "textParagraph": { "text": '<font color=\"#BA0300\"><b>Error:</b> ' + error_message + '</font>' } }
Java
/** * Validates case creation form input values. * * @param caseDetails The values of each form input submitted by the user. * @return A map from field name to error message. An empty object * represents a valid form submission. */ Map<String, String> validateFormInputs(Map<String, String> caseDetails) { Map<String, String> errors = new HashMap<String, String>(); if (!caseDetails.containsKey("name")) { errors.put("name", "You must provide a name"); } if (!caseDetails.containsKey("description")) { errors.put("description", "You must provide a description"); } if (!caseDetails.containsKey("priority")) { errors.put("priority", "You must provide a priority"); } if (caseDetails.containsKey("impact") && !Arrays.asList(new String[]{"P0", "P1"}).contains(caseDetails.get("priority"))) { errors.put("impact", "If an issue blocks a critical customer operation, priority must be P0 or P1"); } return errors; } /** * Returns a text paragraph with red text indicating a form field validation error. * * @param errorMessage A description of input value error. * @return The resulting text paragraph. */ JsonObject createErrorTextParagraph(String errorMessage) { JsonObject textParagraph = new JsonObject(); textParagraph.add("text", new JsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> " + errorMessage + "</font>")); JsonObject textParagraphWidget = new JsonObject(); textParagraphWidget.add("textParagraph", textParagraph); return textParagraphWidget; }
Contoh lengkap: Add-on kasus dukungan
Contoh berikut menunjukkan add-on Google Workspace yang menampilkan pratinjau link ke kasus dukungan perusahaan dan memungkinkan pengguna membuat kasus dukungan dari dalam Google Dokumen.
Contoh ini melakukan hal berikut:
- Membuat kartu dengan kolom formulir untuk membuat kasus dukungan dari menu @ Dokumen.
- Memvalidasi input formulir dan menampilkan pesan error untuk input yang tidak valid.
- Menyisipkan nama dan link kasus dukungan yang dibuat ke dalam dokumen Dokumen sebagai smart chip.
- Melihat pratinjau link ke kasus dukungan, seperti
https://www.example.com/support/cases/1234
. Smart chip menampilkan ikon, dan kartu pratinjau menyertakan nama kasus, prioritas, dan deskripsi.
Manifes
Apps Script
{ "timeZone": "America/New_York", "exceptionLogging": "STACKDRIVER", "runtimeVersion": "V8", "oauthScopes": [ "https://www.googleapis.com/auth/workspace.linkpreview", "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { "name": "Manage support cases", "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } }, "docs": { "linkPreviewTriggers": [ { "runFunction": "caseLinkPreview", "patterns": [ { "hostPattern": "example.com", "pathPrefix": "support/cases" }, { "hostPattern": "*.example.com", "pathPrefix": "cases" }, { "hostPattern": "cases.example.com" } ], "labelText": "Support case", "localizedLabelText": { "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-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" } ] } } }
Node.js
{ "oauthScopes": [ "https://www.googleapis.com/auth/workspace.linkpreview", "https://www.googleapis.com/auth/workspace.linkcreate" ], "addOns": { "common": { "name": "Manage support cases", "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png", "layoutProperties": { "primaryColor": "#dd4b39" } }, "docs": { "linkPreviewTriggers": [ { "runFunction": "$URL1", "patterns": [ { "hostPattern": "example.com", "pathPrefix": "support/cases" }, { "hostPattern": "*.example.com", "pathPrefix": "cases" }, { "hostPattern": "cases.example.com" } ], "labelText": "Support case", "localizedLabelText": { "es": "Caso de soporte" }, "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-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" } ] } } }
Kode
Apps Script
/** * 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. */ /** * 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; } /** * 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; } } /** * 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("&"); } /** * 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>'); } /** * 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 }] } } }; }
Node.js
/** * 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. */ /** * Responds to any HTTP request related to link previews. * * @param {Object} req An HTTP request context. * @param {Object} res An HTTP response context. */ exports.createLinkPreview = (req, res) => { const event = req.body; if (event.docs.matchedUrl.url) { const url = event.docs.matchedUrl.url; const parsedUrl = new URL(url); // If the event object URL matches a specified pattern for preview links. if (parsedUrl.hostname === 'example.com') { if (parsedUrl.pathname.startsWith('/support/cases/')) { return res.json(caseLinkPreview(parsedUrl)); } } } }; /** * * A support case link preview. * * @param {!URL} url The event object. * @return {!Card} The resulting preview link card. */ function caseLinkPreview(url) { // Builds a preview card with the case name, and description // Uses the text from the card's header for the title of the smart chip. // Parses the URL and identify the case details. const name = `Case ${url.searchParams.get("name")}`; return { action: { linkPreview: { title: name, previewCard: { header: { title: name }, sections: [{ widgets: [{ textParagraph: { text: url.searchParams.get("description") } }] }] } } } }; } /** * Responds to any HTTP request related to 3P resource creations. * * @param {Object} req An HTTP request context. * @param {Object} res An HTTP response context. */ exports.create3pResources = (req, res) => { const event = req.body; if (event.commonEventObject.parameters?.submitCaseCreationForm) { res.json(submitCaseCreationForm(event)); } else { res.json(createCaseInputCard(event)); } }; /** * Produces a support case creation form card. * * @param {!Object} event The event object. * @param {!Object=} errors An optional map of per-field error messages. * @param {boolean} isUpdate Whether to return the form as an update card navigation. * @return {!Card|!ActionResponse} The resulting card or action response. */ function createCaseInputCard(event, errors, isUpdate) { const cardHeader1 = { title: "Create a support case" }; const cardSection1TextInput1 = { textInput: { name: "name", label: "Name" } }; const cardSection1TextInput2 = { textInput: { name: "description", label: "Description", type: "MULTIPLE_LINE" } }; const cardSection1SelectionInput1 = { selectionInput: { name: "priority", label: "Priority", type: "DROPDOWN", items: [{ text: "P0", value: "P0" }, { text: "P1", value: "P1" }, { text: "P2", value: "P2" }, { text: "P3", value: "P3" }] } }; const cardSection1SelectionInput2 = { selectionInput: { name: "impact", label: "Impact", items: [{ text: "Blocks a critical customer operation", value: "Blocks a critical customer operation" }] } }; const cardSection1ButtonList1Button1Action1 = { function: process.env.URL, parameters: [ { key: "submitCaseCreationForm", value: true } ], persistValues: true }; const cardSection1ButtonList1Button1 = { text: "Create", onClick: { action: cardSection1ButtonList1Button1Action1 } }; const cardSection1ButtonList1 = { buttonList: { buttons: [cardSection1ButtonList1Button1] } }; // Builds the creation form and adds error text for invalid inputs. const cardSection1 = []; if (errors?.name) { cardSection1.push(createErrorTextParagraph(errors.name)); } cardSection1.push(cardSection1TextInput1); if (errors?.description) { cardSection1.push(createErrorTextParagraph(errors.description)); } cardSection1.push(cardSection1TextInput2); if (errors?.priority) { cardSection1.push(createErrorTextParagraph(errors.priority)); } cardSection1.push(cardSection1SelectionInput1); if (errors?.impact) { cardSection1.push(createErrorTextParagraph(errors.impact)); } cardSection1.push(cardSection1SelectionInput2); cardSection1.push(cardSection1ButtonList1); const card = { header: cardHeader1, sections: [{ widgets: cardSection1 }] }; if (isUpdate) { return { renderActions: { action: { navigations: [{ updateCard: card }] } } }; } else { return { action: { navigations: [{ pushCard: card }] } }; } } /** * Submits the creation form. If valid, returns a render action * that inserts a new link into the document. If invalid, returns an * update card navigation that re-renders the creation form with error messages. * * @param {!Object} event The event object with form input values. * @return {!ActionResponse|!SubmitFormResponse} The resulting response. */ function submitCaseCreationForm(event) { const caseDetails = { name: event.commonEventObject.formInputs?.name?.stringInputs?.value[0], description: event.commonEventObject.formInputs?.description?.stringInputs?.value[0], priority: event.commonEventObject.formInputs?.priority?.stringInputs?.value[0], impact: !!event.commonEventObject.formInputs?.impact?.stringInputs?.value[0], }; const errors = validateFormInputs(caseDetails); if (Object.keys(errors).length > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { const title = `Case ${caseDetails.name}`; // Adds the case details as parameters to the generated link URL. const url = new URL('https://example.com/support/cases/'); for (const [key, value] of Object.entries(caseDetails)) { url.searchParams.append(key, value); } return createLinkRenderAction(title, url.href); } } /** * Validates case creation form input values. * * @param {!Object} caseDetails The values of each form input submitted by the user. * @return {!Object} A map from field name to error message. An empty object * represents a valid form submission. */ function validateFormInputs(caseDetails) { const errors = {}; if (caseDetails.name === undefined) { errors.name = 'You must provide a name'; } if (caseDetails.description === undefined) { errors.description = 'You must provide a description'; } if (caseDetails.priority === undefined) { errors.priority = 'You must provide a priority'; } if (caseDetails.impact && !(['P0', 'P1']).includes(caseDetails.priority)) { errors.impact = 'If an issue blocks a critical customer operation, priority must be P0 or P1'; } return errors; } /** * Returns a text paragraph with red text indicating a form field validation error. * * @param {string} errorMessage A description of input value error. * @return {!TextParagraph} The resulting text paragraph. */ function createErrorTextParagraph(errorMessage) { return { textParagraph: { text: '<font color=\"#BA0300\"><b>Error:</b> ' + errorMessage + '</font>' } } } /** * 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 }] } } }; }
Python
# 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. from typing import Any, Mapping from urllib.parse import urlencode import os import flask import functions_framework @functions_framework.http def create_3p_resources(req: flask.Request): """Responds to any HTTP request related to 3P resource creations. Args: req: An HTTP request context. Returns: An HTTP response context. """ event = req.get_json(silent=True) parameters = event["commonEventObject"]["parameters"] if "parameters" in event["commonEventObject"] else None if parameters is not None and parameters["submitCaseCreationForm"]: return submit_case_creation_form(event) else: return create_case_input_card(event) def create_case_input_card(event, errors = {}, isUpdate = False): """Produces a support case creation form card. Args: event: The event object. errors: An optional dict of per-field error messages. isUpdate: Whether to return the form as an update card navigation. Returns: The resulting card or action response. """ card_header1 = { "title": "Create a support case" } card_section1_text_input1 = { "textInput": { "name": "name", "label": "Name" } } card_section1_text_input2 = { "textInput": { "name": "description", "label": "Description", "type": "MULTIPLE_LINE" } } card_section1_selection_input1 = { "selectionInput": { "name": "priority", "label": "Priority", "type": "DROPDOWN", "items": [{ "text": "P0", "value": "P0" }, { "text": "P1", "value": "P1" }, { "text": "P2", "value": "P2" }, { "text": "P3", "value": "P3" }] } } card_section1_selection_input2 = { "selectionInput": { "name": "impact", "label": "Impact", "items": [{ "text": "Blocks a critical customer operation", "value": "Blocks a critical customer operation" }] } } card_section1_button_list1_button1_action1 = { "function": os.environ["URL"], "parameters": [ { "key": "submitCaseCreationForm", "value": True } ], "persistValues": True } card_section1_button_list1_button1 = { "text": "Create", "onClick": { "action": card_section1_button_list1_button1_action1 } } card_section1_button_list1 = { "buttonList": { "buttons": [card_section1_button_list1_button1] } } # Builds the creation form and adds error text for invalid inputs. card_section1 = [] if "name" in errors: card_section1.append(create_error_text_paragraph(errors["name"])) card_section1.append(card_section1_text_input1) if "description" in errors: card_section1.append(create_error_text_paragraph(errors["description"])) card_section1.append(card_section1_text_input2) if "priority" in errors: card_section1.append(create_error_text_paragraph(errors["priority"])) card_section1.append(card_section1_selection_input1) if "impact" in errors: card_section1.append(create_error_text_paragraph(errors["impact"])) card_section1.append(card_section1_selection_input2) card_section1.append(card_section1_button_list1) card = { "header": card_header1, "sections": [{ "widgets": card_section1 }] } if isUpdate: return { "renderActions": { "action": { "navigations": [{ "updateCard": card }] } } } else: return { "action": { "navigations": [{ "pushCard": card }] } } def submit_case_creation_form(event): """Submits the creation form. If valid, returns a render action that inserts a new link into the document. If invalid, returns an update card navigation that re-renders the creation form with error messages. Args: event: The event object with form input values. Returns: The resulting response. """ formInputs = event["commonEventObject"]["formInputs"] if "formInputs" in event["commonEventObject"] else None case_details = { "name": None, "description": None, "priority": None, "impact": None, } if formInputs is not None: case_details["name"] = formInputs["name"]["stringInputs"]["value"][0] if "name" in formInputs else None case_details["description"] = formInputs["description"]["stringInputs"]["value"][0] if "description" in formInputs else None case_details["priority"] = formInputs["priority"]["stringInputs"]["value"][0] if "priority" in formInputs else None case_details["impact"] = formInputs["impact"]["stringInputs"]["value"][0] if "impact" in formInputs else False errors = validate_form_inputs(case_details) if len(errors) > 0: return create_case_input_card(event, errors, True) # Update mode else: title = f'Case {case_details["name"]}' # Adds the case details as parameters to the generated link URL. url = "https://example.com/support/cases/?" + urlencode(case_details) return create_link_render_action(title, url) def validate_form_inputs(case_details): """Validates case creation form input values. Args: case_details: The values of each form input submitted by the user. Returns: A dict from field name to error message. An empty object represents a valid form submission. """ errors = {} if case_details["name"] is None: errors["name"] = "You must provide a name" if case_details["description"] is None: errors["description"] = "You must provide a description" if case_details["priority"] is None: errors["priority"] = "You must provide a priority" if case_details["impact"] is not None and case_details["priority"] not in ['P0', 'P1']: errors["impact"] = "If an issue blocks a critical customer operation, priority must be P0 or P1" return errors def create_error_text_paragraph(error_message): """Returns a text paragraph with red text indicating a form field validation error. Args: error_essage: A description of input value error. Returns: The resulting text paragraph. """ return { "textParagraph": { "text": '<font color=\"#BA0300\"><b>Error:</b> ' + error_message + '</font>' } } def create_link_render_action(title, url): """Returns a submit form response that inserts a link into the document. Args: title: The title of the link to insert. url: The URL of the link to insert. Returns: The resulting submit form response. """ return { "renderActions": { "action": { "links": [{ "title": title, "url": url }] } } }
Kode berikut menunjukkan cara menerapkan pratinjau link untuk resource yang dibuat:
# 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. from typing import Any, Mapping from urllib.parse import urlparse, parse_qs import flask import functions_framework @functions_framework.http def create_link_preview(req: flask.Request): """Responds to any HTTP request related to link previews. Args: req: An HTTP request context. Returns: An HTTP response context. """ event = req.get_json(silent=True) if event["docs"]["matchedUrl"]["url"]: url = event["docs"]["matchedUrl"]["url"] parsed_url = urlparse(url) # If the event object URL matches a specified pattern for preview links. if parsed_url.hostname == "example.com": if parsed_url.path.startswith("/support/cases/"): return case_link_preview(parsed_url) return {} def case_link_preview(url): """A support case link preview. Args: url: A matching URL. Returns: The resulting preview link card. """ # Parses the URL and identify the case details. query_string = parse_qs(url.query) name = f'Case {query_string["name"][0]}' # Uses the text from the card's header for the title of the smart chip. return { "action": { "linkPreview": { "title": name, "previewCard": { "header": { "title": name }, "sections": [{ "widgets": [{ "textParagraph": { "text": query_string["description"][0] } }] }], } } } }
Java
/** * 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. */ import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.apache.http.client.utils.URIBuilder; import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; public class Create3pResources implements HttpFunction { private static final Gson gson = new Gson(); /** * Responds to any HTTP request related to 3p resource creations. * * @param request An HTTP request context. * @param response An HTTP response context. */ @Override public void service(HttpRequest request, HttpResponse response) throws Exception { JsonObject event = gson.fromJson(request.getReader(), JsonObject.class); JsonObject parameters = event.getAsJsonObject("commonEventObject").getAsJsonObject("parameters"); if (parameters != null && parameters.has("submitCaseCreationForm") && parameters.get("submitCaseCreationForm").getAsBoolean()) { response.getWriter().write(gson.toJson(submitCaseCreationForm(event))); } else { response.getWriter().write(gson.toJson(createCaseInputCard(event, new HashMap<String, String>(), false))); } } /** * Produces a support case creation form. * * @param event The event object. * @param errors A map of per-field error messages. * @param isUpdate Whether to return the form as an update card navigation. * @return The resulting card or action response. */ JsonObject createCaseInputCard(JsonObject event, Map<String, String> errors, boolean isUpdate) { JsonObject cardHeader = new JsonObject(); cardHeader.add("title", new JsonPrimitive("Create a support case")); JsonObject cardSectionTextInput1 = new JsonObject(); cardSectionTextInput1.add("name", new JsonPrimitive("name")); cardSectionTextInput1.add("label", new JsonPrimitive("Name")); JsonObject cardSectionTextInput1Widget = new JsonObject(); cardSectionTextInput1Widget.add("textInput", cardSectionTextInput1); JsonObject cardSectionTextInput2 = new JsonObject(); cardSectionTextInput2.add("name", new JsonPrimitive("description")); cardSectionTextInput2.add("label", new JsonPrimitive("Description")); cardSectionTextInput2.add("type", new JsonPrimitive("MULTIPLE_LINE")); JsonObject cardSectionTextInput2Widget = new JsonObject(); cardSectionTextInput2Widget.add("textInput", cardSectionTextInput2); JsonObject cardSectionSelectionInput1ItemsItem1 = new JsonObject(); cardSectionSelectionInput1ItemsItem1.add("text", new JsonPrimitive("P0")); cardSectionSelectionInput1ItemsItem1.add("value", new JsonPrimitive("P0")); JsonObject cardSectionSelectionInput1ItemsItem2 = new JsonObject(); cardSectionSelectionInput1ItemsItem2.add("text", new JsonPrimitive("P1")); cardSectionSelectionInput1ItemsItem2.add("value", new JsonPrimitive("P1")); JsonObject cardSectionSelectionInput1ItemsItem3 = new JsonObject(); cardSectionSelectionInput1ItemsItem3.add("text", new JsonPrimitive("P2")); cardSectionSelectionInput1ItemsItem3.add("value", new JsonPrimitive("P2")); JsonObject cardSectionSelectionInput1ItemsItem4 = new JsonObject(); cardSectionSelectionInput1ItemsItem4.add("text", new JsonPrimitive("P3")); cardSectionSelectionInput1ItemsItem4.add("value", new JsonPrimitive("P3")); JsonArray cardSectionSelectionInput1Items = new JsonArray(); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3); cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4); JsonObject cardSectionSelectionInput1 = new JsonObject(); cardSectionSelectionInput1.add("name", new JsonPrimitive("priority")); cardSectionSelectionInput1.add("label", new JsonPrimitive("Priority")); cardSectionSelectionInput1.add("type", new JsonPrimitive("DROPDOWN")); cardSectionSelectionInput1.add("items", cardSectionSelectionInput1Items); JsonObject cardSectionSelectionInput1Widget = new JsonObject(); cardSectionSelectionInput1Widget.add("selectionInput", cardSectionSelectionInput1); JsonObject cardSectionSelectionInput2ItemsItem = new JsonObject(); cardSectionSelectionInput2ItemsItem.add("text", new JsonPrimitive("Blocks a critical customer operation")); cardSectionSelectionInput2ItemsItem.add("value", new JsonPrimitive("Blocks a critical customer operation")); JsonArray cardSectionSelectionInput2Items = new JsonArray(); cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem); JsonObject cardSectionSelectionInput2 = new JsonObject(); cardSectionSelectionInput2.add("name", new JsonPrimitive("impact")); cardSectionSelectionInput2.add("label", new JsonPrimitive("Impact")); cardSectionSelectionInput2.add("items", cardSectionSelectionInput2Items); JsonObject cardSectionSelectionInput2Widget = new JsonObject(); cardSectionSelectionInput2Widget.add("selectionInput", cardSectionSelectionInput2); JsonObject cardSectionButtonListButtonActionParametersParameter = new JsonObject(); cardSectionButtonListButtonActionParametersParameter.add("key", new JsonPrimitive("submitCaseCreationForm")); cardSectionButtonListButtonActionParametersParameter.add("value", new JsonPrimitive(true)); JsonArray cardSectionButtonListButtonActionParameters = new JsonArray(); cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter); JsonObject cardSectionButtonListButtonAction = new JsonObject(); cardSectionButtonListButtonAction.add("function", new JsonPrimitive(System.getenv().get("URL"))); cardSectionButtonListButtonAction.add("parameters", cardSectionButtonListButtonActionParameters); cardSectionButtonListButtonAction.add("persistValues", new JsonPrimitive(true)); JsonObject cardSectionButtonListButtonOnCLick = new JsonObject(); cardSectionButtonListButtonOnCLick.add("action", cardSectionButtonListButtonAction); JsonObject cardSectionButtonListButton = new JsonObject(); cardSectionButtonListButton.add("text", new JsonPrimitive("Create")); cardSectionButtonListButton.add("onClick", cardSectionButtonListButtonOnCLick); JsonArray cardSectionButtonListButtons = new JsonArray(); cardSectionButtonListButtons.add(cardSectionButtonListButton); JsonObject cardSectionButtonList = new JsonObject(); cardSectionButtonList.add("buttons", cardSectionButtonListButtons); JsonObject cardSectionButtonListWidget = new JsonObject(); cardSectionButtonListWidget.add("buttonList", cardSectionButtonList); // Builds the form inputs with error texts for invalid values. JsonArray cardSection = new JsonArray(); if (errors.containsKey("name")) { cardSection.add(createErrorTextParagraph(errors.get("name").toString())); } cardSection.add(cardSectionTextInput1Widget); if (errors.containsKey("description")) { cardSection.add(createErrorTextParagraph(errors.get("description").toString())); } cardSection.add(cardSectionTextInput2Widget); if (errors.containsKey("priority")) { cardSection.add(createErrorTextParagraph(errors.get("priority").toString())); } cardSection.add(cardSectionSelectionInput1Widget); if (errors.containsKey("impact")) { cardSection.add(createErrorTextParagraph(errors.get("impact").toString())); } cardSection.add(cardSectionSelectionInput2Widget); cardSection.add(cardSectionButtonListWidget); JsonObject cardSectionWidgets = new JsonObject(); cardSectionWidgets.add("widgets", cardSection); JsonArray sections = new JsonArray(); sections.add(cardSectionWidgets); JsonObject card = new JsonObject(); card.add("header", cardHeader); card.add("sections", sections); JsonObject navigation = new JsonObject(); if (isUpdate) { navigation.add("updateCard", card); } else { navigation.add("pushCard", card); } JsonArray navigations = new JsonArray(); navigations.add(navigation); JsonObject action = new JsonObject(); action.add("navigations", navigations); JsonObject renderActions = new JsonObject(); renderActions.add("action", action); if (!isUpdate) { return renderActions; } JsonObject update = new JsonObject(); update.add("renderActions", renderActions); return update; } /** * Submits the creation form. If valid, returns a render action * that inserts a new link into the document. If invalid, returns an * update card navigation that re-renders the creation form with error messages. * * @param event The event object with form input values. * @return The resulting response. */ JsonObject submitCaseCreationForm(JsonObject event) throws Exception { JsonObject formInputs = event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs"); Map<String, String> caseDetails = new HashMap<String, String>(); 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<String, String> errors = validateFormInputs(caseDetails); if (errors.size() > 0) { return createCaseInputCard(event, errors, /* isUpdate= */ true); } else { String title = String.format("Case %s", caseDetails.get("name")); // Adds the case details as parameters to the generated link URL. URIBuilder uriBuilder = new URIBuilder("https://example.com/support/cases/"); for (String caseDetailKey : caseDetails.keySet()) { uriBuilder.addParameter(caseDetailKey, caseDetails.get(caseDetailKey)); } return createLinkRenderAction(title, uriBuilder.build().toURL().toString()); } } /** * Validates case creation form input values. * * @param caseDetails The values of each form input submitted by the user. * @return A map from field name to error message. An empty object * represents a valid form submission. */ Map<String, String> validateFormInputs(Map<String, String> caseDetails) { Map<String, String> errors = new HashMap<String, String>(); if (!caseDetails.containsKey("name")) { errors.put("name", "You must provide a name"); } if (!caseDetails.containsKey("description")) { errors.put("description", "You must provide a description"); } if (!caseDetails.containsKey("priority")) { errors.put("priority", "You must provide a priority"); } if (caseDetails.containsKey("impact") && !Arrays.asList(new String[]{"P0", "P1"}).contains(caseDetails.get("priority"))) { errors.put("impact", "If an issue blocks a critical customer operation, priority must be P0 or P1"); } return errors; } /** * Returns a text paragraph with red text indicating a form field validation error. * * @param errorMessage A description of input value error. * @return The resulting text paragraph. */ JsonObject createErrorTextParagraph(String errorMessage) { JsonObject textParagraph = new JsonObject(); textParagraph.add("text", new JsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> " + errorMessage + "</font>")); JsonObject textParagraphWidget = new JsonObject(); textParagraphWidget.add("textParagraph", textParagraph); return textParagraphWidget; } /** * Returns a submit form response that inserts a link into the document. * * @param title The title of the link to insert. * @param url The URL of the link to insert. * @return The resulting submit form response. */ JsonObject createLinkRenderAction(String title, String url) { JsonObject link = new JsonObject(); link.add("title", new JsonPrimitive(title)); link.add("url", new JsonPrimitive(url)); JsonArray links = new JsonArray(); links.add(link); JsonObject action = new JsonObject(); action.add("links", links); JsonObject renderActions = new JsonObject(); renderActions.add("action", action); JsonObject linkRenderAction = new JsonObject(); linkRenderAction.add("renderActions", renderActions); return linkRenderAction; } }
Kode berikut menunjukkan cara menerapkan pratinjau link untuk resource yang dibuat:
/** * 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. */ import com.google.cloud.functions.HttpFunction; import com.google.cloud.functions.HttpRequest; import com.google.cloud.functions.HttpResponse; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.util.HashMap; import java.util.Map; public class CreateLinkPreview implements HttpFunction { private static final Gson gson = new Gson(); /** * Responds to any HTTP request related to link previews. * * @param request An HTTP request context. * @param response An HTTP response context. */ @Override public void service(HttpRequest request, HttpResponse response) throws Exception { JsonObject event = gson.fromJson(request.getReader(), JsonObject.class); String url = event.getAsJsonObject("docs") .getAsJsonObject("matchedUrl") .get("url") .getAsString(); URL parsedURL = new URL(url); // If the event object URL matches a specified pattern for preview links. if ("example.com".equals(parsedURL.getHost())) { if (parsedURL.getPath().startsWith("/support/cases/")) { response.getWriter().write(gson.toJson(caseLinkPreview(parsedURL))); return; } } response.getWriter().write("{}"); } /** * A support case link preview. * * @param url A matching URL. * @return The resulting preview link card. */ JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException { // Parses the URL and identify the case details. Map<String, String> caseDetails = new HashMap<String, String>(); for (String pair : url.getQuery().split("&")) { caseDetails.put(URLDecoder.decode(pair.split("=")[0], "UTF-8"), URLDecoder.decode(pair.split("=")[1], "UTF-8")); } // Builds a preview card with the case name, and description // Uses the text from the card's header for the title of the smart chip. JsonObject cardHeader = new JsonObject(); String caseName = String.format("Case %s", caseDetails.get("name")); cardHeader.add("title", new JsonPrimitive(caseName)); JsonObject textParagraph = new JsonObject(); textParagraph.add("text", new JsonPrimitive(caseDetails.get("description"))); JsonObject widget = new JsonObject(); widget.add("textParagraph", textParagraph); JsonArray widgets = new JsonArray(); widgets.add(widget); JsonObject section = new JsonObject(); section.add("widgets", widgets); JsonArray sections = new JsonArray(); sections.add(section); JsonObject previewCard = new JsonObject(); previewCard.add("header", cardHeader); previewCard.add("sections", sections); JsonObject linkPreview = new JsonObject(); linkPreview.add("title", new JsonPrimitive(caseName)); linkPreview.add("previewCard", previewCard); JsonObject action = new JsonObject(); action.add("linkPreview", linkPreview); JsonObject renderActions = new JsonObject(); renderActions.add("action", action); return renderActions; } }
Referensi terkait
- Melihat pratinjau link dengan smart chip
- Menguji add-on
- Manifes Google Dokumen
- Antarmuka kartu untuk pratinjau link