Créer des ressources tierces à partir du menu @

Cette page explique comment créer un module complémentaire Google Workspace permettant aux utilisateurs de Google Docs de créer des ressources, telles qu'une demande d'assistance ou une tâche de projet, dans un service tiers à partir de Google Docs.

Un module complémentaire Google Workspace vous permet d'ajouter votre service au menu @ dans Docs. Le module complémentaire ajoute des éléments de menu qui permettent aux utilisateurs de créer des ressources dans votre service via une boîte de dialogue de formulaire dans Docs.

Comment les utilisateurs créent-ils des ressources ?

Pour créer une ressource dans votre service à partir d'un document Google Docs, les utilisateurs saisissent @ dans un document et sélectionnent votre service dans le menu @ :

L'utilisateur affiche un aperçu d'une carte.

Lorsque les utilisateurs saisissent @ dans un document et sélectionnent votre service, vous leur présentez une fiche comprenant les entrées de formulaire dont les utilisateurs ont besoin pour créer une ressource. Une fois que l'utilisateur a envoyé le formulaire de création de ressource, votre module complémentaire doit créer la ressource dans votre service et générer une URL qui pointe vers celle-ci.

Le module complémentaire insère un chip dans le document pour la ressource créée. Lorsque les utilisateurs maintiennent le pointeur sur ce chip, celui-ci appelle le déclencheur d'aperçu du lien associé au module complémentaire. Assurez-vous que votre module complémentaire insère des chips dont les formats de lien sont compatibles avec les déclencheurs d'aperçu de votre lien.

Prérequis

Apps Script ;

  • Module complémentaire Google Workspace qui accepte les aperçus de lien pour les formats de lien des ressources créées par les utilisateurs Pour créer un module complémentaire avec des aperçus de liens, consultez Prévisualiser des liens avec des chips intelligents.

Node.js

  • Module complémentaire Google Workspace qui accepte les aperçus de lien pour les formats de lien des ressources créées par les utilisateurs Pour créer un module complémentaire avec des aperçus de liens, consultez Prévisualiser des liens avec des chips intelligents.

Python

  • Module complémentaire Google Workspace qui accepte les aperçus de lien pour les formats de lien des ressources créées par les utilisateurs Pour créer un module complémentaire avec des aperçus de liens, consultez Prévisualiser des liens avec des chips intelligents.

Java

  • Module complémentaire Google Workspace qui accepte les aperçus de lien pour les formats de lien des ressources créées par les utilisateurs Pour créer un module complémentaire avec des aperçus de liens, consultez Prévisualiser des liens avec des chips intelligents.

Configurer la création de ressources pour votre module complémentaire

Cette section explique comment configurer la création de ressources pour votre module complémentaire. Elle comprend les étapes suivantes:

  1. Configurez la création de ressources dans la ressource de déploiement ou le fichier manifeste de votre module complémentaire.
  2. Créez les fiches de formulaire dont les utilisateurs ont besoin pour créer des ressources dans votre service.
  3. Gérez les envois de formulaire pour que la fonction qui crée la ressource s'exécute lorsque les utilisateurs envoient le formulaire.

Configurer la création de ressources

Pour configurer la création des ressources, spécifiez les sections et les champs suivants dans la ressource de déploiement ou le fichier manifeste de votre module complémentaire:

  1. Dans la section addOns du champ docs, implémentez le déclencheur createActionTriggers qui inclut un runFunction. (Vous définissez cette fonction dans la section suivante, Créer les fiches de formulaire.)

    Pour savoir quels champs vous pouvez spécifier dans le déclencheur createActionTriggers, consultez la documentation de référence sur les fichiers manifestes Apps Script ou les ressources de déploiement pour d'autres environnements d'exécution.

  2. Dans le champ oauthScopes, ajoutez le champ d'application https://www.googleapis.com/auth/workspace.linkcreate afin que les utilisateurs puissent autoriser le module complémentaire à créer des ressources. Plus précisément, ce champ d'application permet au module complémentaire de lire les informations que les utilisateurs envoient au formulaire de création de ressources et d'insérer un chip intelligent dans le document en fonction de ces informations.

Par exemple, consultez la section addons d'une ressource de déploiement qui configure la création de ressources pour le service de demande d'assistance suivant:

{
  "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"
        }
      ]
    }
  }
}

Dans cet exemple, le module complémentaire Google Workspace permet aux utilisateurs de créer des demandes d'assistance. Chaque déclencheur createActionTriggers doit comporter les champs suivants:

  • Un identifiant unique
  • Libellé de texte qui apparaît dans le menu Docs @
  • URL de logo renvoyant vers une icône qui apparaît à côté du texte du libellé dans le menu @
  • Une fonction de rappel qui fait référence à une fonction Apps Script ou à un point de terminaison HTTP renvoyant une carte

Créer les fiches de formulaire

Pour créer des ressources dans votre service à partir du menu Docs@, vous devez implémenter les fonctions que vous avez spécifiées dans l'objet createActionTriggers.

Lorsqu'un utilisateur interagit avec l'un de vos éléments de menu, le déclencheur createActionTriggers correspondant s'active et sa fonction de rappel présente une fiche avec des entrées de formulaire permettant de créer la ressource.

Éléments et actions compatibles

Pour créer l'interface de la fiche, vous utilisez des widgets afin d'afficher les informations et les entrées dont les utilisateurs ont besoin pour créer la ressource. La plupart des widgets et actions des modules complémentaires Google Workspace sont compatibles, à quelques exceptions près:

  • Les pieds de page des fiches ne sont pas acceptés.
  • Les notifications ne sont pas prises en charge.
  • Pour les navigations, seule la navigation updateCard est prise en charge.

