Esta página explica como criar um complemento do Google Workspace que permite que
os usuários do Documentos Google criem recursos, como um caso de suporte ou uma tarefa de projeto,
em um serviço de terceiros no Documentos Google.
Com um complemento do Google Workspace, você pode adicionar seu serviço ao menu "@" nos Documentos. O complemento adiciona itens de menu que permitem que os usuários criem
recursos no seu serviço usando uma caixa de diálogo de formulário no Documentos.
Como os usuários criam recursos
Para criar um recurso no seu serviço em um documento do Google Docs, os usuários
digitam @ em um documento e selecionam seu serviço no menu @:
Quando os usuários digitam @ em um documento e selecionam seu serviço, você mostra
um card que inclui as entradas de formulário necessárias para criar um
recurso. Depois que o usuário envia o formulário de criação de recursos, o complemento precisa
criar o recurso no serviço e gerar um URL que aponte para ele.
O complemento insere um ícone
no documento para o recurso criado. Quando os usuários mantêm o ponteiro sobre
esse ícone, ele invoca o acionador de visualização de link associado ao complemento. Verifique se o complemento insere ícones com padrões de link aceitos pelos acionadores de visualização de link.
Pré-requisitos
Apps Script
Um complemento do Google Workspace que oferece suporte a visualizações de link para os padrões
de link dos recursos criados pelos usuários. Para criar um complemento com visualizações de links, consulte
Visualizar links com ícones inteligentes.
Node.js
Um complemento do Google Workspace que oferece suporte a visualizações de link para os padrões
de link dos recursos criados pelos usuários. Para criar um complemento com prévias de links, consulte
Visualizar links com ícones inteligentes.
Python
Um complemento do Google Workspace que oferece suporte a visualizações de link para os padrões
de link dos recursos criados pelos usuários. Para criar um complemento com prévias de links, consulte
Visualizar links com ícones inteligentes.
Java
Um complemento do Google Workspace que oferece suporte a visualizações de link para os padrões
de link dos recursos criados pelos usuários. Para criar um complemento com visualizações de links, consulte
Visualizar links com ícones inteligentes.
Configurar a criação de recursos para o complemento
Esta seção explica como configurar a criação de recursos para seu complemento, o que inclui as seguintes etapas:
Para configurar a criação de recursos, especifique as seguintes seções e campos no
manifesto do complemento:
Na seção addOns do campo docs, implemente o gatilho createActionTriggers que inclui um runFunction. Você define
essa função na seção a seguir, Criar os cards de formulário.
No campo oauthScopes, adicione o escopo
https://www.googleapis.com/auth/workspace.linkcreate para que os usuários possam
autorizar o complemento a criar recursos.
Especificamente, esse escopo permite que o complemento leia as informações que os usuários enviam para o formulário de criação de recursos e insira um ícone inteligente no documento com base nessas informações.
Como exemplo, consulte a seção addons de um manifesto que configura
a criação de recursos para o seguinte serviço de caso de suporte:
{"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"}]}}}
No exemplo, o complemento do Google Workspace permite que os usuários criem casos de suporte.
Cada acionador createActionTriggers precisa ter os seguintes
campos:
Um ID exclusivo
Um rótulo de texto que aparece no menu "Documentos"
Um URL de logotipo que aponta para um ícone exibido ao lado do texto do rótulo no
menu @
Uma função de callback que faz referência a uma função do Apps Script ou a um endpoint HTTP que retorna um card
Criar os cards do formulário
Para criar recursos no seu serviço no menu @
do Documentos, é necessário implementar todas as funções
especificadas no objeto createActionTriggers.
Quando um usuário interage com um dos itens do menu, o gatilho
createActionTriggers correspondente é acionado, e a função de callback apresenta um card
com entradas de formulário para criar o recurso.
Elementos e ações aceitos
Para criar a interface do card, use widgets para mostrar informações e entradas
que os usuários precisam para criar o recurso. A maioria dos widgets e ações
de complementos do Google Workspace são compatíveis com as seguintes exceções:
Não é possível usar rodapés de cartão.
Não há suporte para notificações.
Para navegações, somente a navegação updateCard é aceita.
Exemplo de card com entradas de formulário
O exemplo a seguir mostra uma função de callback do Apps Script
que exibe um card quando um usuário seleciona Create support case no
menu @:
/** * 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. */functioncreateCaseInputCard(event,errors,isUpdate){constcardHeader=CardService.newCardHeader().setTitle('Createasupportcase')constcardSectionTextInput1=CardService.newTextInput().setFieldName('name').setTitle('Name').setMultiline(false);constcardSectionTextInput2=CardService.newTextInput().setFieldName('description').setTitle('Description').setMultiline(true);constcardSectionSelectionInput1=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);constcardSectionSelectionInput2=CardService.newSelectionInput().setFieldName('impact').setTitle('Impact').setType(CardService.SelectionInputType.CHECK_BOX).addItem('Blocksacriticalcustomeroperation','Blocksacriticalcustomeroperation',false);constcardSectionButtonListButtonAction=CardService.newAction().setPersistValues(true).setFunctionName('submitCaseCreationForm').setParameters({});constcardSectionButtonListButton=CardService.newTextButton().setText('Create').setTextButtonStyle(CardService.TextButtonStyle.TEXT).setOnClickAction(cardSectionButtonListButtonAction);constcardSectionButtonList=CardService.newButtonSet().addButton(cardSectionButtonListButton);// Builds the form inputs with error texts for invalid values.constcardSection=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);constcard=CardService.newCardBuilder().setHeader(cardHeader).addSection(cardSection).build();if(isUpdate){returnCardService.newActionResponseBuilder().setNavigation(CardService.newNavigation().updateCard(card)).build();}else{returncard;}}
/** * 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. */functioncreateCaseInputCard(event,errors,isUpdate){constcardHeader1={title:"Create a support case"};constcardSection1TextInput1={textInput:{name:"name",label:"Name"}};constcardSection1TextInput2={textInput:{name:"description",label:"Description",type:"MULTIPLE_LINE"}};constcardSection1SelectionInput1={selectionInput:{name:"priority",label:"Priority",type:"DROPDOWN",items:[{text:"P0",value:"P0"},{text:"P1",value:"P1"},{text:"P2",value:"P2"},{text:"P3",value:"P3"}]}};constcardSection1SelectionInput2={selectionInput:{name:"impact",label:"Impact",items:[{text:"Blocks a critical customer operation",value:"Blocks a critical customer operation"}]}};constcardSection1ButtonList1Button1Action1={function:process.env.URL,parameters:[{key:"submitCaseCreationForm",value:true}],persistValues:true};constcardSection1ButtonList1Button1={text:"Create",onClick:{action:cardSection1ButtonList1Button1Action1}};constcardSection1ButtonList1={buttonList:{buttons:[cardSection1ButtonList1Button1]}};// Builds the creation form and adds error text for invalid inputs.constcardSection1=[];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);constcard={header:cardHeader1,sections:[{widgets:cardSection1}]};if(isUpdate){return{renderActions:{action:{navigations:[{updateCard:card}]}}};}else{return{action:{navigations:[{pushCard:card}]}};}}
defcreate_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"inerrors:card_section1.append(create_error_text_paragraph(errors["name"]))card_section1.append(card_section1_text_input1)if"description"inerrors:card_section1.append(create_error_text_paragraph(errors["description"]))card_section1.append(card_section1_text_input2)if"priority"inerrors:card_section1.append(create_error_text_paragraph(errors["priority"]))card_section1.append(card_section1_selection_input1)if"impact"inerrors: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}]}ifisUpdate:return{"renderActions":{"action":{"navigations":[{"updateCard":card}]}}}else:return{"action":{"navigations":[{"pushCard":card}]}}
/** * Produces a support case creation form. * * @param event The event object. * @param errors A map of per-field error messages. * @param isUpdate Whether to return the form as an update card navigation. * @return The resulting card or action response. */JsonObjectcreateCaseInputCard(JsonObjectevent,Map<String,String>errors,booleanisUpdate){JsonObjectcardHeader=newJsonObject();cardHeader.add("title",newJsonPrimitive("Create a support case"));JsonObjectcardSectionTextInput1=newJsonObject();cardSectionTextInput1.add("name",newJsonPrimitive("name"));cardSectionTextInput1.add("label",newJsonPrimitive("Name"));JsonObjectcardSectionTextInput1Widget=newJsonObject();cardSectionTextInput1Widget.add("textInput",cardSectionTextInput1);JsonObjectcardSectionTextInput2=newJsonObject();cardSectionTextInput2.add("name",newJsonPrimitive("description"));cardSectionTextInput2.add("label",newJsonPrimitive("Description"));cardSectionTextInput2.add("type",newJsonPrimitive("MULTIPLE_LINE"));JsonObjectcardSectionTextInput2Widget=newJsonObject();cardSectionTextInput2Widget.add("textInput",cardSectionTextInput2);JsonObjectcardSectionSelectionInput1ItemsItem1=newJsonObject();cardSectionSelectionInput1ItemsItem1.add("text",newJsonPrimitive("P0"));cardSectionSelectionInput1ItemsItem1.add("value",newJsonPrimitive("P0"));JsonObjectcardSectionSelectionInput1ItemsItem2=newJsonObject();cardSectionSelectionInput1ItemsItem2.add("text",newJsonPrimitive("P1"));cardSectionSelectionInput1ItemsItem2.add("value",newJsonPrimitive("P1"));JsonObjectcardSectionSelectionInput1ItemsItem3=newJsonObject();cardSectionSelectionInput1ItemsItem3.add("text",newJsonPrimitive("P2"));cardSectionSelectionInput1ItemsItem3.add("value",newJsonPrimitive("P2"));JsonObjectcardSectionSelectionInput1ItemsItem4=newJsonObject();cardSectionSelectionInput1ItemsItem4.add("text",newJsonPrimitive("P3"));cardSectionSelectionInput1ItemsItem4.add("value",newJsonPrimitive("P3"));JsonArraycardSectionSelectionInput1Items=newJsonArray();cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);JsonObjectcardSectionSelectionInput1=newJsonObject();cardSectionSelectionInput1.add("name",newJsonPrimitive("priority"));cardSectionSelectionInput1.add("label",newJsonPrimitive("Priority"));cardSectionSelectionInput1.add("type",newJsonPrimitive("DROPDOWN"));cardSectionSelectionInput1.add("items",cardSectionSelectionInput1Items);JsonObjectcardSectionSelectionInput1Widget=newJsonObject();cardSectionSelectionInput1Widget.add("selectionInput",cardSectionSelectionInput1);JsonObjectcardSectionSelectionInput2ItemsItem=newJsonObject();cardSectionSelectionInput2ItemsItem.add("text",newJsonPrimitive("Blocks a critical customer operation"));cardSectionSelectionInput2ItemsItem.add("value",newJsonPrimitive("Blocks a critical customer operation"));JsonArraycardSectionSelectionInput2Items=newJsonArray();cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);JsonObjectcardSectionSelectionInput2=newJsonObject();cardSectionSelectionInput2.add("name",newJsonPrimitive("impact"));cardSectionSelectionInput2.add("label",newJsonPrimitive("Impact"));cardSectionSelectionInput2.add("items",cardSectionSelectionInput2Items);JsonObjectcardSectionSelectionInput2Widget=newJsonObject();cardSectionSelectionInput2Widget.add("selectionInput",cardSectionSelectionInput2);JsonObjectcardSectionButtonListButtonActionParametersParameter=newJsonObject();cardSectionButtonListButtonActionParametersParameter.add("key",newJsonPrimitive("submitCaseCreationForm"));cardSectionButtonListButtonActionParametersParameter.add("value",newJsonPrimitive(true));JsonArraycardSectionButtonListButtonActionParameters=newJsonArray();cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);JsonObjectcardSectionButtonListButtonAction=newJsonObject();cardSectionButtonListButtonAction.add("function",newJsonPrimitive(System.getenv().get("URL")));cardSectionButtonListButtonAction.add("parameters",cardSectionButtonListButtonActionParameters);cardSectionButtonListButtonAction.add("persistValues",newJsonPrimitive(true));JsonObjectcardSectionButtonListButtonOnCLick=newJsonObject();cardSectionButtonListButtonOnCLick.add("action",cardSectionButtonListButtonAction);JsonObjectcardSectionButtonListButton=newJsonObject();cardSectionButtonListButton.add("text",newJsonPrimitive("Create"));cardSectionButtonListButton.add("onClick",cardSectionButtonListButtonOnCLick);JsonArraycardSectionButtonListButtons=newJsonArray();cardSectionButtonListButtons.add(cardSectionButtonListButton);JsonObjectcardSectionButtonList=newJsonObject();cardSectionButtonList.add("buttons",cardSectionButtonListButtons);JsonObjectcardSectionButtonListWidget=newJsonObject();cardSectionButtonListWidget.add("buttonList",cardSectionButtonList);// Builds the form inputs with error texts for invalid values.JsonArraycardSection=newJsonArray();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);JsonObjectcardSectionWidgets=newJsonObject();cardSectionWidgets.add("widgets",cardSection);JsonArraysections=newJsonArray();sections.add(cardSectionWidgets);JsonObjectcard=newJsonObject();card.add("header",cardHeader);card.add("sections",sections);JsonObjectnavigation=newJsonObject();if(isUpdate){navigation.add("updateCard",card);}else{navigation.add("pushCard",card);}JsonArraynavigations=newJsonArray();navigations.add(navigation);JsonObjectaction=newJsonObject();action.add("navigations",navigations);JsonObjectrenderActions=newJsonObject();renderActions.add("action",action);if(!isUpdate){returnrenderActions;}JsonObjectupdate=newJsonObject();update.add("renderActions",renderActions);returnupdate;}
A função createCaseInputCard renderiza o seguinte card:
O card inclui entradas de texto, um menu suspenso e uma caixa de seleção. Ele também tem um
botão de texto com uma ação onClick que executa outra função para
processar o
envio do formulário de criação.
Depois que o usuário preenche o formulário e clica em
Criar, o complemento envia as entradas do formulário para a função de ação onClick,
chamada de submitCaseCreationForm no nosso exemplo. Nesse ponto, o complemento pode validar as entradas e usá-las para
criar o recurso no serviço de terceiros.
Processar envios de formulário
Depois que um usuário envia o formulário de criação, a função associada à ação
onClick é executada. Para uma experiência ideal do usuário, seu
complemento precisa processar
envios de formulários bem-sucedidos e com erros.
Processar a criação de recursos bem-sucedida
A função onClick do seu complemento precisa criar o
recurso no serviço de terceiros e gerar um URL que aponte para ele.
Para comunicar o URL do recurso de volta aos Documentos
para a criação de ícones, a função onClick precisa retornar um SubmitFormResponse
com uma matriz de um elemento em renderActions.action.links que aponta para um
link. O título do link precisa representar o título do recurso criado, e o
URL precisa apontar para esse recurso.
O exemplo a seguir mostra uma SubmitFormResponse para um recurso criado:
/** * 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. */functioncreateLinkRenderAction(title,url){return{renderActions:{action:{links:[{title:title,url:url}]}}};}
/** * 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. */functioncreateLinkRenderAction(title,url){return{renderActions:{action:{links:[{title:title,url:url}]}}};}
defcreate_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}]}}}
/** * 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. */JsonObjectcreateLinkRenderAction(Stringtitle,Stringurl){JsonObjectlink=newJsonObject();link.add("title",newJsonPrimitive(title));link.add("url",newJsonPrimitive(url));JsonArraylinks=newJsonArray();links.add(link);JsonObjectaction=newJsonObject();action.add("links",links);JsonObjectrenderActions=newJsonObject();renderActions.add("action",action);JsonObjectlinkRenderAction=newJsonObject();linkRenderAction.add("renderActions",renderActions);returnlinkRenderAction;}
Depois que o SubmitFormResponse é retornado, a caixa de diálogo modal é fechada e o
complemento insere um ícone no documento.
Quando os usuários mantêm o ponteiro sobre esse ícone, ele invoca o acionador de visualização
de link associado. Verifique se o complemento não insere
cards com padrões de link que não são compatíveis com os acionadores de visualização de link.
Solucionar erros
Se um usuário tentar enviar um formulário com campos inválidos, em vez de retornar um
SubmitFormResponse com um link, o complemento vai
retornar uma ação de renderização que mostra um erro usando uma navegação updateCard.
Isso permite que o usuário veja o que fez
errado e tente novamente. Consulte
updateCard(card)
para
o Apps Script e updateCard
para outros ambientes de execução. As notificações e as navegações pushCard não são compatíveis.
Exemplo de tratamento de erros
O exemplo a seguir mostra o código que é invocado quando um usuário envia o
formulário. Se as entradas forem
inválidas, o cartão será atualizado e mostrará mensagens de erro. Se as entradas forem válidas,
o complemento vai retornar um SubmitFormResponse com um
link para o recurso criado.
/** * 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. */functionsubmitCaseCreationForm(event){constcaseDetails={name:event.formInput.name,description:event.formInput.description,priority:event.formInput.priority,impact:!!event.formInput.impact,};consterrors=validateFormInputs(caseDetails);if(Object.keys(errors).length > 0){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{consttitle=`Case${caseDetails.name}`;// Adds the case details as parameters to the generated link URL.consturl='https://example.com/support/cases/?' + generateQuery(caseDetails);returncreateLinkRenderAction(title,url);}}/*** Build a query path with URL parameters.** @param {!Map} parameters A map with the URL parameters.* @return {!string} The resulting query path.*/functiongenerateQuery(parameters){returnObject.entries(parameters).flatMap(([k,v])=>
Array.isArray(v)?v.map(e=>`${k}=${encodeURIComponent(e)}`):`${k}=${encodeURIComponent(v)}`).join("&");}
/** * 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. */functionsubmitCaseCreationForm(event){constcaseDetails={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],};consterrors=validateFormInputs(caseDetails);if(Object.keys(errors).length > 0){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{consttitle=`Case ${caseDetails.name}`;// Adds the case details as parameters to the generated link URL.consturl=newURL('https://example.com/support/cases/');for(const[key,value]ofObject.entries(caseDetails)){url.searchParams.append(key,value);}returncreateLinkRenderAction(title,url.href);}}
defsubmit_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"inevent["commonEventObject"]elseNonecase_details={"name":None,"description":None,"priority":None,"impact":None,}ifformInputsisnotNone:case_details["name"]=formInputs["name"]["stringInputs"]["value"][0]if"name"informInputselseNonecase_details["description"]=formInputs["description"]["stringInputs"]["value"][0]if"description"informInputselseNonecase_details["priority"]=formInputs["priority"]["stringInputs"]["value"][0]if"priority"informInputselseNonecase_details["impact"]=formInputs["impact"]["stringInputs"]["value"][0]if"impact"informInputselseFalseerrors=validate_form_inputs(case_details)iflen(errors) > 0:returncreate_case_input_card(event,errors,True)# Update modeelse: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)returncreate_link_render_action(title,url)
/** * 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. */JsonObjectsubmitCaseCreationForm(JsonObjectevent)throwsException{JsonObjectformInputs=event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");Map<String,String>caseDetails=newHashMap<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){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{Stringtitle=String.format("Case %s",caseDetails.get("name"));// Adds the case details as parameters to the generated link URL.URIBuilderuriBuilder=newURIBuilder("https://example.com/support/cases/");for(StringcaseDetailKey:caseDetails.keySet()){uriBuilder.addParameter(caseDetailKey,caseDetails.get(caseDetailKey));}returncreateLinkRenderAction(title,uriBuilder.build().toURL().toString());}}
O exemplo de código abaixo valida as entradas do formulário e cria mensagens de erro
para entradas inválidas:
/** * 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. */functionvalidateFormInputs(caseDetails){consterrors={};if(!caseDetails.name){errors.name='Youmustprovideaname';}if(!caseDetails.description){errors.description='Youmustprovideadescription';}if(!caseDetails.priority){errors.priority='Youmustprovideapriority';}if(caseDetails.impact && caseDetails.priority!=='P0' && caseDetails.priority!=='P1'){errors.impact='Ifanissueblocksacriticalcustomeroperation,prioritymustbeP0orP1';}returnerrors;}/** * 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. */functioncreateErrorTextParagraph(errorMessage){returnCardService.newTextParagraph().setText('<fontcolor=\"#BA0300\"><b>Error:</b>'+errorMessage+'</font>');}
/** * 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. */functionvalidateFormInputs(caseDetails){consterrors={};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';}returnerrors;}/** * 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. */functioncreateErrorTextParagraph(errorMessage){return{textParagraph:{text:'<font color=\"#BA0300\"><b>Error:</b> '+errorMessage+'</font>'}}}
defvalidate_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={}ifcase_details["name"]isNone:errors["name"]="You must provide a name"ifcase_details["description"]isNone:errors["description"]="You must provide a description"ifcase_details["priority"]isNone:errors["priority"]="You must provide a priority"ifcase_details["impact"]isnotNoneandcase_details["priority"]notin['P0','P1']:errors["impact"]="If an issue blocks a critical customer operation, priority must be P0 or P1"returnerrorsdefcreate_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>'}}
/** * 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=newHashMap<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(newString[]{"P0","P1"}).contains(caseDetails.get("priority"))){errors.put("impact","If an issue blocks a critical customer operation, priority must be P0 or P1");}returnerrors;}/** * 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. */JsonObjectcreateErrorTextParagraph(StringerrorMessage){JsonObjecttextParagraph=newJsonObject();textParagraph.add("text",newJsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> "+errorMessage+"</font>"));JsonObjecttextParagraphWidget=newJsonObject();textParagraphWidget.add("textParagraph",textParagraph);returntextParagraphWidget;}
Exemplo completo: complemento de caso de suporte
O exemplo a seguir mostra um complemento do Google Workspace que mostra uma prévia de links
para os casos de suporte de uma empresa e permite que os usuários criem casos de suporte no
Documentos Google.
O exemplo faz o seguinte:
Gera um card com campos de formulário para criar um caso de suporte no menu "Docs @".
Valida as entradas de formulário e retorna mensagens de erro para entradas inválidas.
Insere o nome e o link do caso de suporte criado no
documento do Documentos como um ícone inteligente.
Mostra uma prévia do link do caso de suporte, como
https://www.example.com/support/cases/1234. O ícone inteligente mostra um ícone, e o card de visualização inclui o nome, a prioridade e a descrição do caso.
{"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"}]}}}
{"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"}]}}}
/** * 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.*/functioncaseLinkPreview(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.constcaseDetails=parseQuery(event.docs.matchedUrl.url);// Builds a preview card with the case name, and descriptionconstcaseHeader=CardService.newCardHeader().setTitle(`Case${caseDetails["name"][0]}`);constcaseDescription=CardService.newTextParagraph().setText(caseDetails["description"][0]);// Returns the card.// Uses the text from the card's header for the title of the smart chip.returnCardService.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.*/functionparseQuery(url){constquery=url.split("?")[1];if(query){returnquery.split("&").reduce(function(o,e){vartemp=e.split("=");varkey=temp[0].trim();varvalue=temp[1].trim();value=isNaN(value)?value:Number(value);if(o[key]){o[key].push(value);}else{o[key]=[value];}returno;},{});}returnnull;}/** * 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. */functioncreateCaseInputCard(event,errors,isUpdate){constcardHeader=CardService.newCardHeader().setTitle('Createasupportcase')constcardSectionTextInput1=CardService.newTextInput().setFieldName('name').setTitle('Name').setMultiline(false);constcardSectionTextInput2=CardService.newTextInput().setFieldName('description').setTitle('Description').setMultiline(true);constcardSectionSelectionInput1=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);constcardSectionSelectionInput2=CardService.newSelectionInput().setFieldName('impact').setTitle('Impact').setType(CardService.SelectionInputType.CHECK_BOX).addItem('Blocksacriticalcustomeroperation','Blocksacriticalcustomeroperation',false);constcardSectionButtonListButtonAction=CardService.newAction().setPersistValues(true).setFunctionName('submitCaseCreationForm').setParameters({});constcardSectionButtonListButton=CardService.newTextButton().setText('Create').setTextButtonStyle(CardService.TextButtonStyle.TEXT).setOnClickAction(cardSectionButtonListButtonAction);constcardSectionButtonList=CardService.newButtonSet().addButton(cardSectionButtonListButton);// Builds the form inputs with error texts for invalid values.constcardSection=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);constcard=CardService.newCardBuilder().setHeader(cardHeader).addSection(cardSection).build();if(isUpdate){returnCardService.newActionResponseBuilder().setNavigation(CardService.newNavigation().updateCard(card)).build();}else{returncard;}}/** * 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. */functionsubmitCaseCreationForm(event){constcaseDetails={name:event.formInput.name,description:event.formInput.description,priority:event.formInput.priority,impact:!!event.formInput.impact,};consterrors=validateFormInputs(caseDetails);if(Object.keys(errors).length > 0){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{consttitle=`Case${caseDetails.name}`;// Adds the case details as parameters to the generated link URL.consturl='https://example.com/support/cases/?' + generateQuery(caseDetails);returncreateLinkRenderAction(title,url);}}/*** Build a query path with URL parameters.** @param {!Map} parameters A map with the URL parameters.* @return {!string} The resulting query path.*/functiongenerateQuery(parameters){returnObject.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. */functionvalidateFormInputs(caseDetails){consterrors={};if(!caseDetails.name){errors.name='Youmustprovideaname';}if(!caseDetails.description){errors.description='Youmustprovideadescription';}if(!caseDetails.priority){errors.priority='Youmustprovideapriority';}if(caseDetails.impact && caseDetails.priority!=='P0' && caseDetails.priority!=='P1'){errors.impact='Ifanissueblocksacriticalcustomeroperation,prioritymustbeP0orP1';}returnerrors;}/** * 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. */functioncreateErrorTextParagraph(errorMessage){returnCardService.newTextParagraph().setText('<fontcolor=\"#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. */functioncreateLinkRenderAction(title,url){return{renderActions:{action:{links:[{title:title,url:url}]}}};}
/** * 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)=>{constevent=req.body;if(event.docs.matchedUrl.url){consturl=event.docs.matchedUrl.url;constparsedUrl=newURL(url);// If the event object URL matches a specified pattern for preview links.if(parsedUrl.hostname==='example.com'){if(parsedUrl.pathname.startsWith('/support/cases/')){returnres.json(caseLinkPreview(parsedUrl));}}}};/** * * A support case link preview. * * @param {!URL} url The event object. * @return {!Card} The resulting preview link card. */functioncaseLinkPreview(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.constname=`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)=>{constevent=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. */functioncreateCaseInputCard(event,errors,isUpdate){constcardHeader1={title:"Create a support case"};constcardSection1TextInput1={textInput:{name:"name",label:"Name"}};constcardSection1TextInput2={textInput:{name:"description",label:"Description",type:"MULTIPLE_LINE"}};constcardSection1SelectionInput1={selectionInput:{name:"priority",label:"Priority",type:"DROPDOWN",items:[{text:"P0",value:"P0"},{text:"P1",value:"P1"},{text:"P2",value:"P2"},{text:"P3",value:"P3"}]}};constcardSection1SelectionInput2={selectionInput:{name:"impact",label:"Impact",items:[{text:"Blocks a critical customer operation",value:"Blocks a critical customer operation"}]}};constcardSection1ButtonList1Button1Action1={function:process.env.URL,parameters:[{key:"submitCaseCreationForm",value:true}],persistValues:true};constcardSection1ButtonList1Button1={text:"Create",onClick:{action:cardSection1ButtonList1Button1Action1}};constcardSection1ButtonList1={buttonList:{buttons:[cardSection1ButtonList1Button1]}};// Builds the creation form and adds error text for invalid inputs.constcardSection1=[];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);constcard={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. */functionsubmitCaseCreationForm(event){constcaseDetails={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],};consterrors=validateFormInputs(caseDetails);if(Object.keys(errors).length > 0){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{consttitle=`Case ${caseDetails.name}`;// Adds the case details as parameters to the generated link URL.consturl=newURL('https://example.com/support/cases/');for(const[key,value]ofObject.entries(caseDetails)){url.searchParams.append(key,value);}returncreateLinkRenderAction(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. */functionvalidateFormInputs(caseDetails){consterrors={};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';}returnerrors;}/** * 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. */functioncreateErrorTextParagraph(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. */functioncreateLinkRenderAction(title,url){return{renderActions:{action:{links:[{title:title,url:url}]}}};}
# 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.fromtypingimportAny,Mappingfromurllib.parseimporturlencodeimportosimportflaskimportfunctions_framework@functions_framework.httpdefcreate_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"inevent["commonEventObject"]elseNoneifparametersisnotNoneandparameters["submitCaseCreationForm"]:returnsubmit_case_creation_form(event)else:returncreate_case_input_card(event)defcreate_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"inerrors:card_section1.append(create_error_text_paragraph(errors["name"]))card_section1.append(card_section1_text_input1)if"description"inerrors:card_section1.append(create_error_text_paragraph(errors["description"]))card_section1.append(card_section1_text_input2)if"priority"inerrors:card_section1.append(create_error_text_paragraph(errors["priority"]))card_section1.append(card_section1_selection_input1)if"impact"inerrors: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}]}ifisUpdate:return{"renderActions":{"action":{"navigations":[{"updateCard":card}]}}}else:return{"action":{"navigations":[{"pushCard":card}]}}defsubmit_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"inevent["commonEventObject"]elseNonecase_details={"name":None,"description":None,"priority":None,"impact":None,}ifformInputsisnotNone:case_details["name"]=formInputs["name"]["stringInputs"]["value"][0]if"name"informInputselseNonecase_details["description"]=formInputs["description"]["stringInputs"]["value"][0]if"description"informInputselseNonecase_details["priority"]=formInputs["priority"]["stringInputs"]["value"][0]if"priority"informInputselseNonecase_details["impact"]=formInputs["impact"]["stringInputs"]["value"][0]if"impact"informInputselseFalseerrors=validate_form_inputs(case_details)iflen(errors) > 0:returncreate_case_input_card(event,errors,True)# Update modeelse: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)returncreate_link_render_action(title,url)defvalidate_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={}ifcase_details["name"]isNone:errors["name"]="You must provide a name"ifcase_details["description"]isNone:errors["description"]="You must provide a description"ifcase_details["priority"]isNone:errors["priority"]="You must provide a priority"ifcase_details["impact"]isnotNoneandcase_details["priority"]notin['P0','P1']:errors["impact"]="If an issue blocks a critical customer operation, priority must be P0 or P1"returnerrorsdefcreate_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>'}}defcreate_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}]}}}
O código a seguir mostra como implementar uma visualização de link para o recurso
criado:
# 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.fromtypingimportAny,Mappingfromurllib.parseimporturlparse,parse_qsimportflaskimportfunctions_framework@functions_framework.httpdefcreate_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)ifevent["docs"]["matchedUrl"]["url"]:url=event["docs"]["matchedUrl"]["url"]parsed_url=urlparse(url)# If the event object URL matches a specified pattern for preview links.ifparsed_url.hostname=="example.com":ifparsed_url.path.startswith("/support/cases/"):returncase_link_preview(parsed_url)return{}defcase_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]}}]}],}}}}
/** * 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. */importjava.util.Arrays;importjava.util.HashMap;importjava.util.Map;importorg.apache.http.client.utils.URIBuilder;importcom.google.cloud.functions.HttpFunction;importcom.google.cloud.functions.HttpRequest;importcom.google.cloud.functions.HttpResponse;importcom.google.gson.Gson;importcom.google.gson.JsonArray;importcom.google.gson.JsonObject;importcom.google.gson.JsonPrimitive;publicclassCreate3pResourcesimplementsHttpFunction{privatestaticfinalGsongson=newGson();/** * Responds to any HTTP request related to 3p resource creations. * * @param request An HTTP request context. * @param response An HTTP response context. */@Overridepublicvoidservice(HttpRequestrequest,HttpResponseresponse)throwsException{JsonObjectevent=gson.fromJson(request.getReader(),JsonObject.class);JsonObjectparameters=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,newHashMap<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. */JsonObjectcreateCaseInputCard(JsonObjectevent,Map<String,String>errors,booleanisUpdate){JsonObjectcardHeader=newJsonObject();cardHeader.add("title",newJsonPrimitive("Create a support case"));JsonObjectcardSectionTextInput1=newJsonObject();cardSectionTextInput1.add("name",newJsonPrimitive("name"));cardSectionTextInput1.add("label",newJsonPrimitive("Name"));JsonObjectcardSectionTextInput1Widget=newJsonObject();cardSectionTextInput1Widget.add("textInput",cardSectionTextInput1);JsonObjectcardSectionTextInput2=newJsonObject();cardSectionTextInput2.add("name",newJsonPrimitive("description"));cardSectionTextInput2.add("label",newJsonPrimitive("Description"));cardSectionTextInput2.add("type",newJsonPrimitive("MULTIPLE_LINE"));JsonObjectcardSectionTextInput2Widget=newJsonObject();cardSectionTextInput2Widget.add("textInput",cardSectionTextInput2);JsonObjectcardSectionSelectionInput1ItemsItem1=newJsonObject();cardSectionSelectionInput1ItemsItem1.add("text",newJsonPrimitive("P0"));cardSectionSelectionInput1ItemsItem1.add("value",newJsonPrimitive("P0"));JsonObjectcardSectionSelectionInput1ItemsItem2=newJsonObject();cardSectionSelectionInput1ItemsItem2.add("text",newJsonPrimitive("P1"));cardSectionSelectionInput1ItemsItem2.add("value",newJsonPrimitive("P1"));JsonObjectcardSectionSelectionInput1ItemsItem3=newJsonObject();cardSectionSelectionInput1ItemsItem3.add("text",newJsonPrimitive("P2"));cardSectionSelectionInput1ItemsItem3.add("value",newJsonPrimitive("P2"));JsonObjectcardSectionSelectionInput1ItemsItem4=newJsonObject();cardSectionSelectionInput1ItemsItem4.add("text",newJsonPrimitive("P3"));cardSectionSelectionInput1ItemsItem4.add("value",newJsonPrimitive("P3"));JsonArraycardSectionSelectionInput1Items=newJsonArray();cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem1);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem2);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem3);cardSectionSelectionInput1Items.add(cardSectionSelectionInput1ItemsItem4);JsonObjectcardSectionSelectionInput1=newJsonObject();cardSectionSelectionInput1.add("name",newJsonPrimitive("priority"));cardSectionSelectionInput1.add("label",newJsonPrimitive("Priority"));cardSectionSelectionInput1.add("type",newJsonPrimitive("DROPDOWN"));cardSectionSelectionInput1.add("items",cardSectionSelectionInput1Items);JsonObjectcardSectionSelectionInput1Widget=newJsonObject();cardSectionSelectionInput1Widget.add("selectionInput",cardSectionSelectionInput1);JsonObjectcardSectionSelectionInput2ItemsItem=newJsonObject();cardSectionSelectionInput2ItemsItem.add("text",newJsonPrimitive("Blocks a critical customer operation"));cardSectionSelectionInput2ItemsItem.add("value",newJsonPrimitive("Blocks a critical customer operation"));JsonArraycardSectionSelectionInput2Items=newJsonArray();cardSectionSelectionInput2Items.add(cardSectionSelectionInput2ItemsItem);JsonObjectcardSectionSelectionInput2=newJsonObject();cardSectionSelectionInput2.add("name",newJsonPrimitive("impact"));cardSectionSelectionInput2.add("label",newJsonPrimitive("Impact"));cardSectionSelectionInput2.add("items",cardSectionSelectionInput2Items);JsonObjectcardSectionSelectionInput2Widget=newJsonObject();cardSectionSelectionInput2Widget.add("selectionInput",cardSectionSelectionInput2);JsonObjectcardSectionButtonListButtonActionParametersParameter=newJsonObject();cardSectionButtonListButtonActionParametersParameter.add("key",newJsonPrimitive("submitCaseCreationForm"));cardSectionButtonListButtonActionParametersParameter.add("value",newJsonPrimitive(true));JsonArraycardSectionButtonListButtonActionParameters=newJsonArray();cardSectionButtonListButtonActionParameters.add(cardSectionButtonListButtonActionParametersParameter);JsonObjectcardSectionButtonListButtonAction=newJsonObject();cardSectionButtonListButtonAction.add("function",newJsonPrimitive(System.getenv().get("URL")));cardSectionButtonListButtonAction.add("parameters",cardSectionButtonListButtonActionParameters);cardSectionButtonListButtonAction.add("persistValues",newJsonPrimitive(true));JsonObjectcardSectionButtonListButtonOnCLick=newJsonObject();cardSectionButtonListButtonOnCLick.add("action",cardSectionButtonListButtonAction);JsonObjectcardSectionButtonListButton=newJsonObject();cardSectionButtonListButton.add("text",newJsonPrimitive("Create"));cardSectionButtonListButton.add("onClick",cardSectionButtonListButtonOnCLick);JsonArraycardSectionButtonListButtons=newJsonArray();cardSectionButtonListButtons.add(cardSectionButtonListButton);JsonObjectcardSectionButtonList=newJsonObject();cardSectionButtonList.add("buttons",cardSectionButtonListButtons);JsonObjectcardSectionButtonListWidget=newJsonObject();cardSectionButtonListWidget.add("buttonList",cardSectionButtonList);// Builds the form inputs with error texts for invalid values.JsonArraycardSection=newJsonArray();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);JsonObjectcardSectionWidgets=newJsonObject();cardSectionWidgets.add("widgets",cardSection);JsonArraysections=newJsonArray();sections.add(cardSectionWidgets);JsonObjectcard=newJsonObject();card.add("header",cardHeader);card.add("sections",sections);JsonObjectnavigation=newJsonObject();if(isUpdate){navigation.add("updateCard",card);}else{navigation.add("pushCard",card);}JsonArraynavigations=newJsonArray();navigations.add(navigation);JsonObjectaction=newJsonObject();action.add("navigations",navigations);JsonObjectrenderActions=newJsonObject();renderActions.add("action",action);if(!isUpdate){returnrenderActions;}JsonObjectupdate=newJsonObject();update.add("renderActions",renderActions);returnupdate;}/** * 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. */JsonObjectsubmitCaseCreationForm(JsonObjectevent)throwsException{JsonObjectformInputs=event.getAsJsonObject("commonEventObject").getAsJsonObject("formInputs");Map<String,String>caseDetails=newHashMap<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){returncreateCaseInputCard(event,errors,/* isUpdate= */true);}else{Stringtitle=String.format("Case %s",caseDetails.get("name"));// Adds the case details as parameters to the generated link URL.URIBuilderuriBuilder=newURIBuilder("https://example.com/support/cases/");for(StringcaseDetailKey:caseDetails.keySet()){uriBuilder.addParameter(caseDetailKey,caseDetails.get(caseDetailKey));}returncreateLinkRenderAction(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=newHashMap<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(newString[]{"P0","P1"}).contains(caseDetails.get("priority"))){errors.put("impact","If an issue blocks a critical customer operation, priority must be P0 or P1");}returnerrors;}/** * 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. */JsonObjectcreateErrorTextParagraph(StringerrorMessage){JsonObjecttextParagraph=newJsonObject();textParagraph.add("text",newJsonPrimitive("<font color=\"#BA0300\"><b>Error:</b> "+errorMessage+"</font>"));JsonObjecttextParagraphWidget=newJsonObject();textParagraphWidget.add("textParagraph",textParagraph);returntextParagraphWidget;}/** * 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. */JsonObjectcreateLinkRenderAction(Stringtitle,Stringurl){JsonObjectlink=newJsonObject();link.add("title",newJsonPrimitive(title));link.add("url",newJsonPrimitive(url));JsonArraylinks=newJsonArray();links.add(link);JsonObjectaction=newJsonObject();action.add("links",links);JsonObjectrenderActions=newJsonObject();renderActions.add("action",action);JsonObjectlinkRenderAction=newJsonObject();linkRenderAction.add("renderActions",renderActions);returnlinkRenderAction;}}
O código a seguir mostra como implementar uma visualização de link para o recurso
criado:
/** * 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. */importcom.google.cloud.functions.HttpFunction;importcom.google.cloud.functions.HttpRequest;importcom.google.cloud.functions.HttpResponse;importcom.google.gson.Gson;importcom.google.gson.JsonArray;importcom.google.gson.JsonObject;importcom.google.gson.JsonPrimitive;importjava.io.UnsupportedEncodingException;importjava.net.URL;importjava.net.URLDecoder;importjava.util.HashMap;importjava.util.Map;publicclassCreateLinkPreviewimplementsHttpFunction{privatestaticfinalGsongson=newGson();/** * Responds to any HTTP request related to link previews. * * @param request An HTTP request context. * @param response An HTTP response context. */@Overridepublicvoidservice(HttpRequestrequest,HttpResponseresponse)throwsException{JsonObjectevent=gson.fromJson(request.getReader(),JsonObject.class);Stringurl=event.getAsJsonObject("docs").getAsJsonObject("matchedUrl").get("url").getAsString();URLparsedURL=newURL(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. */JsonObjectcaseLinkPreview(URLurl)throwsUnsupportedEncodingException{// Parses the URL and identify the case details.Map<String,String>caseDetails=newHashMap<String,String>();for(Stringpair: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.JsonObjectcardHeader=newJsonObject();StringcaseName=String.format("Case %s",caseDetails.get("name"));cardHeader.add("title",newJsonPrimitive(caseName));JsonObjecttextParagraph=newJsonObject();textParagraph.add("text",newJsonPrimitive(caseDetails.get("description")));JsonObjectwidget=newJsonObject();widget.add("textParagraph",textParagraph);JsonArraywidgets=newJsonArray();widgets.add(widget);JsonObjectsection=newJsonObject();section.add("widgets",widgets);JsonArraysections=newJsonArray();sections.add(section);JsonObjectpreviewCard=newJsonObject();previewCard.add("header",cardHeader);previewCard.add("sections",sections);JsonObjectlinkPreview=newJsonObject();linkPreview.add("title",newJsonPrimitive(caseName));linkPreview.add("previewCard",previewCard);JsonObjectaction=newJsonObject();action.add("linkPreview",linkPreview);JsonObjectrenderActions=newJsonObject();renderActions.add("action",action);returnrenderActions;}}
[null,null,["Última atualização 2024-12-18 UTC."],[[["This guide details building a Google Workspace add-on to create and manage external resources (like support cases) directly within Google Docs."],["Users can create resources via a form within Docs, which then inserts a smart chip linking to the resource in the external service."],["The add-on requires configuration in the manifest file and utilizes Apps Script, Node.js, Python, or Java for development."],["Comprehensive code samples are provided to guide developers through card creation, form submission, and error handling."],["Smart chips representing the created resources offer link previews, enhancing user experience and information access."]]],[]]