Files
jogos_matheus/.claude/commands/vitruvio-criar-form-mobile.md
2026-09-23 12:29:08 -03:00

11 KiB

Create Vitruvio Mobile Form

All messages shown to the user must be written in Portuguese.

Mobile forms are not desktop forms with a different skin. They use a different schema, a smaller component set, a different JavaScript runtime, and an explicit server↔client data contract. Read this whole skill before scaffolding — the mistakes here are not the desktop mistakes.

The four things that make mobile different

  1. Different schema. Root is <mobile-forms>, namespace http://www.davinti.com.br/vitruvio/mobile-form, XSD vitruvio-mobile-form.xsd.
  2. Only 18 components (vs 78 desktop). Do not assume a desktop widget exists on mobile. The full list: CheckBox, ComboBox, GoogleMapsField, HorizontalLayout, ImageLibraryByFieldValue, ImageWidget, Label, MaskedField, MoneyField, NumericField, OptionGroup, ProgressBarWidget, RatingStars, SignaturePadField, TabLayout, TextField, Toggle, VerticalLayout (plus structural SubForm/ItemList). Read ~/.local/share/vitruvio-platform/docs/components/mobile/<Component>.md before using one.
  3. Two JavaScript runtimes.
    • Client-side (initScript, discoveryScript, validators, component event scripts): modern React-Native JS — arrow functions, Promises, .then()/.catch() are fine and expected. Most APIs are async and return Promises.
    • Server-side (<ServerSide><Bridge> execute(...) bodies): Rhino ES5, same rules as scripts/desktop. This is the only place with access to platform libs and services (libService, runtimeService, db, etc.).
  4. Data is explicit. Nothing is auto-injected the way desktop process variables are. Every piece of server data the form needs must be declared — via an <Autoload> variable, a <QueryDataSource> (synced to the device, works offline), or fetched on demand from a named <Bridge> with vCommunicationService.executeOnServer(...).

Step 1 — Confirm you are inside a Vitruvio repo

ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"

If NOT_A_VITRUVIO_REPO, stop and tell the user to cd into the correct repo.

Step 2 — Determine variant and gather the data contract

Ask (only what is missing):

  • Panel or process mobile form? (see Differences: panel vs process below)
  • Key — the panel or process key (folder name).
  • For a process: which formKey(s) — they must match the activiti:formKey in processes/<key>/<key>.bpmn.
  • Which variables does the form need? Because nothing is auto-injected, ask explicitly:
    • process variables to read (each becomes an <Autoload> <variable> and/or a Bridge call)
    • reference/lookup data (each becomes a <QueryDataSource> backed by a queries/*.sql)
    • any server-only logic / libs needed (each becomes a <Bridge>)
  • Offline? Whether the data sources must be available without connectivity (affects autoSyncOnInit / autoSyncOnDiscovery).

Differences: panel vs process

The schema, component set, ServerSide/Bridge mechanics and JS runtimes below are identical for both. Only these differ:

Panel mobile form Process mobile form
File panels/<key>/<key>-mobile.xml processes/<key>/<key>-mobile.xml
Root attribute <mobile-forms> (no processKey) <mobile-forms processKey="<key>">
Forms per file one <form> one <form formKey> per BPMN activiti:formKey (must match)
Manifest set forms.mobile and showInMobileList: true on the panel entry set forms.mobile (and optionally forms.mobileAlternative) on the process entry
Variables use engine.getGlobalVariable / Autoload process variables are fetched server-side via a Bridge (runtimeService.getVariable) and/or declared in <Autoload>

Filename: name files by the artifact key + suffix — <key>-mobile.xml (and <key>-desktop.xml, <key>.bpmn). This keeps every form searchable by its key instead of dozens of identical form-mobile.xml tabs. Older content used form-mobile.xml / form_web_mobile.xml; the real path is whatever forms.mobile points to, so legacy files still work — but new scaffolds use <key>-mobile.xml.

Step 3 — Scaffold the file

Skeleton (process variant shown; for a panel drop processKey and use a single <form>)

<?xml version="1.0" encoding="UTF-8"?>
<mobile-forms processKey="<key>"
              xmlns="http://www.davinti.com.br/vitruvio/mobile-form"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://www.davinti.com.br/vitruvio/mobile-form https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-form.xsd">

  <form formKey="formColeta">
    <name>Coleta</name>
    <description>Etapa de coleta no app</description>

    <!-- Runs when the form opens. CLIENT-SIDE: modern JS, async APIs return Promises. -->
    <initScript language="JavaScript">
      <![CDATA[
        function run() {
          // Autoloaded variables land in the global scope:
          engine.getField('nomeLoja').setValue(engine.getGlobalVariable('nomeLoja'));
        }
      ]]>
    </initScript>

    <!-- Optional: pull server data when the task is discovered (before the user opens it). -->
    <discoveryScript language="JavaScript">
      <![CDATA[
        function run() {
          var params = { id: execution.getProcessInstanceId() };
          vCommunicationService.executeOnServer('bridgeNumeroCarga', params).then(result => {
            var n = parseInt(result, 10);
            execution.setVariable('numeroCarga', n).then(ok => {}).catch(err => {
              console.log('Erro ao setar numeroCarga localmente', err);
            });
          }).catch(err => {
            console.log('Erro ao coletar numeroCarga no server', err);
          });
        }
      ]]>
    </discoveryScript>

    <!-- Block completing the task when a rule is not met. -->
    <validators>
      <ScriptValidator execution="COMPLETE" language="JavaScript" id="validadorComplete">
        <![CDATA[
          function Validator() {
            var msg;
            this.getMessage = function() { return msg; };
            this.isValid = function() {
              if (engine.getField('confirma').getValue() == 'Sim') { return true; }
              msg = 'Confirme a execução antes de finalizar.';
              return false;
            };
          }
          var validator = new Validator();
        ]]>
      </ScriptValidator>
    </validators>

    <components>
      <VerticalLayout spacing="true" margin="true" width="100%">
        <TextField id="nomeLoja" type="string" caption="Loja" readOnly="true" />
        <OptionGroup id="confirma" type="string" caption="Executou?" required="true">
          <entry key="Sim" value="Sim" />
          <entry key="Nao" value="Não" />
        </OptionGroup>
        <SignaturePadField id="assinatura" caption="Assinatura" />
      </VerticalLayout>
    </components>

    <!-- Explicitly declared variables auto-injected into the form's scope on load. -->
    <Autoload>
      <variables autoInjectionScope="ENGINE_GLOBAL_SCOPE" autoPersist="true">
        <variable>nomeLoja</variable>
        <variable>numeroCarga</variable>
      </variables>
    </Autoload>

    <!-- Everything the server provides to this form. -->
    <ServerSide>
      <!-- Named queries (queries/*.sql) synced to the device for offline lookups. -->
      <DataSources>
        <QueryDataSource key="qry_produtos_carga" autoSyncOnInit="true" autoSyncOnDiscovery="true" refreshInSeconds="60" />
      </DataSources>

      <!-- Server-side functions. Rhino ES5. Full access to libService / runtimeService / db.
           Called from the client via vCommunicationService.executeOnServer('id', params). -->
      <Bridges>
        <Bridge language="JavaScript" id="bridgeNumeroCarga">
          <![CDATA[
            function execute(params) {
              var numeroCarga = runtimeService.getVariable(params.id, 'numeroCarga');
              return numeroCarga ? numeroCarga : -1;
            }
          ]]>
        </Bridge>
      </Bridges>
    </ServerSide>
  </form>

</mobile-forms>

How libs and process variables reach the mobile form

The mobile app cannot call libService.loadScript(...) or read process variables directly — those live on the server. The pattern is always declare a Bridge, call it from the client:

<!-- SERVER-SIDE (Rhino ES5): a lib used to build a barcode image -->
<Bridge language="JavaScript" id="imagemCodBarras">
  <![CDATA[
    function execute(codbarras) {
      var generator = libService.loadScript('barcode-gen');
      return ',' + generator.generateEAN13BarcodeImageAsBase64({ value: codbarras });
    }
  ]]>
</Bridge>
// CLIENT-SIDE (modern JS): call the bridge, use the Promise result
vCommunicationService.executeOnServer('imagemCodBarras', codigoBarras).then(base64 => {
  engine.getField('codigoImagem').setValue(base64);
}).catch(err => console.log('Erro no bridge imagemCodBarras', err));

Rules of thumb:

  • Anything needing a platform lib, the database, or a platform service → put it in a Bridge (server, ES5) and call it with vCommunicationService.executeOnServer.
  • Reference/lookup tables the form reads repeatedly → a QueryDataSource backed by a queries/*.sql (works offline once synced).
  • Process variables the form needs → either declare them in <Autoload> or fetch them in a Bridge via runtimeService.getVariable(processInstanceId, 'varName') and store locally with execution.setVariable(...).
  • Client-side execution.getVariable(...) / execution.setVariable(...) are async and return Promises — use .then().

List-based entry: SubForm + ItemList

For "add many items" screens (collect a list of rows), use a SubForm with an <ItemList>:

<SubForm formKey="executarAcao">
  <name>Executar AÇÃO</name>
  <initScript language="JavaScript">
    <![CDATA[ function run(apply) { if (apply) { apply(); } } ]]>
  </initScript>
  <ItemList addItemButtonCaption="Gravar na Lista" caption="Itens">
    <property id="OBSERVACAO" caption="Observação" />
  </ItemList>
  <components>
    <VerticalLayout width="100%" spacing="true" margin="true">
      <!-- fields captured per item -->
    </VerticalLayout>
  </components>
</SubForm>

Validate list completeness with engine.getSubFormListSizeByStatus(function(size){ ... }, "all") inside a COMPLETE validator.

Step 4 — Manifest

If run standalone, update the matching entry in vitruvio.json:

  • Panel: set forms.mobile = "panels/<key>/<key>-mobile.xml" and showInMobileList: true.
  • Process: set forms.mobile = "processes/<key>/<key>-mobile.xml".

Full entry creation is handled by vitruvio-criar-painel / vitruvio-criar-processo.

Step 5 — Report

Tell the user:

  • File created/updated and variant (panel/process).
  • Which variables/queries/bridges were declared, and that only declared data is available on the device — anything else must be added as an Autoload variable, QueryDataSource, or Bridge.
  • Reminder: client scripts are modern JS (Promises); Bridge bodies are server-side Rhino ES5.
  • For processes: each <form formKey> must match an activiti:formKey in the BPMN.
  • Suggest reading docs/components/mobile/ and an example (~/.local/share/vitruvio-platform/examples/processes/*/form_web_mobile.xml) for richer screens.