Exemple de fiche avec des entrées de formulaire

L'exemple suivant montre une fonction de rappel Apps Script qui affiche une fiche lorsqu'un utilisateur sélectionne Créer une demande d'assistance dans le menu "@" :

Apps Script ;

apps-script/3p-resources/3p-resources.gs
/**
 * 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

node/3p-resources/index.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

python/3p-resources/create_3p_resources/main.py

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

java/3p-resources/src/main/java/Create3pResources.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;
}

La fonction createCaseInputCard affiche la carte suivante:

Carte avec entrées de formulaire

La fiche comprend des entrées de texte, un menu déroulant et une case à cocher. Il comporte également un bouton de texte avec une action onClick qui exécute une autre fonction pour gérer l'envoi du formulaire de création.

Une fois que l'utilisateur a rempli le formulaire et cliqué sur Créer, le module complémentaire envoie les entrées du formulaire à la fonction d'action onClick (appelée submitCaseCreationForm dans notre exemple). Il peut alors valider les entrées et les utiliser pour créer la ressource dans le service tiers.

Gérer les envois de formulaire

Une fois qu'un utilisateur a envoyé le formulaire de création, la fonction associée à l'action onClick s'exécute. Pour une expérience utilisateur optimale, votre module complémentaire doit gérer à la fois les envois de formulaire réussis et erronés.

Gérer la création réussie des ressources

La fonction onClick de votre module complémentaire doit créer la ressource dans votre service tiers et générer une URL qui pointe vers celui-ci.

Afin de communiquer l'URL de la ressource à Docs pour la création des chips, la fonction onClick doit renvoyer un SubmitFormResponse avec un tableau à un élément dans renderActions.action.links qui pointe vers un lien. Le titre du lien doit correspondre au titre de la ressource créée et l'URL doit renvoyer vers cette ressource.

L'exemple suivant présente un objet SubmitFormResponse pour une ressource créée:

Apps Script ;

apps-script/3p-resources/3p-resources.gs
/**
 * 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

node/3p-resources/index.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

python/3p-resources/create_3p_resources/main.py

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

java/3p-resources/src/main/java/Create3pResources.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;
}

Une fois le SubmitFormResponse renvoyé, la boîte de dialogue modale se ferme et le module complémentaire insère un chip dans le document. Lorsque les utilisateurs maintiennent le pointeur sur ce chip, celui-ci appelle le déclencheur d'aperçu du lien associé. Assurez-vous que votre module complémentaire n'insère pas de chips dont les formats de lien ne sont pas compatibles avec vos déclencheurs d'aperçu de lien.

Gérer les erreurs

Si un utilisateur tente d'envoyer un formulaire comportant des champs non valides, au lieu de renvoyer un SubmitFormResponse avec un lien, le module complémentaire doit renvoyer une action de rendu qui affiche une erreur à l'aide d'une navigation updateCard. Cela permet à l'utilisateur de voir ce qu'il a mal fait et de réessayer. Consultez updateCard(card) pour Apps Script et updateCard pour les autres environnements d'exécution. Les notifications et les navigations dans pushCard ne sont pas compatibles.

Exemple de gestion des erreurs

L'exemple suivant montre le code appelé lorsqu'un utilisateur envoie le formulaire. Si les entrées ne sont pas valides, la carte est mise à jour et affiche des messages d'erreur. Si les entrées sont valides, le module complémentaire renvoie un SubmitFormResponse avec un lien vers la ressource créée.

Apps Script ;

apps-script/3p-resources/3p-resources.gs
/**
 * 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

node/3p-resources/index.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

python/3p-resources/create_3p_resources/main.py

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

java/3p-resources/src/main/java/Create3pResources.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());
  }
}

L'exemple de code suivant valide les entrées du formulaire et crée des messages d'erreur pour les entrées non valides:

Apps Script ;

apps-script/3p-resources/3p-resources.gs
/**
 * 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

node/3p-resources/index.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

python/3p-resources/create_3p_resources/main.py

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

java/3p-resources/src/main/java/Create3pResources.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;
}

Exemple complet: Module complémentaire pour les demandes d'assistance

L'exemple suivant présente un module complémentaire Google Workspace qui affiche un aperçu des liens vers les demandes d'assistance d'une entreprise et permet aux utilisateurs de créer des demandes d'assistance à partir de Google Docs.

en effectuant les opérations suivantes :

  • Génère une fiche avec des champs de formulaire pour créer une demande d'assistance à partir du menu @ de Docs.
  • Valide les entrées de formulaire et renvoie des messages d'erreur en cas de données non valides.
  • Il insère le nom et le lien de la demande d'assistance créée dans le document Docs en tant que chip intelligent.
  • Aperçu du lien vers la demande d'assistance, par exemple https://www.example.com/support/cases/1234. Le chip intelligent affiche une icône, et la fiche d'aperçu inclut le nom, la priorité et la description de la demande.

Ressource de déploiement

Apps Script ;

apps-script/3p-resources/appsscript.json
{
  "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

node/3p-resources/deployment.json
{
  "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"
        }
      ]
    }
  }
}

Coder

Apps Script ;

apps-script/3p-resources/3p-resources.gs
/**
 * 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

node/3p-resources/index.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

python/3p-resources/create_3p_resources/main.py
# 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
                }]
            }
        }
    }

Le code suivant montre comment mettre en œuvre un aperçu du lien pour la ressource créée:

python/3p-resources/create_link_preview/main.py
# 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

java/3p-resources/src/main/java/Create3pResources.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;
  }

}

Le code suivant montre comment mettre en œuvre un aperçu du lien pour la ressource créée:

java/3p-resources/src/main/java/CreateLinkPreview.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 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;
  }

}