Como estruturar cards performáticos no dashboard da tela inicial, gerenciar estados de colapso no controlador central e evitar erros silenciosos de renderização no aplicativo móvel.

Na rotina diária de promotores e representantes de vendas no setor de bens de consumo embalados (CPG/FMCG), o painel inicial do aplicativo Salesforce Consumer Goods Cloud (CG Cloud Mobile) atua como a bússola de toda a jornada de trabalho.

É no Sales Cockpit (painel geral de vendas e rotas) ou no Store / Customer-360 Cockpit (painel específico da visita à loja) que o profissional visualiza metas consolidadas, lista de visitas prioritárias, avisos de rupturas de estoque, faturas vencidas e oportunidades de vendas abertas.

No entanto, para os desenvolvedores e arquitetos que trabalham com o CGC Modeler (Mv2), a criação ou extensão de um Cockpit Card customizado representa uma das tarefas mais exigentes da plataforma.

Diferente do desenvolvimento web comum, onde basta adicionar um componente visual na tela, o framework declarativo do Modeler impõe uma disciplina rigorosa de separação de responsabilidades. Para que um novo card funcione perfeitamente no dispositivo móvel, é mandatório realizar alterações coordenadas em sete pontos distintos da arquitetura, conectando contratos de Processo (.processflow.xml), Interface de Usuário (.userinterface.xml), Controladores de Negócio (BoStoreCockpitHelper), ListObjects e coleções de dados no banco SQLite local (app.db3).

O menor descuido em uma correspondência de identificador textual (string match), na declaração de métodos ou nas convenções de nomenclatura de arquivos customizados (LiMy... e LoMy...) faz com que o card desapareça da tela sem emitir logs de erro ou exiba a temida mensagem em tempo de execução: “We couldn’t render this component”.

Neste artigo prático, dissecamos a engenharia dos Cockpit Cards com código implementado e validado diretamente no workspace do Modeler (projeto_modeler), demonstramos a mecânica de Lazy Loading que garante abertura instantânea do aplicativo, explicamos a regra estrita do DisplayedSubcomponentName para alternar entre dados e mensagens de lista vazia (empty-state), e detalhamos exatamente onde coletar as evidências visuais no seu ambiente de desenvolvimento.


1. O Mecanismo de Lazy Loading: Por Que Usar CREATE em vez de LOAD?

Em dispositivos móveis de campo, o desempenho de inicialização (first paint) é um critério de aceitação inegociável. Um representante que abre o aplicativo pela manhã não pode esperar dez ou quinze segundos enquanto dezenas de consultas complexas em SQLite são disparadas simultaneamente para preencher todos os cards do dashboard.

Para solucionar esse problema, o framework do Modeler implementa o padrão de Carregamento Sob Demanda (Lazy Loading):

Fluxo de Execução do Cockpit:
1. O usuário abre o Cockpit
   └──> EntryActions do ProcessFlow executam 'CREATE' de ListObjects vazios (instantâneo)
2. A tela inicial é renderizada imediatamente com a estrutura dos cards
3. O usuário rola a tela para baixo
   └──> O CardContainer entra no campo de visão da tela
   └──> O evento 'LoadContainerData' é disparado automaticamente pela casca mobile
   └──> A cadeia de ações executa 'LoadData', calcula métricas e busca registros no SQLite
4. O card é preenchido com dados reais e o cabeçalho exibe o indicador (ex: "2 Oportunidades")

O Erro Fatal em EntryActions

Nunca utilize a ação actionType="LOAD" para carregar os dados de um card dentro do bloco <EntryActions> do seu arquivo de processo. Fazer isso obriga o aplicativo a executar todas as queries no banco de dados antes mesmo de desenhar o primeiro pixel na tela, travando o aparelho.

Em <EntryActions>, execute estritamente a instanciação vazia com actionType="CREATE". A busca física de dados deve aguardar o evento de visibilidade do container (<LoadContainerData>).


2. Os 7 Locais Obrigatórios de Implementação (O Checklist Arquitetural)

Para implementar um card customizado denominado CardOpportunities (exibindo oportunidades abertas do cliente no Store Cockpit), realizamos as alterações diretamente no workspace projeto_modeler. Vejamos os sete pontos de contrato:

No Arquivo de Processo (Call_StoreCockpitTabProcess.processflow.xml):

Ponto 1: Declarations (Contexto do Processo)

Declare as variáveis de estado que alimentarão o card. O ListObject customizado deve obrigatoriamente iniciar com o prefixo LoMy:

<ProcessContext>
  <Declarations>
    <!-- O ListObject contendo a coleção de oportunidades em memória -->
    <Declaration name="CardOpportunities_List" type="LoMyOpportunityForStoreCockpit" />
    
    <!-- Texto informativo do cabeçalho do card (ex: "2 Oportunidades") -->
    <Declaration name="CardOpportunities_InformationText" type="String" />
    
    <!-- Flag de controle que alimenta a propriedade IsReadyToLoad -->
    <Declaration name="CardOpportunities_DataLoaded" type="DomBool" />
    
    <!-- Propriedade MANDATÓRIA que define o subcomponente visual visível -->
    <Declaration name="CardOpportunities_DisplayedSubcomponentName" type="String" />

    <Declaration name="selectedOpportunityPKey" type="DomPKey" />
  </Declarations>
</ProcessContext>

Ponto 2: EntryActions (Instanciação Leve em Memória)

Instancie o ListObject vazio no ciclo de entrada do processo com actionType="CREATE":

<EntryActions>
  <!-- Custom Card: Opportunities (Lazy Loading Instant CREATE) -->
  <Action actionType="CREATE" name="CardOpportunities_GetList" type="LoMyOpportunityForStoreCockpit">
    <Return name="ProcessContext::CardOpportunities_List" />
  </Action>
</EntryActions>

Ponto 3: Registro de Eventos na Ação VIEW

Na ação que exibe a interface do Cockpit (actionType="VIEW" name="ShowCockpit"), mapeie os eventos emitidos pela interface aos nomes das ações de negócio:

<Action actionType="VIEW" name="ShowCockpit">
  <UIDescription>Call::StoreCockpitTabUI</UIDescription>
  <Events>
    <!-- Disparado quando o card fica visível na rolagem móvel -->
    <Event name="CardOpportunities_loadData" action="CardOpportunities_LoadData" />
    
    <!-- Disparado quando o usuário clica em uma oportunidade da lista -->
    <Event name="CardOpportunities_itemSelected" action="CardOpportunities_ShowSelected" />
  </Events>
</Action>

Ponto 4: Cadeia de Carregamento de Dados (Actions Body)

Encadeie as ações sequenciais para calcular os dados, formatar o título do header e definir dinamicamente se o card deve renderizar a lista ou o empty-state:

<!-- Passo A: Executa a busca e cálculos no ListObject -->
<Action actionType="LOGIC" name="CardOpportunities_LoadData" call="ProcessContext::CardOpportunities_List.calculateOpportunitiesForCard">
  <Parameters>
    <Input name="customerPKey" value="ProcessContext::MainBO.bpaMainPKey" />
  </Parameters>
  <Return name="ProcessContext::CardOpportunities_List" />
  <TransitionTo action="CardOpportunities_GetInfoText" />
</Action>

<!-- Passo B: Obtém o texto formatado para o cabeçalho (ex: "2 Oportunidades") -->
<Action actionType="LOGIC" name="CardOpportunities_GetInfoText" call="ProcessContext::CardOpportunities_List.getInformationText">
  <Return name="ProcessContext::CardOpportunities_InformationText" />
  <TransitionTo action="CardOpportunities_AssignDisplayedSubcomponentName" />
</Action>

<!-- Passo C: O controlador decide se exibe a lista ou o empty-state -->
<Action actionType="LOGIC" name="CardOpportunities_AssignDisplayedSubcomponentName" call="ProcessContext::CardController.getDisplayedSubcomponentName">
  <Parameters>
    <Input name="loItems" value="ProcessContext::CardOpportunities_List" />
    <Input name="type" type="Literal" value="Opportunities" />
  </Parameters>
  <Return name="ProcessContext::CardOpportunities_DisplayedSubcomponentName" />
  <TransitionTo action="SetCardOpportunities_DataLoaded" />
</Action>

<!-- Passo D: Marca a flag de dados carregados -->
<Action actionType="LOGIC" name="SetCardOpportunities_DataLoaded" call="Utils.identity">
  <Parameters>
    <Input name="value" value="1" type="Literal" />
  </Parameters>
  <Return name="ProcessContext::CardOpportunities_DataLoaded" />
</Action>

Ponto 5: Navegação entre Processos (Ações de Seleção)

Capture o identificador do item clicado para detalhamento:

<Action actionType="LOGIC" name="CardOpportunities_ShowSelected" call="Utils.identity">
  <Parameters>
    <Input name="value" value="Event.pKey" />
  </Parameters>
  <Return name="ProcessContext::selectedOpportunityPKey" />
</Action>

No Arquivo de Interface (Call_StoreCockpitTabUI.userinterface.xml):

Ponto 6: Declaração do CardContainer e Layout Multi-Subcomponente

No arquivo de UI, posicione o card dentro de <Area areaName="mainArea" areaPattern="Card">:

<!-- Custom Card: Opportunities with Lazy Loading and Multi-Subcomponent DisplayedSubcomponentName -->
<CardContainer name="CardOpportunities">
  <Bindings>
    <Binding type="Visible" target="Visible" call="ProcessContext::CardController.isCardVisible">
      <Parameters>
        <Input name="cardName" type="Literal" value="CardOpportunities" />
      </Parameters>
    </Binding>
    <Binding type="Text" target="IsCollapsible" call="ProcessContext::CardController.isCardCollapsible" bindingMode="ONE_WAY">
      <Parameters>
        <Input name="cardName" type="Literal" value="CardOpportunities" />
      </Parameters>
    </Binding>
    <Resource target="Title" type="Label" id="OpportunitiesCardTitle" defaultLabel="Open Opportunities" />
    <Binding target="InformationText" type="Text" binding="ProcessContext::CardOpportunities_InformationText" bindingMode="ONE_WAY" />
    <Binding target="IsReadyToLoad" type="Text" binding="ProcessContext::CardOpportunities_DataLoaded" bindingMode="ONE_WAY" />
    <Binding target="DisplayedSubcomponentName" type="Text" binding="ProcessContext::CardOpportunities_DisplayedSubcomponentName" bindingMode="ONE_WAY" />
    <Binding target="CollapseState" type="Text" binding="ProcessContext::CardController.collapseState_CardOpportunities" bindingMode="TWO_WAY" />
  </Bindings>
  <VisibilityRoles allRoles="true" />
  <Events>
    <LoadContainerData event="CardOpportunities_loadData" />
  </Events>

  <!-- Subcomponente 1: A lista de registros preenchida -->
  <CockpitList name="Opportunities" hasBorder="false" dataSource="ProcessContext::CardOpportunities_List.Items[]">
    <Items name="Items" itemPattern="OpportunityCardItems">
      <ItemListLayout>
        <Default>
          <Col flex="1" layoutType="itemIdentifier" bindingId="name" />
          <Col width="8em" layoutType="itemSecondary" bindingId="formattedAmount" />
        </Default>
      </ItemListLayout>
      <Bindings>
        <Binding target="name" type="Text" binding=".name" bindingMode="ONE_WAY" />
        <Binding target="formattedAmount" type="Text" binding=".formattedAmount" bindingMode="ONE_WAY" />
      </Bindings>
    </Items>
    <Events>
      <ItemSelectedEvent event="CardOpportunities_itemSelected">
        <Params>
          <Param name="pKey" value=".pKey" />
        </Params>
      </ItemSelectedEvent>
    </Events>
  </CockpitList>

  <!-- Subcomponente 2: O Empty-State exibido quando não há registros -->
  <NoDataMessage name="CardNoDataMessageUiPlugin">
    <Bindings>
      <Resource target="Text" type="Label" id="NoOpportunitiesId" defaultLabel="No open opportunities for this customer." />
    </Bindings>
  </NoDataMessage>
</CardContainer>

No Controlador Central de Negócios (BoStoreCockpitHelper):

Ponto 7: Visibilidade, Colapso e Subcomponente Ativo

No arquivo XML do controlador (BoStoreCockpitHelper.businessobject.xml), declare a propriedade de colapso:

<SimpleProperty name="collapseState_CardOpportunities" type="DomBool"/>

No arquivo JavaScript de regras de visibilidade (BoStoreCockpitHelper.IsCardVisible.bl.js):

/*
 * Data: 2026-09-29
 * Objetivo da Customização: Ativa visibilidade do card customizado CardOpportunities no Store Cockpit.
 */
case 'CardOpportunities':
  visible = true;
  break;Code language: JavaScript (javascript)

E no arquivo de colapso (BoStoreCockpitHelper.IsCardCollapsible.bl.js):

/*
 * Data: 2026-09-29
 * Objetivo da Customização: Permite colapso do card customizado CardOpportunities no Store Cockpit.
 */
case 'CardOpportunities':
  collapsible = true;
  break;Code language: JavaScript (javascript)

3. A Regra Crítica do DisplayedSubcomponentName

A causa número um de telas com a mensagem de falha “We couldn’t render this component” é a divergência de strings entre o controlador e o XML da interface.

Um CardContainer com <CockpitList> é um container multi-subcomponente. Ele renderiza exatamente um único elemento filho por vez, selecionado por correspondência exata de texto (exact string match) entre o valor retornado em DisplayedSubcomponentName e o atributo name do elemento filho:

Valor retornado em DisplayedSubcomponentNameElemento Filho Renderizado na UI
Exatamente "Opportunities" (corresponde a <CockpitList name="Opportunities">)A lista de registros preenchida.
Exatamente "CardNoDataMessageUiPlugin"O componente de empty-state informando que não há itens.
null, undefined ou string divergenteNenhum elemento. O aplicativo exibe a mensagem de falha: “We couldn’t render this component”.

Observe o código core estendido em BoStoreCockpitHelper.GetDisplayedSubcomponentName.bl.js:

var sDisplayedSubcomponentName = type;

if (loItems.getCount() === 0) {
  sDisplayedSubcomponentName = "CardNoDataMessageUiPlugin";
}
/*
 * Data: 2026-09-29
 * Objetivo da Customização: Suporte à alternância de subcomponente (Opportunities vs CardNoDataMessageUiPlugin) para o card customizado CardOpportunities.
 */
else if (type === "Opportunities") {
  sDisplayedSubcomponentName = (loItems && loItems.getCount() > 0) ? "Opportunities" : "CardNoDataMessageUiPlugin";
}Code language: JavaScript (javascript)

O acoplamento triplo é mandatório:

  1. No ProcessFlow, passe o literal exato: <Input name="type" type="Literal" value="Opportunities" />.
  2. Na UserInterface, a lista deve ter exatamente esse nome: <CockpitList name="Opportunities" ...>.
  3. O componente de dados ausentes deve ter sempre o nome reservado do framework: <NoDataMessage name="CardNoDataMessageUiPlugin">.

Na execução real no aplicativo móvel Salesforce Consumer Goods Cloud Mobile, podemos constatar esse acoplamento operando com precisão:

Como observado na captura real do aplicativo durante a visita ao cliente *NTO Atlanta (com o contato Naomi Anderson), o card customizado Open Opportunities foi posicionado e renderizado com perfeição no topo da coluna central do Store Cockpit (“Centro de decisões da loja”). Note que os registros calculados dinamicamente em tempo de execução — Campanha Verão CPG (R$ 15.000,00) e Display Promocional Check-out (R$ 8.500,00) — aparecem estruturados nas duas colunas (itemIdentifier e itemSecondary), sem provocar qualquer atraso no carregamento da tela inicial. Como a lista retornou registros (getCount() > 0), o controlador resolveu DisplayedSubcomponentName para "Opportunities", ativando a <CockpitList> e dispensando a mensagem vazia de forma fluida.


4. Regras Mandatórias de Validação no sf modeler workspace validate

Ao validar customizações com o compilador do Modeler, atente-se às regras de ouro que validamos em nosso workspace:

  1. Convenção de Nomes de Contratos Customizados: Todo ListItem customizado deve obrigatoriamente iniciar com LiMy (ex: LiMyOpportunityForStoreCockpit). Todo ListObject customizado deve obrigatoriamente iniciar com LoMy (ex: LoMyOpportunityForStoreCockpit). O descumprimento gera os códigos de erro 03112658 e 03112660.
  2. Tags de Fronteira no JavaScript (.bl.js): Toda função de regra de negócio em Mv2 deve conter as marcas de inserção de código://///////////////////////////////////////////////////////////////////////////////////////////// // // // Add your customizing javaScript code below. // // // /////////////////////////////////////////////////////////////////////////////////////////////// ... /////////////////////////////////////////////////////////////////////////////////////////////// // // // Add your customizing javaScript code above. // // // ///////////////////////////////////////////////////////////////////////////////////////////////A ausência dessas linhas dispara os erros 03112630 e 03112631.
  3. DataSource Obrigatório: No XML do ListObject, a tag <DataSource> é obrigatória, mesmo para listas geradas em memória. Omiti-la causa o erro de XSD 00000001: Missing child element(s).
  4. isCollapsible Não É Atributo Direto: No XML do CardContainer, isCollapsible não deve ser atributo da tag raiz; use sempre a tag interna <Binding target="IsCollapsible" ... />.

Conclusão

A arquitetura de Cockpit Cards do Salesforce Consumer Goods Cloud Modeler equilibra desempenho extremo em campo com isolamento rigoroso de responsabilidades.

Ao aplicar o padrão de carregamento preguiçoso (lazy loading), respeitar a correspondência estrita de strings no DisplayedSubcomponentName e validar os contratos contra as convenções do compilador no projeto_modeler, sua equipe entrega painéis de vendas rápidos, livres de travamentos e prontos para a escala das maiores operações de bens de consumo do mundo.


Você já precisou criar Cockpit Cards customizados no Modeler do Consumer Goods Cloud? Quais foram os maiores desafios enfrentados na sincronização entre ProcessFlow, UI e o BoHelper? Compartilhe suas experiências nos comentários abaixo ou debata com a gente no LinkedIn.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *