Árvore de páginas

CONTEÚDO

  1. Introdução
  2. Visão geral
  3. O que é a personalização
  4. O que é a customização
  5. Diferenças da personalização/customização progress para o PO-UI
  6. Requisitos gerais para o desenvolvimento
  7. Consulta de programas passiveis de personalização e customização
  8. Consulta de programas progress que foram migrados para PO-UI
  9. Consulta de documentações de programas que permitem customização 
  10. Personalização 
  11. Template padrão API Rest com pontos de UPC
  12. Guia de Padrões de desenvolvimento de API para o produto Datasul 
  13. Como personalizar - Documento de referencia
  14. Como customizar - Documento de referencia



01. Introdução

Neste manual vamos falar sobre como funciona a personalização e customização de telas html PO-UI, vamos falar sobre as diferenças entre personalizar e customizar, 
em que momento poderá ser utilizado a personalização ou customização, com exemplos e possíveis cenários de implementação. 


02. Visão Geral

Este manual irá ajudar a entender para que serve a personalização e customização, entender em que momento cada um poderá ser utilizado e como efetuar a implementação nas telas PO-UI em conjunto com o back-end progress.   



03. O que é a personalização

A personalização se trata de alterações simples na tela, como:

    • Alteração de cor 
    • Apresentação da ordem do campo no qual será visualizado na tela
    • Validações de interface
    • Mascaras de apresentação de campo
    • Alterar label/opções de um checkbox 
    • Alterar um label de um campo  
    • Transformar um campo em somente leitura
  •  Entre outras alterações não especificas. Se a necessidade se encaixar em alguma das opções acima ou algo que seja uma alteração equiparada as opções supra citadas, a melhor opção é utilizar a personalização. Para maiores informações de como personalizar consultar a pagina/tópico "Como personalizar".
  • A personalização low-code hoje se aplica apenas ao PO-DYNAMIC-FORM e PO-DYNAMIC-VIEW, para utilizar PO-PAGE-DYNAMIC-TABLE, Deve-se também implementar endpoint em Progress, o qual será utilizado como fonte de dados para os campos personalizados.



04. O que é a customização

A customização já contempla uma alteração mais especifica das telas e do back-end progress.

    • sendo possível incluir/eliminar campos da tela 
    • incluir ações nas telas
    • customizar a massa de dados no beck-end que será apresentado no front-end
    • incluir validações para mostrar/salvar a massa de dados
  • Para alterações dos componentes em tela já não é possível fazer de forma low-code. A maior parte das alterações será feita em progress, para informar a tela quais os campos serão ou não apresentados e quais os dados que vão aparecer nesses campos (consulta) ou que será armazenado (cadastro).

05. Diferenças da personalização/customização progress para o PO-UI

Existem algumas diferenças na customização de telas quando se comparado o progress com as novas telas html, exemplo.:      

   
Progress  PO-UI
Incluir botão em tela progressNo html não tem o evento no botão e sim uma ação
No progress há eventos de telaNo html não há eventos de tela, mas é possível adicionar "eventos" na BO.
Regra de negocio junto com a telaRegra de negocio separada da tela (BO)
No progress era possível personalizar/customizar qualquer telaNo html somente tela com componentes dinâmicos
No progress era possível chamar uma tela a partir de outra telaNo html não é possivel chamar novas telas

06. Requisitos gerais para o desenvolvimento

Tecnico:

Produto:

      Personalização: 

    • Programa em THF - PO-UI utilizando componentes dinâmicos
    • Programa deve ser cadastrado no cadastro de programas (men012aa), com as flags "visualiza menu" e "permite personalização" marcadas.

      Customização:

    • Programa em PO-UI preparado para ser customizado:
    • Tela construída com componentes dinâmicos
    • Beckend progress preparado com a técnica de EPC/UPC rest.   

07. Como saber os programas passiveis de personalização e customização

Para identificar todas as telas que já estão permitindo a personalização/customização, entrar no produto e utilizar a visão de dados "Programas Customizados (vdProgramasDatasulUPC)".


08. Como saber quais programas progress que foram migrados para PO-UI

Para consultar as telas progress que ja foram migradas para PO-UI, dentro do produto utilizar a visão de dados "De-Para Programa HTML x Progress (vdDeParaProgsHTML)". Esta visão de dados ira mostrar todas as telas progress que já foram migradas para PO-UI.


09. Como identificar o que pode ser personalizado ou customizado nos programas

Para consultar os programas que permitem a personalização/customização, basta apertar o F1 para chamar a ajuda da tela e conferir na documentação se a tela já esta preparada para personalização ou customização.

Estamos sempre em evolução!

Estas documentações estão em evolução, talvez nem toda tela tenha ainda a documentação dela e a customização vai depender da equipe responsável pelo programa em aplicar a técnica de customização e assim documentar o que poderá ser customizado.


10. Personalização

EXEMPLO DE UTILIZAÇÃO - Produto Datasul

Cadastro de campos personalizados

A seguir são apresentados as telas necessárias para a realização do cadastro dos campos personalizados.


Ao localizar no menu o programa Campos personalizados (html.personalization-metadata), é apresentada a tela em formato de lista que conterá todos os campos (metadados) cadastrados no produto Datasul. Para cadastrar um campo que será utilizado na personalização, basta clicar no botão +Adicionar.



A tela a seguir apresenta o cadastro do metadado relacionado a um campo que pode ser apresentado no programa como personalizado



Para alguns tipos de campo, é possível informar uma lista de opções apresentadas em tela, para isso basta informar os dados (chave, valor).


CampoDescriçãoObrigatório
Código Programa Datasul

Código do programa base que podem ser aplicadas as técnicas de personalização

Nota

É possível cadastrar os campos somente em programas que permitem a personalização. Programas que permitem personalização, devem ser cadastrados no "Cadastro de programa" com o "template": "programa THF" e possuir a flag "permite personalização" marcada.  

Sim

Identificador Campo

Identificador único do campo (por programa), necessário para a geração da tela personalizada (código do campo)Sim

Nome Campo

Nome do campo que será apresentado na tela (label do campo)

Caso o campo não seja informado, o nome do campo apresentado será o informado no identificador.

Não
Tipo Campo

Tipo do campo cadastrado

Caso o campo não seja informado, será considerado que o campo é do tipo Character

Tipos de campos permitidos:

TipoDescrição
CheckboxValores lógicos, apresentado na interface como um componente de Switch.
NúmerosValores numéricos, não sendo permitido a inserção de caracteres.
Data

Valores de datas, apresentado na interface como DatePicker.

Hora

Valor do horário, apresentado máscara 99:99 por padrão.

CharacterValores alfanuméricos.
MoedaValores monetários.
TextAreaValores alfanuméricos, apresentado na interface em um box com 3 linhas.
SenhaValores alfanuméricos, apresentados na interface como Password.
Radio GroupLista de seleção apresentadas no formato de radio group, permitido no máximo 3 opções.
Checkbox GroupLista de seleção múltipla apresentadas no formato de checkbox group, permitido no máximo 3 opções.
SelectLista de seleção apresentadas no formato de select, é obrigatório cadastrar ao menos 4 opções.
MultiselectLista de seleção múltipla apresentadas no formato de select, é obrigatório cadastrar ao menos 4 opções.
ComboboxLista de seleção apresentadas no formato de combobox, é obrigatório informar o atributo optionsService para resgatar os dados da lista.
Não
Lista Opções

Informa a lista de opções a serem apresentadas em conjunto com o tipo do componente especificado.

Informação

Esta opção está disponível somente para os tipos Radio Group, Checkbox Group, Select e Multiselect


Somente LeituraOpção para que o campo seja apresentado como somente leitura (torna o campo readOnly)Sim
Habilita personalizaçãoOpção para habilitar ou desabilitar a apresentação da personalização por campo (não envia o campo para ser renderizado no fron-end).Sim



Após cadastrar o campo, o mesmo é apresentado na tela inicial onde é possível realizar filtros sobre seus resultados, bem como efetuar ações de edição de campos, inclusão de atributos, habilitar ou desabilitar todos os campos por programa e exclusão do campo.




Ao clicar na opção de editar, não será possível modificar o código do programa Datasul vinculado e também seu identificador. Os demais campos estão habilitados para edição.





Atributos de campos personalizados

Com a configuração de atributos dos campos personalizados, é possível adicionar outras características tais como:

- Alteração de cor;

- Apresentação da ordem do campo no qual será visualizado em tela;

- Validações de interface;

- Máscara de apresentação do campo, entre muitos outros.

Ao clicar no botão Atributos apresentado na grid principal (Opções), uma tela de parametrizações é apresentada para que seja possível adicionar as novas características.

Ao renderizar os campos personalizados em tela, esses atributos serão inclusos no campo personalizado e enviados para tela.

Atributos do PO-UI

Os nomes dos atributos devem ser os mesmos que estão documentados nas propriedades do componente no PO-UI.

Exemplo: Para personalizar um campo no formato CPF, criamos o campo COD_CPF e adicionamos um atributo do tipo mask que conterá o formato 999.999.999-99.

O atributo mask corresponde a uma propriedade do componente PO-INPUT.






11. Template padrão API Rest com pontos de UPC (Customização)

Abaixo um exemplo de uma API Rest com a mínimas definições e com entradas de upc nos contextos web de get, post, put e delete (Na pagina Customização PO-UI é possível encontrar exemplos da utilização).

/*------------------------------------------------------------------------
    File        : templateExemplo.p
    Purpose     : demonstração de definições API REST .

    Syntax      :

    Description : API REST 

    Author(s)   : 
    Created     : 
    Notes       :
  ----------------------------------------------------------------------*/

/* ***************************  Definitions  ************************** */

//Definição da ut-api padrão das apis rest
{utp/ut-api.i}

//Definição da ut-api-action padrão das apis rest com os contextos web: get, post, put e delete
{utp/ut-api-action.i pFindAll GET /~* }
 
{utp/ut-api-action.i pUpdateById PUT /~* }
 
{utp/ut-api-action.i pGetMetadata POST /metadata/~* }
{utp/ut-api-action.i pValidateForm POST /validateForm/~* }
{utp/ut-api-action.i pValidateField POST /validateField/~* }
{utp/ut-api-action.i pCreate POST /~* }
 
{utp/ut-api-action.i pDeleteById DELETE /~* }

//Definição da ut-api-notfound que trata erros caso o a procedure do metodo chamado nao exista
{utp/ut-api-notfound.i}

// Definição de temp table  

/* ***************************  Definitions end  ************************** */  

/* ***************************  Main Block  *************************** */
// a definição da include/i-epcrest.i pode ser feita nas procedures conforme a necessidade de manipular/validar os dados
// conforme o exemplo, sera chamada a template de upc do exemplo abaixo
 {include/i-epcrest.i &endpoint=create &event=afterCreate &jsonVar=oBody}


Abaixo um template de programa de UPC rest.

/*------------------------------------------------------------------------
    File        : templateExemploUpc.p
    Purpose     : demonstração de definições upc REST .

    Syntax      :

    Description : Exemplo de programa Upc sendo chamado pela API rest com pontos de epc 

    Author(s)   : 
    Created     : 
    Notes       :
  ----------------------------------------------------------------------*/

/* ***************************  Definitions  ************************** */
//deifinições obrigatorias para poder manipular as informações
USING PROGRESS.json.*.
USING PROGRESS.json.ObjectModel.*.

//Definições obrigatorias para receber os parametros passados para a include "include/i-epcrest.i"
define input        parameter pEndPoint as char       no-undo.
define input        parameter pEvent    as char       no-undo.
define input        parameter pProgram  as char       no-undo.
define input-output parameter pObjParam as JsonObject no-undo.

/* ***************************  Definitions end  ************************** */

/* ***************************  Main Block  *************************** */
if pEndPoint = "create" and
   pEvent    = "afterCreate"   then do:

   message "pEndPoint: " pEndPoint skip
           "pEvent: "    pEvent    skip
           "pProgram: "  pProgram  skip
           "method: "    pObjParam:getCharacter("method")  skip
           "URI: "       pObjParam:getCharacter("uri")
           view-as alert-box.

end.

return "OK".

12. Guia de Padrões de desenvolvimento de API para o produto Datasul

Índice

Introdução


O desenvolvimento de APIs permite a exposição e o consumo de dados com o objetivo da integração front-end (portais, customizações, etc) ao back-end do produto Datasul, de maneira segura e padronizada.

A estrutura de integração de APIs Datasul suporta o envio de requisições no estilo de arquitetura REST com o desenvolvimento da regra de negócio em Progress. 


Abaixo o fluxo das requisições via HTTP (DATASUL-REST) e formato de execução via Progress:



Esta funcionalidade está disponível para utilização conforme apresentado no quadro abaixo:

Matriz de Evolução



Contextos


Existem 2 contextos definidos que se aplicam tanto para a Antiga arquitetura (Jboss) quanto para o DTS4THF (Tomcat).

Estes contextos tem o objetivo de definir o acesso das requisições, ou seja, se são contextos de acessos internos (via menu) pelo contexto DTS/DATASUL-REST ou acessos externos (via APPs por exemplo) pelo contexto /API.


IMPORTANTE!

O contexto /api foi criado com autenticação via "Basic Authentication", para atender o guia de API TOTVS, sendo possível conexão com qualquer API que anteriormente era "dts/datasul-rest/resources/prg".




Formato URL


O Guia de Implementação de API TOTVS define que no formato das URIs dos endpoints deve conter:

  • o nome do produto;
  • o módulo;
  • a versão da API;
  • o recurso alvo da funcionalidade em questão.

Modelo de acesso aos Contextos:


/prg


Tomando como exemplo o endpoint de integração do recurso de "Usuários" do aplicativo "Foundation" do produto "Datasul", a URI básica deste serviço deve ser: /prg/sec/v1/users

Para o produto Datasul, o serviço responsável é implementado no contexto /dts/datasul-rest/

Desta forma, a URL final do serviço exemplo acima seria composta da seguinte maneira: 


http://host:port/dts/datasul-rest/resources/prg/sec/v1/users

A URL definida pelo Padrão de API TOTVS segue o seguinte formato:

/dts/datasul-rest/resouces/prg/<módulo>/<versão API>/<recurso>/

Informações que forem passadas após o recurso, serão tratadas como parâmetros PATH.


/api


Para exemplo do funcionamento /api vamos utilizar o mesmo recurso de integração de usuário exemplificado anteriormente.

URI deste serviço deve ser: /api/sec/v1/users

No geral o funcionamento do /api é igual ao /prg, com a ressalva de que o endpoint /api não esta dentro do contexto /dts/datasul-rest/


http://host:port/api/sec/v1/users


A URL definida pelo Padrão de API TOTVS segue o seguinte formato:

/api/<módulo>/<versão API>/<recurso>/

Informações que forem passadas após o recurso, serão tratadas como parâmetros PATH.




Serviço Progress


Para "publicar" a funcionalidade Progress ABL basta criar o programa (.p) com o seguinte caminho: sec/api/v1/users.p (<módulo>/api/<versão API>/<recurso>.p). 

O arquivo *.p é  o fonte original de criação da API, porém para sua real utilização é preciso compilá-lo para se transformar em um arquivo *.r.


A sub-pasta "api" passa então a concentrar todas as funcionalidades de integração do módulo em questão:

Os programas Progress disponibilizados, deverão seguir o padrão de localização abaixo e devem estar compilados, ou seja, é necessário o *.r:

<módulo>/api/<versão API>/<recurso>.r


OBS: Outros caminhos e parâmetros podem ser adicionados a URL, mas sempre de acordo com o Guia de Implementação de APIs.


     O Guia de Implementação de API TOTVS define também que a troca de mensagens é feita (impreterivelmente) no formato JSON, e por conta disso, a troca de mensagens com as funcionalidades Progress também devem ser feitas nesse formato, mais especificamente por meio de um parâmetro de entrada e outro de saída do tipo LONGCHAR que devem ser devidamente tratados (parseados e formatados) pela funcionalidade utilizando as includes utilitárias disponibilizadas:

  • utp/ut-api.i: Faz o parser do parâmetro LONGCHAR de entrada e cria um objeto JsonObject chamado jsonInput.
  • utp/ut-api-action.i: Faz o roteamento do objecto jsonInput para uma procedure interna especificada pelo desenvolvedor.
  • utp/ut-api-notfound.i: Caso nenhuma procedure interna tenha sido encontrada, retorna uma mensagem "Method not found" com HTTP Status 400.

>>>>>Abaixo um exemplo de recurso desenvolvido em Progress ABL para ser utilizado junto ao serviço de API<<<<<<

No início do código estão todas as includes necessárias. O que vale ressaltar neste trecho é referente a include ut-api-action:

  • Nesta include são declaradas as procedures utilizadas na API, por exemplo: pi-send, pi-update.
  • Na mesma declaração é definido qual o método http aplicado, por exemplo: GET, POST, entre outros.
  • Em seguida é definido como o recurso será acessado pela URI, por exemplo: /~*/SEND
  • Isso significa que ao acessar a URI teremos algo como: http://host:port/dts/datasul-rest/resources/prg/sec/v1/users/send onde:


 


{utp/ut-api.i}
{utp/ut-api-action.i pi-send    GET /~*/SEND by=email,address=~* }
{utp/ut-api-action.i pi-update  POST /~* }
{utp/ut-api-action.i pi-find    GET /~* }
{utp/ut-api-action.i pi-default GET }
{utp/ut-api-notfound.i}
  
PROCEDURE pi-send:
    DEF INPUT PARAM jsonInput AS JsonObject NO-UNDO.
    DEF OUTPUT PARAM jsonOutput AS JsonObject NO-UNDO.
     
    DEFINE VARIABLE aJsonArray  AS JsonArray  NO-UNDO.
    DEFINE VARIABLE oJsonObject AS JsonObject NO-UNDO.
 
    aJsonArray = NEW JSONArray().
 
    oJsonObject = NEW JSONObject().
    oJsonObject:ADD("teste", "teste").
    oJsonObject:ADD("teste1", "teste1").
    oJsonObject:ADD("teste2", "teste2").
    aJsonArray:ADD(oJsonObject).
 
    oJsonObject = NEW JSONObject().
    oJsonObject:ADD("teste", "teste").
    oJsonObject:ADD("teste1", "teste1").
    oJsonObject:ADD("teste2", "teste2").
    aJsonArray:ADD(oJsonObject).
 
    jsonOutput = JsonAPIResponseBuilder:ok(aJsonArray, false).
END.
  
PROCEDURE pi-update:
    DEF INPUT PARAM jsonInput AS JsonObject NO-UNDO.
    DEF OUTPUT PARAM jsonOutput AS JsonObject NO-UNDO.

    jsonOutput = NEW JSONObject().
    jsonOutput = jsonInput.
END.
 
PROCEDURE pi-find:
    DEF INPUT PARAM jsonInput AS JsonObject NO-UNDO.
    DEF OUTPUT PARAM jsonOutput AS JsonObject NO-UNDO.
    
    jsonOutput = NEW JSONObject().
    jsonOutput:ADD("method", "GET").
    jsonOutput:ADD("procedure", "pi-find").
    jsonOutput:ADD("description", "Test").
END.

PROCEDURE pi-default:
    DEF INPUT PARAM jsonInput AS JsonObject NO-UNDO.
    DEF OUTPUT PARAM jsonOutput AS JsonObject NO-UNDO.
    
    jsonOutput = NEW JSONObject().
    jsonOutput:ADD("method", "GET").
    jsonOutput:ADD("procedure", "pi-default").
    jsonOutput:ADD("description", "Test").
END.


     No exemplo acima, temos as seguintes includes utilitárias:



Algumas considerações sobre o uso da include de roteamento (ut-api-action):

  • Os roteamentos devem ser definidos do mais específico (detalhado) para o mais genérico (simples);
  • O utilitário faz uso da função MATCHES do Progress, que basicamente permite o uso do ponto "." (ponto) como coringa de uma determinada posição (1 caractere apenas) e o "*" (asterisco) para um conjunto de caracteres variáveis;
  • O caracter de escape "~" deve ser utilizado sempre que necessário, antecedendo caracteres especiais que comprometam a compilação do código progress;
  • Para definir mais de um parâmetro de pesquisa, utilize "," (vírgula) como separador. O processamento de mais de um parâmetro de pesquisa será sempre traduzido para usar o operador AND.
  • Permite o uso de todos métodos HTTP suportados pelo API Manager (GET, POST, PUT, DELETE, PATCH, ...)

A include ut-api.i precisa ser adicionada obrigatoriamente no início do programa Progress, visto que esta include faz uso da instrução USING para importação de classes. Portanto, devido a esta caraterística do Progress ABL, somente será possível adicionar outras includes depois da adição da ut-api.i e ut-api-notfound.i, respectivamente.



Classes utilitárias


Com o objetivo de facilitar a manipulação dos objetos JsonObject recebidos e enviados pela API Progress foram desenvolvidas algumas classes de utilitários:

  • JsonAPIRequestParser - Extrai informações do objeto JSON recebido como parâmetro da requisição.
  • JsonAPIResponse - Trata a criação do objeto JSON de response da requisição.
  • JsonAPIResponseBuilder - Trata a criação do objeto JSON de response da requisição através de um builder.
  • JsonAPIUtils - Utilitário com métodos que facilitam a manipulação de informações relacionados a API



Formato Mensagem JSON


O objeto JsonObject, recebido pela requisição no programa Progress conterá informações completas da requisição, desde informações do:

  • HEADER;
  • QUERY PARAMs;
  • PATH PARAMs;
  • o próprio PAYLOAD e;
  • arquivos MULTIPART.

Com esta mensagem, o desenvolvedor poderá efetuar os devidos filtros e classes utilitárias necessárias. 

Exemplo de mensagem:

{ 
    uri: valor,
	method: GET,
    headers: {},
	pathParams: [ "param1", "param2" ],
	queryParams: { query1: [valor1, valor2], query2: [valor1]},
	payload: { },
	multyPartFile: [ {file: ...}, {file: ...}]
}



Acesso a Diferentes Empresas


O cliente pode fazer acesso a diferentes empresas via BASIC AUTHENTICATION. Para realizar esse processo é necessário primeiramente o login no sistema, e em seguida descobrir todas as empresas do produto, selecionando um dos links a seguir: 

  • (JBoss ou Tomcat): http://<host>:<port>/api/btb/v1/companies
  • (JBoss): http://<host>:<port>/dts/datasul-rest/resources/btb/v1/companies
  • (Tomcat): http://<host>:<port>/dts/datasul-rest/resources/prg/btb/v1/companies


Atenção

Os links acima retornam todas as empresas do produto, então se faz necessário verificar que a empresa utilizada realmente possui vínculo com o usuário logado no sistema, uma vez que não existe controle de vinculo entre empresa e usuário.

Se atente aos campos host e port no link, esses deverão ser modificados pelo usuário de acordo com o ambiente utilizado.

Em novas implementações, de preferência ao endpont com o contexto /api.


A partir do momento que se sabe qual empresa se deseja enviar na requisição e que a mesma está associada ao usuário, é possível fazer a chamada da API que desejar, passando o código como parâmetro dentro do atributo companyId, esse especificado dentro do Header.



GET /api/btb/v1/companies HTTP/1.1
Host: host:port
companyId: 10
Authorization: Basic MTM6MTM=

Informação

A requisição para a API pode ser feita pela ferramenta Postman ou por outra ferramenta que de suporte a requisições feitas pela API.



Login


Na Antiga Arquitetura (Jboss) para realizar o login no contexto DTS/DATASUL-REST (apesar de existir BASIC AUTHENTICATION com o contexto /api), ainda é possível passar como parâmetro usuário e senha para na seguinte URL.

URL para Login

http://localhost:8180/dts/datasul-rest/resources/login?username=<USER>&password=<PASSWORD>

Aviso importante!

Caso a sua aplicação/portal esteja em um servidor diferente do JBOSS Datasul, será necessário informar a propriedade portal.java.naming.security.datasulurl no arquivo datasul_framework.properties conforme exemplo abaixo:
portal.java.naming.security.datasulurl=http://<servidor JBoss Datasul>:<porta>
Isso é necessário pois o login deverá ser feito no servidor JBOSS do Datasul e não no servidor da aplicação/portal.

Para passagem da senha deve ser em Lower Case e é necessário converte-la para SHA1 (utilizando Byte Array), com este resultado realizar a conversão para BASE64. Tendo em vista que a conversão para BASE64 irá trazer caracteres incoerentes para passagem da URL, será necessário fazer a conversão para URL-ENCODE.

  • Senha: testPassword
  • BASE64(SHA1): i7YRj4/Wk1rQh2o740pxfTJwj/0=
  • URL-ENCODE: i7YRj4%2FWk1rQh2o740pxfTJwj%2F0%3D

Observe que por trazer os caracteres " / e = " na conversão de SHA1 e BASE64, é necessário o URL-ENCODE.

Sugestão para URL-ENCODE: https://meyerweb.com/eric/tools/dencoder/


Exemplo de Conversão
DEFINE VARIABLE testPass AS CHARACTER NO-UNDO.
testPass = BASE64-ENCODE(SHA1-DIGEST(LC("testPassword"))).                 
// Utilize uma lógica de conversão URL-ENCODE para variavel testPass

Aviso importante!

Para realizar esse encode NÃO pode utilizar codificação HEXADECIMAL, caso que acontece no Java (processo interno da linguagem), então é preciso utilizar o Byte Array!

Exemplo de URL final

http://localhost:8180/dts/datasul-rest/resources/login?username=teste&password=i7YRj4%2FWk1rQh2o740pxfTJwj%2F0%3D



Para o servidor de aplicação JBOSS é possível realizar a autenticação via BASIC authentication através de duas maneiras 1) por endpoint REST ou 2) pelo componente JOSSO.

ATENÇÃO

Em caso de erro no login por troca do HTTP METHOD de POST para GET ou vice e versa. Será obrigatório o uso do login via componente JOSSO explicado abaixo!

A chamada com header http BASIC no produto Datasul é possível ser feita diretamente na chamada do endpoint. Basta informar corretamente o HEADER e realizar a request.

EXEMPLO-1: chamando o endpoint http://host:port/dts/datasul-rest/resources/btb/v1/companies

Chamada BASIC datasul-rest
GET dts/datasul-rest/resources/btb/v1/companies HTTP/1.1
Host: host
...
Origin: http://host:port
Authorization: BASIC Z3VpdGE6Z3VpdGE=

EXEMPLO-2: chamando o endpoint http://host:port/api/btb/v1/companies

Chamada BASIC api
GET api/btb/v1/companies HTTP/1.1
Host: host
...
Origin: http://host:port
Authorization: BASIC Z3VpdGE6Z3VpdGE=

Em ambo os caso a codificação BASIC é construída com a notação BASE64(username:password)

A chamada com header http BASIC no produto Datasul através do componente JOSSO é feita diretamente na chamada do endpoint de autenticação. Basta informar corretamente o HEADER e realizar a request.

EXEMPLO: chamando o endpoint de autenticação http://host:port/josso/signon/auth.do com BASIC

Request BASIC
GET josso/signon/auth.do HTTP/1.1
Host: host
...
Origin: http://host:port
Authorization: BASIC Z3VpdGE6Z3VpdGE=
Response
{
 "status":"200",
 "description":"Login Authorized - Token Generated",
 "JOSSO_SESSIONID":"C7C4669F68BB35995C4BE13142769FA3",
 "JWT":""
}

Neste caso basta captura o JSON de retorno acima e utilizar o JOSSO_SESSIONID nas demais requisições. Lembrando que a codificação BASIC é construída com a notação BASE64(username:password)

Para acessar o produto DTS4THF com apis devemos utilizar o formato BASIC do http. Para saber mais clique aqui



Logout



Para a arquitetura do JBOSS é possível realizar o logout via as chamadas REST. Isso pode ser feito para que os contextos de trabalho com o josso sejam liberados e que a session seja invalidada no fechamento dos APPs.

Desta maneira, existe a seguinte url para realizar o logout no produto via chamada REST.


Chamar URL de logout
Contexto api
http://host:port/api/logout

Contexto dts/datasul-rest
http://host:port/dts/datasul-rest/resources/logout


13. Como personalizar - Documento de referencia



01. INTRODUÇÃO/OBJETIVO

Temos como objetivo implementar técnicas para facilitar a personalização de telas TOTVS - Linha Datasul de forma lowcode, apenas com cadastro de campos por parte do cliente.



02. VISÃO GERAL

A partir da release 12.1.31 são disponibilizados as técnicas e cadastros para implementar a personalização em telas HTML da linha Datasul.

Nesta técnica de personalização, o desenvolvedor deverá realizar o cadastro dos campos a serem personalizados e criar alguns componentes em PO-UI que utilizem os componentes: PO-DYNAMIC-FORM, PO-DYNAMIC-VIEW e PO-PAGE-DYNAMIC-TABLE (este último somente se for necessário). Deve-se também implementar endpoint em Progress, o qual será utilizado como fonte de dados para os campos personalizados.



03. PRÉ-REQUISITOS

Para utilização desta técnica será necessário possuir conhecimento de desenvolvimento com: APIs REST em Progress, Angular, TypeScript e PO-UI.



04. TÉCNICAS

A Técnica de personalização de telas HTML com PO-UI contempla os seguintes objetos:


Endpoint Progress do Framework


Retorna a lista de campos personalizados que devem ser previamente cadastrados na tela de Personalização em HTML. 

Neste item, deverá ser utilizado o endpoint Progress /api/btb/v1/personalizationView/metadata/ + código_do_programa ,onde deve ser passado o Código do Programa Datasul que conterá a lista de campos personalizados.

Exemplo: Implementação de personalização para o programa pedido-execucao-monitor, deve-se utilizar o endpoint "/api/btb/v1/personalizationView/metadata/pedido-execucao-monitor":

JSon de retorno do Endpoint do Framework
JSon de retorno do Endpoint do Framework
{
    "fields": [
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "cod_idioma",
            "label": "Idioma",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "cod_idiom_padr",
            "label": "Idioma Padrão",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": false,
            "property": "des_idioma",
            "label": "Descrição",
            "type": "string"
        },
        {
            "visible": false,
            "property": "id",
            "type": "number",
            "key": true
        }
    ]
}

Nota!

Atributo KEY: pode-se marcar como "Key":true o que faz parte da chave/identificador do registro.
Assim é possível ter vários campos marcados como Key, independente do nome da propriedade.

Exemplo:

Caso dois campos (empresa=10 e filial=100) sejam marcados com "key":true e concatenar os valores.

Ao buscar o registro completo, se tem a url: <URL>/customers/10|100


Endpoint Progress que devem ser implementados na "área de negócio"

Deve retornar os dados que serão apresentados nos campos personalizados;

Componente PO-UIEndpointTipo RequisiçãoDescrição

po-dynamic-view

po-dynamic-form

/byid/nome_do_programa/idGET

Retorna um registro único.

Recebe no PathParms o "nome do programa" e o "id".

po-page-dynamic-table/nome_do_programaGET

Retorna uma lista de registros.

Recebe no PathParams o "nome do programa".

po-dynamic-form/validateForm/nome_do_programaPOST

Valida o formulário.

Recebe no PathParams o "nome do programa".

po-dynamic-form/nome_do_programaPOST

Efetua a criação de um novo registro. 

Recebe no PathParams o "nome do programa".

po-dynamic-form/nome_do_programa/idPUT

Efetua a alteração do registro.

Recebe no PathParams o "nome do programa" e o "id".

po-dynamic-form/nome_do_programa/idDELETE

Efetua a eliminação do registro.

Recebe no PathParams o "nome do programa" e o "id".

Aviso

Em todas os componentes dinâmicos da tabela apresentada, efetuará uma requisição para obter a lista de campos personalizados na API REST do Framework, com a utilização da requisição:

 "/api/btb/v1/personalizationView/metadata/" + código_do_programa


Utilitário facilitador no Progress

Foi implementado no Progress o utilitário  - btb/personalizationUtil.p - com seu include btb/personalizationUtil.i, que deve ser utilizado para retornar à área de negócio a lista de campos personalizáveis de um determinado programa, cujo o intuito é facilitar a implementação para que seja enviado somente os valores dos campos personalizáveis.

Devido a característica do PO-UI dinâmico, caso seja enviado os dados de campos que não estão na lista de campos, o PO-UI irá apresentar o valor do campo com uma label com o mesmo nome do campo. Com a obtenção da lista de campos pode-se evitar o envio de informações que estão fora da lista de campos personalizados.




- Include btb/personalizationUtil.i


Include btb/personalizationUtil.i
DEFINE TEMP-TABLE ttPersonalization NO-UNDO 
    FIELD codProgDtsul AS CHARACTER
    FIELD codField     AS CHARACTER
    FIELD codType      AS CHARACTER
    FIELD codLabel     AS CHARACTER
    FIELD codValid     AS CHARACTER
    FIELD logReadOnly  AS LOGICAL INITIAL FALSE
    FIELD logEnable    AS LOGICAL INITIAL TRUE
    INDEX codigo IS PRIMARY codProgDtsul codField.

- Procedures disponíveis no programa btb/personalizationUtil.p


ProcedureParâmetrosDescrição/Exemplo
piGetTTPersonalization

INPUT cProg AS CHARACTER

OUTPUT TABLE ttPersonalization

Retorna a temp-table ttPersonalization com a lista de campos personalizáveis de um determinado programa.

Exemplo:

Exemplo de chamada do utilitário btb/personalizationUtil.p
{ btb/personalizationUtil.i }

DEFINE VARIABLE hPers AS HANDLE NO-UNDO.

RUN btb/personalizatinUtil.p PERSISTENT SET hPers.
RUN piGetTTPersonalization IN hPers ("codigo_do_programa", OUTPUT TABLE ttPersonalization).
DELETE PROCEDURE hPers.

FOR EACH ttPersonalization:
    DISPLAY ttPersonalization.
END.
piGetFieldList

INPUT cProg AS CHARACTER

OUTPUT cList AS CHARACTER

OUTPUT cTypeList AS CHARACTER

Retorna duas listas CHARACTER, uma contendo a lista de campos e uma lista dos seus respectivos tipos, de um determinado programa.

Observação: As listas usam como separador a vírgula ",".

Exemplo:

Exemplode chamada de procedure
DEFINE VARIABLE hPers     AS HANDLE    NO-UNDO.
DEFINE VARIABLE cList     AS CHARACTER NO-UNDO.
DEFINE VARIABLE cTypeList AS CHARACTER NO-UNDO.

RUN btb/personalizatinUtil.p PERSISTENT SET hPers.
RUN piGetFieldList IN hPers ("codigo_do_programa", OUTPUT cList, OUTPUT cTypeList).
DELETE PROCEDURE hPers.

MESSAGE cList SKIP cTypeList VIEW-AS ALERT-BOX



Implementação pela área de negócio a interface em PO-DYNAMIC-FORM e PO-DYNAMIC-VIEW.

Todos os componentes dinâmicos do PO-UI realizam, no mínimo, duas requisições REST, uma para obter a lista de campos personalizáveis e outra para obter os dados a serem apresentados nesses campos.


Abaixo temos o papel de cada componente dinâmico que podemos utilizar:

PO-DYNAMIC-FORM: Para a edição e criação de um novo registro personalizado;

PO-DYNAMIC-VIEW: Para a visualização do registro personalizado.

Dica

Pode-se implementar também um componente PO-PAGE-DYNAMIC-TABLE, para a navegação dos registros e permitir a visualização e edição dos mesmos.


Em nossa técnica, em todas as requisições REST, será enviado:

- Para buscar a lista de campos personalizados utilizando o endpoint Progress fornecido pelo framework, que é o /api/btb/v1/personalizationView/metadata/ + codigo_do_programa;

- O código do programa personalizado e também um id do registro corrente para obtenção dos valores, necessários para buscar os valores dos dados a serem apresentados.



Atributos de campos personalizados

Com a configuração de atributos dos campos personalizados, é possível adicionar outras características tais como:


- Alteração de cor;

- Apresentação da ordem do campo no qual será visualizado em tela;

- Validações de interface;

- Máscara de apresentação do campo, entre muitos outros.

Ao clicar no botão Atributos apresentado na grid principal (Opções), uma tela de parametrizações é apresentada para que seja possível adicionar as novas características.

Ao renderizar os campos personalizados em tela, esses atributos serão inclusos no campo personalizado e enviados para tela.

Atributos do PO-UI

Os nomes dos atributos devem ser os mesmos que estão documentados nas propriedades do componente no PO-UI.

Exemplo: Para personalizar um campo no formato CPF, criamos o campo COD_CPF e adicionamos um atributo do tipo mask que conterá o formato 999.999.999-99.

O atributo mask corresponde a uma propriedade do componente PO-INPUT.





Componentes em PO-UI

A seguir, são apresentados exemplos de códigos para a implementação com HTML e TypeScript:


Componente HTML (personalization-detail.component.html)

<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-detail
  p-title="Detalhe do Idioma"
  [p-breadcrumb]="breadcrumb"
  (p-edit)="editClick()"
  (p-back)="goBackClick()"> 
Trecho da personalização
  <!-- INICIO CODIGO PERSONALIZAVEL -->

  <po-dynamic-view
    [p-fields]="fields"
    [p-value]="record">
  </po-dynamic-view>

  <!-- FINAL CODIGO PERSONALIZAVEL -->

</po-page-detail

Componente TypeScript (personalization-detail.component.ts)

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components';

import { PersonalizationService } from '../personalization.service';

@Component({
  selector: 'app-personalization-detail',
  templateUrl: './personalization-detail.component.html',
  styleUrls: ['./personalization-detail.component.css']
})
Variáveis
export class PersonalizationDetailComponent implements OnInit {
  public static cProg = 'html.aplicativos-eai';  
  
  // definicao das variaveis utilizadas
  public currentId: string;
  public fields: Array<any> = [];
  public record = {};
  public showLoading = false;

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;
Construtor
  // construtor com os servicos necessarios
  constructor(
    private service: PersonalizationService,
    private activatedRoute: ActivatedRoute,
    private route: Router
  ) { }   
Leitura do componente
  // load do componente
  public ngOnInit(): void {
    this.activatedRoute.params.subscribe(pars => {
      this.showLoading = true;
      this.record = {};

      // Carrega o registro pelo ID
      // tslint:disable-next-line:no-string-literal
      this.currentId = pars['id'];

      //---BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO ----
      // busca os valores dos dados a serem apresentados
      this.service.loadValuesById(this.currentId).subscribe(resp => {
        Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

        // ---BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA ----
        // carrega a lista de campos personalizados somente apos receber os dados a serem apresentados
        this.service.loadMetadata().subscribe(metadata => {
          // tslint:disable-next-line:no-string-literal
          this.fields = metadata['fields'];
          this.showLoading = false;
        });
      });
    });
    this.setBreadcrumb();
  }
  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', link: '/' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas' , link: '/personalization' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Detalhe do Idioma' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  } 
}
Ação dos botões Editar e Voltar
  // Redireciona quando clicar no botao Edit
  public editClick(): void {
    this.route.navigate(['/personalization', 'edit', this.currentId]);
  }

  // Redireciona quando clicar no botao Voltar
  public goBackClick(): void {
    this.route.navigate(['/personalization']);
  }
}

Componente HTML (personalization-edit.component.html)

<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-edit
  [p-title]="cTitle"
  [p-breadcrumb]="breadcrumb"
  [p-disable-submit]="formEdit.form.invalid"
  (p-cancel)="cancelClick()"
  (p-save)="saveClick()">
Trecho da personalização
  <!-- INICIO CODIGO PERSONALIZAVEL -->

  <po-dynamic-form
    #formEdit
    p-auto-focus="string"
    [p-fields]="fields"
    [p-validate]="validationUrl"
    [p-value]="record">
  </po-dynamic-form>

  <!-- FINAL CODIGO PERSONALIZAVEL -->
</po-page-edit>

Componente TypeScript (personalization-edit.component.ts)

import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb, PoBreadcrumbItem, PoDialogService, PoNotificationService } from '@po-ui/ng-components';

import { PersonalizationService } from './../personalization.service';

@Component({
  selector: 'app-personalization-edit',
  templateUrl: './personalization-edit.component.html',
  styleUrls: ['./personalization-edit.component.css']
})
Variáveis
export class PersonalizationEditComponent implements OnInit {
  
  // Define as variaveis a serem utilizadas
  public cTitle: string;
  public currentId: string;
  public record = {};
  public fields: Array<any> = [];
  public isUpdate = false;
  public showLoading = false;
  public validationUrl = this.service.getUrlAreaValidation();

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;

   // Obtem a referencia do componente HTML
  @ViewChild('formEdit', { static: true })
  formEdit: NgForm;  

Construtor
// Construtor da classe com os servicos necessarios
  constructor(
    private service: PersonalizationService,
    private activatedRoute: ActivatedRoute,
    private route: Router,
    private poDialog: PoDialogService,
    private poNotification: PoNotificationService
  ) { }

Leitura do componente
 // Load do componente
  public ngOnInit(): void {
    this.isUpdate = false;
    this.showLoading = true;

    // Carrega o registro pelo ID
    this.activatedRoute.params.subscribe(pars => {
      // tslint:disable-next-line:no-string-literal
      this.currentId = pars['id'];

      // Se nao tiver o ID definido sera um CREATE
      if (this.currentId === undefined) {
        this.isUpdate = false;
        this.cTitle = 'Novo Idioma';
      } else {
        this.isUpdate = true;
        this.cTitle = 'Edição do Idioma';
      }

      // Atualiza o breadcrumb de acordo com o tipo de edicao
      this.setBreadcrumb();

      // Se for uma alteracao, busca o registro a ser alterado
      if (this.isUpdate) {
        // ----- BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO -----
        this.service.loadValuesById(this.currentId).subscribe(resp => {
          Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

          // ----- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA -----
          // Em alteracao temos que receber o registro para depois buscar a lista de campos
          this.getMetadata();
        });
      } else {
        // Se for create, pega a lista de campos
        this.getMetadata();
      }
    });
  }

  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', action: this.beforeRedirect.bind(this) };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas', action: this.beforeRedirect.bind(this) };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: this.cTitle };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  }

  // Retorna a lista de campos
  private getMetadata() {
    this.service.loadMetadata().subscribe(metadata => {
        // tslint:disable-next-line:no-string-literal
        this.fields = metadata['fields'];
        this.showLoading = false;
    });
 }

  // Redireciona via breadcrumb
  private beforeRedirect(itemBreadcrumbLabel) {
    if (this.formEdit.valid) {
      this.route.navigate(['/']);
    } else {
      this.poDialog.confirm({
        title: 'Cancelamento de edição',
        message: 'Os dados ainda não foram gravados, confirma redirecinamento ?',
        confirm: () => this.route.navigate(['/'])
      });
    }
  }
Gravação do registro
  // Grava o registro quando clicado no botao Salvar
  public saveClick(): void {
    this.showLoading = true;
    if (this.isUpdate) {
      // Altera um registro ja existente
      this.service.update(this.currentId, this.record).subscribe(resp => {
        this.poNotification.success('Dados atualizados com sucesso');
        this.showLoading = false;
        this.route.navigate(['/personalization']);
      });
    } else {
      // Cria um registro novo
      this.service.create(this.currentId, this.record).subscribe(resp => {
        this.poNotification.success('Dados criados com sucesso');
        this.showLoading = false;
        this.route.navigate(['/personalization']);
      });
    }
  }

  // Cancela a edicao e redireciona ao clicar no botao Cancelar
  public cancelClick(): void {
    this.poDialog.confirm({
      title: 'Confirmar cancelamento',
      message: 'Voce deseja realmente cancelar a edição?',
      confirm: () => this.route.navigate(['/personalization'])
    });
  }
}
Cancelamento da edição
  // Cancela a edicao e redireciona ao clicar no botao Cancelar
  public cancelClick(): void {
    this.poDialog.confirm({
      title: 'Confirmar cancelamento',
      message: 'Voce deseja realmente cancelar a edição?',
      confirm: () => this.route.navigate(['/personalization'])
    });
  }
}

Componente HTML (personalization-list.component.html)

<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>
Trecho da personalização
<!-- INICIO CODIGO PERSONALIZAVEL -->
 
<po-page-dynamic-table
  p-auto-router
  p-title="Listagem de Idiomas"
  [p-actions]="actions"
  [p-breadcrumb]="breadcrumb"
  [p-fields]="fields"
  [p-service-api]="serviceApi">
</po-page-dynamic-table>
 
<!-- FINAL CODIGO PERSONALIZAVEL --> 

Componente TypeScript (personalization-list.component.ts)

import { Component, OnInit } from '@angular/core';
import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components';
import { PoPageDynamicTableActions } from '@po-ui/ng-templates';

import { PersonalizationService } from './../personalization.service';

@Component({
  selector: 'app-personalization-list',
  templateUrl: './personalization-list.component.html',
  styleUrls: ['./personalization-list.component.css']
})
Variáveis
export class PersonalizationListComponent implements OnInit {

  // Definicao das variaveis utilizadas
  public serviceApi: string;
  public fields: Array<any> = [];
  public showLoading = false;

  public literals;

  public readonly actions: PoPageDynamicTableActions = {
    new: '/personalization/create',
    detail: '/personalization/detail/:id',
    edit: '/personalization/edit/:id',
    remove: true
  };

  public breadcrumb: PoBreadcrumb = { items: [] };
  public breadcrumbItem: PoBreadcrumbItem;
Construtor
// Construtor da classe
  constructor(
    private service: PersonalizationService
    ) { }
Leitura do componente
 // Load do componente
  public ngOnInit(): void {
    this.fields = [];
    this.serviceApi = this.service.getUrlArea();
    this.showLoading = true;

    // ------- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA --------
    this.service.loadMetadata().subscribe(metadata => {
      // tslint:disable-next-line:no-string-literal
      this.fields = metadata['fields'];
      this.showLoading = false;
    });

    this.setBreadcrumb();
  }

  private setBreadcrumb(): void {
    this.breadcrumbItem = { label: 'Home', link: '/' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
    this.breadcrumbItem = { label: 'Listagem de Idiomas' };
    this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem);
  }
}

Componente de serviço (personalization.service.ts)

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { PoNotificationService } from '@po-ui/ng-components';

@Injectable({
  providedIn: 'root'
})
Endpoints
export class PersonalizationService {
  public progCode = 'html.aplicativos-eai';

  // Endpoint progress do framework para obtencao da lista de campos personalizados
  private urlMetadata = '/api/btb/v1/personalizationView/metadata/';

  // Endpoint progress da area de negocio para obtencao dos valores dos campos personalizados
  private urlArea = '/api/trn/v1/idiomaValues/';
Construtor
// Construtor
constructor(
    private http: HttpClient,
    private poNotification: PoNotificationService,
  ) { }
BUSCA OS CAMPOS PERSONALIZADOS
  // ------- BUSCA OS CAMPOS PERSONALIZADOS DO PROGRAMA --------
  public loadMetadata() {
    return this.http.post<any[]>(this.urlMetadata + this.progCode).pipe();
  }

  // ------- BUSCA OS VALORES DOS CAMPOS PERSONALIZADOS DA AREA DE NEGOCIO --------
  public loadValuesById(cId) {
    // tslint:disable-next-line:whitespace
    return this.http.get<any[]>(this.urlArea + 'byid/' + this.progCode + '/' + cId).pipe();
  }
  public loadAllValues() {
    return this.http.get<any[]>(this.urlArea + this.progCode + '/').pipe();
  }

  public create(cId, record) {
    return this.http.post<any[]>(this.urlArea + this.progCode + '/' + cId, record).pipe();
  }

  public update(cId, record) {
    return this.http.put<any[]>(this.urlArea + this.progCode + '/' + cId, record).pipe();
  }

  public getUrlArea(): string {
    return this.urlArea + this.progCode + '/';
  }

  public getUrlAreaValidation(): string {
    return this.urlArea + 'validateForm/' + this.progCode;
  }
} 



Endpoint em Progress para a área de negócio

A seguir, são apresentados exemplos de endpoint em Progress, que deverão ser implementados pela área de negócio para obtenção dos valores a serem apresentados nos campos personalizados.


Obtenção dos valores dos campos personalizados (idiomaValues.p)

{utp/ut-api.i}

{utp/ut-api-action.i pGetDataById GET /byid/~* }
{utp/ut-api-action.i pGetAll GET /~* }

{utp/ut-api-action.i pValidateForm POST /validateForm/~* }
{utp/ut-api-action.i pCreate POST /~* }

{utp/ut-api-action.i pUpdate PUT /~* }

{utp/ut-api-action.i pDelete DELETE /~* }

{utp/ut-api-notfound.i}

{btb/personalizationUtil.i}

DEFINE VARIABLE oRequest   AS JsonAPIRequestParser NO-UNDO.
DEFINE VARIABLE oResponse  AS JsonAPIResponse      NO-UNDO.
DEFINE VARIABLE oObj       AS JsonObject           NO-UNDO.
DEFINE VARIABLE oList      AS JsonArray            NO-UNDO.
DEFINE VARIABLE oBody      AS JsonObject           NO-UNDO. 

DEFINE VARIABLE cId        AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cProg      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cList      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cTypeList  AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cFld       AS CHARACTER            NO-UNDO.
DEFINE VARIABLE cType      AS CHARACTER            NO-UNDO.
DEFINE VARIABLE ix         AS INTEGER              NO-UNDO.
DEFINE VARIABLE hTab       AS HANDLE               NO-UNDO.
DEFINE VARIABLE hQry       AS HANDLE               NO-UNDO.
DEFINE VARIABLE hPers      AS HANDLE               NO-UNDO.
DEFINE VARIABLE rTab       AS RECID                NO-UNDO.
Endpoint para retorno de valores
// ** Procedure que retorna os valores **
PROCEDURE pGetDataById:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    oObj = NEW JsonObject().

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(2) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(3) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pGetDataById - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pGetDataById - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            NO-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            oObj:add("id", RECID(idioma)).

            // deve somente alimentar os campos que serao personalizados
            hTab = BUFFER idioma:HANDLE.
            FOR EACH ttPersonalization:    
                ASSIGN cFld = ttPersonalization.codField.
                oObj:add(cFld, hTab:BUFFER-FIELD(cFld):buffer-value()) NO-ERROR.
            END.
            hTab:BUFFER-RELEASE().
            DELETE OBJECT hTab NO-ERROR.
        END.
        LOG-MANAGER:WRITE-MESSAGE("pGetDataById - oObj = " + String(oObj:getJsonText()), ">>>>>").
    END.

    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Endpoint para retorno de todos os valores
// ** Procedure que retorna os valores **
PROCEDURE pGetAll:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    oList = NEW JsonArray().

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pGetAll - cProg = " + cProg, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO:
        FOR EACH idioma NO-LOCK:
            oObj = NEW JsonObject().
            oObj:add("id", RECID(idioma)).

            // deve somente alimentar os campos que serao personalizados    
            hTab = BUFFER idioma:HANDLE.
            FOR EACH ttPersonalization:    
                ASSIGN cFld = ttPersonalization.codField.
                oObj:add(cFld, hTab:BUFFER-FIELD(cFld):buffer-value()) NO-ERROR.
            END.
            hTab:BUFFER-RELEASE().
            DELETE OBJECT hTab NO-ERROR.

            oList:add(oObj).
        END.
    END.

    oObj = NEW JSonObject().
    oObj:add("items", oList).

    LOG-MANAGER:WRITE-MESSAGE("pGetAll - oObj = " + String(oObj:getJsonText()), ">>>>>").
    
    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Criação de novo registro na tabela
// ** Procedure que cria um novo registro na tabela **
PROCEDURE pCreate:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO. 
    
    DEFINE VARIABLE cCodIdioma     AS CHARACTER NO-UNDO.
    DEFINE VARIABLE cDesIdioma     AS CHARACTER NO-UNDO.
    DEFINE VARIABLE cCodIdiomPadr  AS CHARACTER NO-UNDO.
    DEFINE VARIABLE rIdioma        AS RECID     NO-UNDO.
    DEFINE VARIABLE lCreated       AS LOGICAL   NO-UNDO INITIAL FALSE.
 
    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().

    rTab = ?.

    // Obtem o programa e o codigo do registro corrente
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pCreate - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pCreate - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        CREATE idioma.
        
        // faz a gravacao dos dados onde os campos personalizados estao com o mesmo nome dos campos da tabela 
        hTab = BUFFER idioma:HANDLE.
        RUN piSetPersonalizationData (hTab, oBody).
        rTab = hTab:RECID.

        hTab:BUFFER-RELEASE() NO-ERROR.
        DELETE OBJECT hTab NO-ERROR.
    END.
    
    // Retorna o ID e se foi criado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', rTab).
    oBody:add('created', (IF lCreated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Atualização de conteúdo do registro pelo ID
 /** Procedure que atualiza o conteudo do registro pelo ID **/ 
PROCEDURE pUpdate:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE lUpdated       AS LOGICAL    NO-UNDO INITIAL FALSE.

    DEFINE VARIABLE oIdioma        AS JsonObject NO-UNDO.
    DEFINE VARIABLE oId            AS JsonObject NO-UNDO.

    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
    oObj     = NEW JsonObject().
   
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pUpdate - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pUpdate - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // se houver algum campo personalizado, busca as informacoes
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            EXCLUSIVE-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            oObj:add("id", RECID(idioma)).
 
            // faz a gravacao dos dados onde os campos personalizados estao com o mesmo nome dos campos da tabela 
            hTab = BUFFER idioma:HANDLE.
            RUN piSetPersonalizationData (hTab, oBody).
            hTab:BUFFER-RELEASE() NO-ERROR.
            DELETE OBJECT hTab NO-ERROR.

            lUpdated = TRUE.
        END.
    END.

    // Retorna o ID e se foi atualizado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', cId).
    oBody:add('updated', (IF lUpdated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Exclusão do registro pelo ID
 // ** Procedure que atualiza o conteudo do registro pelo ID **
PROCEDURE pDelete:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE lDeleted AS LOGICAL NO-UNDO INITIAL FALSE.

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
    oObj     = NEW JsonObject().
   
    cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR.
    cId = oRequest:getPathParams():getCharacter(2) NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("pDelete - cProg = " + cProg, ">>>>>").
    LOG-MANAGER:WRITE-MESSAGE("pDelete - cId = " + cId, ">>>>>").

    // retorna a lista de campos a serem personalizados
    EMPTY TEMP-TABLE ttPersonalization.
    RUN btb/personalizationUtil.p PERSISTENT SET hPers.
    RUN piGetTTPersonalization IN hPers (cProg, OUTPUT table ttPersonalization).
    DELETE PROCEDURE hPers NO-ERROR.

    // somente elimina o registro se houverem campos personalizaveis
    FIND FIRST ttPersonalization NO-LOCK NO-ERROR.
    IF  AVAILABLE ttPersonalization THEN DO TRANSACTION ON ERROR UNDO, LEAVE:
        FIND idioma
            WHERE RECID(idioma) = integer(cId) 
            EXCLUSIVE-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            DELETE idioma.

            ASSIGN lDeleted = TRUE.
        END.
    END.
    
    // Retorna o ID e se foi criado com sucesso
    oObj = NEW JsonObject().
    oObj:add('id', cId).
    oObj:add('deleted', (IF lDeleted THEN 'OK' ELSE 'NOK')).
    
    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
Validação do formulário
 PROCEDURE pValidateForm:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE cProp      AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oValue     AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cValue     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oNewValue  AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oNewFields AS JsonArray            NO-UNDO.
    DEFINE VARIABLE cFocus     AS CHARACTER            NO-UNDO.

    DEFINE VARIABLE oRet       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oMessages  AS JsonArray            NO-UNDO.

    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
   
    // obtem o nome da propriedade que ocorreu o LEAVE para validacao
    cProp      = oBody:getCharacter("property")     NO-ERROR.
    oValue     = oBody:getJsonObject("value")       NO-ERROR.
    cValue     = STRING(oValue:GetCharacter(cProp)) NO-ERROR.
    cId        = oValue:getCharacter("id")          NO-ERROR.
    
    /* Recebemos do HTML o JSON abaixo
    {
        "property": "codAcoes",
        "value": {
            "codIdiomPadr": "01 Português",
            "codIdioma": "12345678",
            "desIdioma": "12345678901234567890",
            "id": 6,
        }
    }
    */

    // Novas Acoes sobre os campos da tela
    
    // oNewValue guarda os valores a serem especificados para os campos
    ASSIGN oNewValue = NEW JsonObject().
    
    // oNewFields guarda a lista de campos que serao alterados/modificados
    ASSIGN oNewFields = NEW JsonArray().
    
    // cFocus especifica em qual campo sera feito o focus
    ASSIGN cFocus = cProp.

    // oMessages guarda as mensagens de retorno formato 
    // { code: '00', message: 'texto', detailedMessage: 'detalhes da mensagem' }
    ASSIGN oMessages = NEW JsonArray().

    //
    // adicinar a logica de validacoes dos campos aqui
    //
   
    ASSIGN oRet = NEW JsonObject().
    // value -> contem todos os valores dos campos de tela
    oRet:add('value', oNewValue).
    // fields -> contem a lista de campos com suas novas propriedades
    oRet:add('fields', oNewFields).
    // focus -> especifica em qual campo o cursor vai ficar posicionado
    oRet:add('focus', cFocus).
    // _messages -> contem uma lista de mensagens que vao aparecer como notificacoes
    oRet:add('_messages', oMessages).
    
    oResponse   = NEW JsonAPIResponse(oRet).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.
 PROCEDURE piSetPersonalizationData:
    DEFINE INPUT PARAMETER hTab  AS HANDLE     NO-UNDO.
    DEFINE INPUT PARAMETER oBody AS JsonObject NO-UNDO. 

    FOR EACH ttPersonalization:
        ASSIGN cFld  = ttPersonalization.codField
               cType = ttPersonalization.codType.
        CASE cType:
            WHEN "string"   THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getCharacter(cFld) NO-ERROR.
            WHEN "number"   THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getInteger(cFld) NO-ERROR.
            WHEN "currency" THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDecimal(cFld) NO-ERROR.
            WHEN "boolean"  THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getLogical(cFld) NO-ERROR.
            WHEN "datetime" THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDatetime(cFld) NO-ERROR.
            WHEN "date"     THEN
                hTab:BUFFER-FIELD(cFld):buffer-value() = oBody:getDate(cFld) NO-ERROR.
        END CASE.
    END.
END PROCEDURE.
    
/* fim */

Envio de valores para PO-UI

Caso sejam enviados valores da área de negócio que não estejam cadastrados como campos personalizados, o PO-UI, o padrão adicionará essa informação extra na tela, onde será apresentado em formato String sem uma label válida.


Tela do componente HTML com o resultado da personalização



Tela de Listagem



Tela de Criação/Edição





Tela de Detalhe/Visualização



Dica

Os fontes de exemplo para a personalização também estão disponíveis em nosso GIT (fwk-totvs-jille) em LINKS ÚTEIS.


05. LINKS ÚTEIS

Documentação API Datasul:

                Desenvolvimento de APIs para o produto Datasul

PO-UI:

Migração THF PO-UI (https://po-ui.io/guides/migration-thf-to-po-ui)

Dynamic-View (https://po-ui.io/documentation/po-dynamic-view);

I18N (https://po-ui.io/documentation/po-i18n)

PO-UI (https://po-ui.io/documentation);

06. CONCLUSÃO

A ideia era apresentar uma técnica de construção para a personalização de um programa TOTVS da Linha Datasul, de forma segura e simples.

Contamos com seu feedback e sugestões para manter a melhoria continua da documentação.




14. Como customizar - Documento de referencia



CONTEÚDO

01. INTRODUÇÃO / OBJETIVO 

Temos como objetivo desta técnica, apresentar um MVP de como customizar as telas HTML, através de intervenções em API Rest no back-end Progress.

Para que caso surja a necessidade do cliente final customizar o resultado de uma tela, ele possa fazer isso de forma dinâmica e em tempo de renderização.

Para isso vamos utilizar o Framework PO-UI como front-end e seus componentes dinâmicos, comunicando com back-end Datasul Progress.

02. VISÃO GERAL

No PO-UI, uma tela dinâmica trabalha recebendo uma lista de campos, que serão apresentados em tela, e uma outra lista contendo os dados que serão apresentados nestes campos.

Nesta técnica, vamos apresentar como fornecer estas duas listas para o PO-UI, onde poderemos customizá-las de acordo com a necessidade.

Este guia será divido basicamente em duas partes, como vamos trabalhar no Back-end Progress e acessar esses dados através do Front-End PO-UI.

Abaixo temos um fluxo das informações do PO-UI até a UPC em Progress:



IMPORTANTE

IMPORTANTE: Esta técnica está disponível a partir da versão 12.1.29 do Framework da Linha Datasul.

03. PRÉ-REQUISITOS

Temos como pré-requisito para execução da técnica citada abaixo: 

  • API Rest desenvolvida no último padrão divulgado pelo Framework, utilizando a técnica de construção de APIs (Desenvolvimento de APIs para o produto Datasul);
  • Utilização do Framework PO-UI na última versão disponível;
  • Utilização do Framework Tomcat Datasul;
  • Utilização da técnica de customização com EPC na api do programa a ser customizado;
  • Tela preparada para a customização;

04. TÉCNICAS

Técnica Back-End Progress:

Introdução:

A técnica Back-end Progress é formada pelos passos abaixo:


Construção de API REST para tela customizada:

Para que possamos customizar uma tela HTML construída em PO-UI, necessitamos que o Back-end nos retorne qual o metadado e os valores da tela em questão através de uma API Rest.

Sendo assim essa API deve conter no mínimo dois endpoints básicos:

  • Endpoint que retornará o metadados da tela;
  • Endpoint para retornar os valores da tela;

Cadastro da API Rest no Cadastro de Programas (men012aa) com a respectiva UPC:

Tendo criado a API REST que retorna os dados básicos para a tela, partimos para o segundo o passo, que é a preparação do endpoint da API para a customização.

Esta API deverá ser cadastrada no cadastro de programas (MEN012AA), onde poderemos também especificar a UPC que será utilizada.

Na técnica de construção de APIs REST é informado sobre a utilização da include "utp/ut-api.i", pois esta include identificará se a API possui uma UPC cadastrada ou não.

IMPORTANTE

IMPORTANTE: A UPC para APIs REST possui um formato diferenciado das UPCs Padrões e de Ponto Estratégico, pois um dos parâmetros utilizados é um JsonObject.


Técnica de customização com EPC

Utilizar a include "include/i-epcrest.i" para chamada UPC na API Rest :

Enfim para chamarmos um programa de customização, criamos uma include que fará esta chamada. Abaixo segue mais detalhes sobre esta include.

Ela encontra-se na pasta include e possui o nome i-epcrest.i, conforme o exemplo abaixo:

{include/i-epcrest.i &endpoint=<nome_end_point> &event=<nome_do_evento> &jsonVar=<variável_jsonObject_com_conteúdo>}

IMPORTANTE

IMPORTANTE: Não é permitido misturar tipos diferentes de UPCs no mesmo programa, pois as assinaturas são incompatíveis e poderão ocorrer erros de parâmetros.


Pré-Processadores da include i-epcrest.i:

Abaixo temos a lista de pré-processadores que devem ser passados para a include i-epcrest.i:

PreprocessadorDescrição
endpointEspecifica o endpoint que esta sendo chamado pelo HTML. Uma API REST deve possuir 1 ou mais endpoints.
eventÉ o nome do evento que esta ocorrendo antes de chamar a UPC. Exemplo: beforeCreate, getAll, getMetaData, etc.
jsonVarÉ a variável do tipo JsonObject que será passada como INPUT-OUTPUT para a UPC.

IMPORTANTE

IMPORTANTE: Todas as UPCs de API REST deverão importar os seguintes pacotes:

                       USING PROGRESS.json.*.

                       USING PROGRESS.json.ObjectModel.*.

                       USING com.totvs.framework.api.*.

Parâmetros recebidos na UPC da API REST:

ParametroTipoTipo de DadosDescrição
pEndPointINPUTCHARACTERContem o nome do endpoint que está sendo executado.
pEventINPUTCHARACTERContem o nome do evento que está sendo executado.
pAPIINPUTCHARACTERContem o nome da API que está sendo executada.
jsonIOINPUT-OUTPUTJSONObjectContem o JSON com os dados (campos ou valores) que poderão ser customizados.

Front-End PO-UI:

Introdução:

Para termos uma tela dinâmica, de acordo com o que o back-end retorna, precisamos utilizar os componentes dinâmicos ou as templates do PO-UI sendo eles:

Componentes:

  • Dynamic-Form;

  • Dynamic-View.

Templates:

  • Page-Dynamic-Detail;
  • Page-Dymic-Edit;
  • Page-Dynamic-Search;
  • Page-Dynamic-Table.

Comunicando com o Back-End Progress:

Basicamente para comunicar com o back-end teremos que ter dois serviços que irão alimentar as informações para tela:

  • Metadado
    • Serviço que irá retornar os campos e as propriedades da tela;
  • Values
    • Serviço que irá retornar os valores dos campos;

05. EXEMPLO DE UTILIZAÇÃO

Back-End Progress

Introdução:

Para exemplificar a técnica citada acima, criamos uma API Rest que irá retornar os dados da tabela de idiomas, chamando uma UPC que acrescenta algumas informações da tabela usuar_mestre.

Cadastro da UPC:

Primeiramente temos que cadastrar a API REST no cadastro de programas (MEN012AA) e também especificar a UPC a ser utilizada, conforme o exemplo abaixo:


Atenção: Ao cadastrar uma API e sua EPC, o nome externo da API/ENDPOINT NÃO deve possuir extensão.


Na aba Opções, teremos que especificar o Template como "API REST", conforme o exemplo abaixo:

API Rest com chamada de UPC:

Abaixo temos um exemplo de API REST com suas procedures que são:

  • pGetMetaData que retorna os metadados do campos em questão;
  • pFindAll que retorna os valores dos campos em questão;
  • pFindById que retorna um registro de um determinado ID;
  • pCreate que cria um novo registro;
  • pUpdateById que altera um registro de um determinado ID;
  • pDeleteById que elimina 1 ou mais registros.

Como podemos verificar todas as procedures possuem chamadas para o programa de UPC:

API REST - idiomas.p
{utp/ut-api.i}

{utp/ut-api-action.i pFindById GET /byid/~* }
{utp/ut-api-action.i pIdiomas GET /translations/~* }
{utp/ut-api-action.i pFindAll GET /~* }

{utp/ut-api-action.i pUpdateById PUT /~* }

{utp/ut-api-action.i pGetMetadata POST /metadata/~* }
{utp/ut-api-action.i pValidateForm POST /validateForm/~* }
{utp/ut-api-action.i pValidateField POST /validateField/~* }
{utp/ut-api-action.i pCreate POST /~* }

{utp/ut-api-action.i pDeleteById DELETE /~* }

{utp/ut-api-notfound.i}

DEFINE TEMP-TABLE ttIdiomas NO-UNDO
    FIELD cod_idioma      LIKE idioma.cod_idioma      SERIALIZE-NAME "codIdioma"
    FIELD des_idioma      LIKE idioma.des_idioma      SERIALIZE-NAME "desIdioma"
    FIELD cod_idiom_padr  LIKE idioma.cod_idiom_padr  SERIALIZE-NAME "codIdiomPadr"
    FIELD dat_ult_atualiz LIKE idioma.dat_ult_atualiz SERIALIZE-NAME "datUltAtualiz"
    FIELD hra_ult_atualiz LIKE idioma.hra_ult_atualiz SERIALIZE-NAME "hraUltAtualiz"
    FIELD id              AS RECID.

/** Procedure que retorna a metadata **/
PROCEDURE pGetMetadata:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oResponse AS JsonAPIResponse NO-UNDO.
    DEFINE VARIABLE oIdiomas  AS JsonArray       NO-UNDO.
    DEFINE VARIABLE oOpts     AS JsonArray       NO-UNDO.
    DEFINE VARIABLE oObj      AS JsonObject      NO-UNDO.
    DEFINE VARIABLE oOpt      AS JsonObject      NO-UNDO.

    ASSIGN oIdiomas = NEW JsonArray().
    
    /* Define a lista de campos a serem apresentados no HTML */
    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'codIdioma').
    oObj:add('label', "~{~{language~}~}").
    oObj:add('visible', TRUE).
    oObj:add('disable', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).
    
    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'desIdioma').
    oObj:add('label', '~{~{description~}~}').
    oObj:add('visible', TRUE).
    oObj:add('required', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).

    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'codIdiomPadr').
    oObj:add('label', '~{~{defaultLanguage~}~}').
    oObj:add('visible', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).

    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'datUltAtualiz').
    oObj:add('label', '~{~{lastUpdate~}~}').
    oObj:add('visible', TRUE).
    oObj:add('format', 'dd/MM/yyyy').
    oObj:add('disable', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('date')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).

    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'hraUltAtualiz').
    oObj:add('label', '~{~{hourLastUpdate~}~}').
    oObj:add('visible', TRUE).
    oObj:add('disable', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).

    // acoes de tela para testes de validacao do formulario
    ASSIGN oOpts = NEW JsonArray().
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageDefaultFocus~}~}').
    oOpt:add('value', 'focoCodIdiomPadr').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{descriptionFocus~}~}').
    oOpt:add('value', 'FocoDesIdioma').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageDefaultDisable~}~}').
    oOpt:add('value', 'DesabilitaCodIdiomaPadrao').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageDefaultEnable~}~}').
    oOpt:add('value', 'HabilitaCodIdiomaPadrao').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{cpfMask~}~}').
    oOpt:add('value', 'MascaraCPF').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{cnpjMask~}~}').
    oOpt:add('value', 'MascaraCNPJ').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{changeValueLanguage~}~}').
    oOpt:add('value', 'TrocaValorDesIdioma').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageHide~}~}').
    oOpt:add('value', 'EsconderDesIdioma').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageShow~}~}').
    oOpt:add('value', 'AparecerDesIdioma').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{errorMessageShow~}~}').
    oOpt:add('value', 'showErrorMessage').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{languageLabelChange~}~}').
    oOpt:add('value', 'mudaLabelDesIdioma').
    oOpts:add(oOpt).

    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'codAcoes').
    oObj:add('label', '~{~{screenActions~}~}').
    oObj:add('visible', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('options', oOpts).
    oObj:add('gridColumns', 12).
    oObj:add('validate', '/api/trn/v1/idiomas/validateField').
    oIdiomas:add(oObj).

    // Informacoes de Tipo de Pessoa
    ASSIGN oOpts = NEW JsonArray().
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{PF~}~}').
    oOpt:add('value', 'f').
    oOpts:add(oOpt).
    ASSIGN oOpt = NEW JsonObject().
    oOpt:add('label', '~{~{PJ~}~}').
    oOpt:add('value', 'j').
    oOpts:add(oOpt).
    
    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'tipUsuario').
    oObj:add('label', '~{~{userType~}~}').
    oObj:add('visible', TRUE).
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('options', oOpts).
    oObj:add('gridColumns', 6).
    oObj:add('validate', '/api/trn/v1/idiomas/validateField').    
    oIdiomas:add(oObj).

    ASSIGN oObj = NEW JsonObject().
    oObj:add('property', 'codCpfCnpj').
    oObj:add('label', '~{~{documentOptions~}~}').
    oObj:add('visible', TRUE).
    oObj:add('mask', '999.999.999-99').
    oObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    oObj:add('gridColumns', 6).
    oIdiomas:add(oObj).
    
    // Adiciona o campo ID na lista de campos para a interface HTML
    // Isso facilitara o gerenciamento do registro na interface HTML
    oIdiomas:add(JsonAPIUtils:getIdField()).

    // Adiciona o JsonArray em um JsonObject para enviar para a UPC
    oObj        = NEW JsonObject().
    oObj:add('root', oIdiomas).
    
    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=getMetaData &event=getMetaData &jsonVar=oObj}    

    // Recupera o JsonArray de dentro do JsonObject retornado pela UPC
    oIdiomas = oObj:getJsonArray('root').    
    
    // Retorna a colecao de campos customizados ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oIdiomas).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/** Procedure que retorna os valores **/
PROCEDURE pFindAll:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oResponse AS JsonAPIResponse NO-UNDO.
    DEFINE VARIABLE oIdiomas  AS JsonArray       NO-UNDO.
    DEFINE VARIABLE oObj      AS JsonObject      NO-UNDO.
    DEFINE VARIABLE oId       AS JsonObject      NO-UNDO.
 
    EMPTY TEMP-TABLE ttIdiomas.

    // Monta a lista de valores dos campos
    FOR EACH idioma NO-LOCK BY idioma.cod_idioma:
        CREATE ttIdiomas.
        BUFFER-COPY idioma TO ttIdiomas.
        // Alimenta o campo ID utilizado pela interface HTML como chave primaria
        ASSIGN ttIdiomas.id = RECID(idioma).
    END.
    
    // Obtem um jsonArray com base no conteudo da temp-table
    oIdiomas    = JsonAPIUtils:convertTempTableToJsonArray(TEMP-TABLE ttIdiomas:HANDLE).

    // Adiciona o JsonArray em um JsonObject para enviar para a UPC
    oObj        = NEW JsonObject().
    oObj:add('root', oIdiomas).
    
    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=findAll &event=findAll &jsonVar=oObj}

    // Recupera o JsonArray de dentro do JsonObject retornado pela UPC
    oIdiomas = oObj:getJsonArray('root').    

    // Retorna a colecao de dados customizados ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oIdiomas).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/** Procedure que retorna 1 registro pelo ID **/ 
PROCEDURE pFindById:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oRequest   AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse  AS JsonAPIResponse      NO-UNDO.
    DEFINE VARIABLE oIdioma    AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oId        AS JsonObject           NO-UNDO.
    
    DEFINE VARIABLE cId        AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE iId        AS INTEGER              NO-UNDO.

    EMPTY TEMP-TABLE ttIdiomas.

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Obtem o ID
    cId = oRequest:getPathParams():getCharacter(2).
    
    // Localiza o registro na tabela IDIOMA pelo ID (recid)
    FIND FIRST idioma 
        WHERE RECID(idioma) = INT(cId)
        NO-LOCK NO-ERROR.
    IF AVAILABLE idioma THEN DO:
        BUFFER-COPY idioma TO ttIdiomas.
        ASSIGN ttIdiomas.id = RECID(idioma).
    END.
    
    // Obtem um jsonArray com base no conteudo da temp-table
    oIdioma     = JsonAPIUtils:convertTempTableFirstItemToJsonObject(TEMP-TABLE ttIdiomas:HANDLE).

    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=findById &event=findById &jsonVar=oIdioma}    
   
    // Retorna o registro customizado ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oIdioma).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/** Procedure que cria um novo registro na tabela **/
PROCEDURE pCreate:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO. 

    DEFINE VARIABLE oBody          AS JsonObject           NO-UNDO. 
    DEFINE VARIABLE oRequest       AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse      AS JsonAPIResponse      NO-UNDO.
    
    DEFINE VARIABLE cCodIdioma     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cDesIdioma     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cCodIdiomPadr  AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE rIdioma        AS RECID                NO-UNDO.
    DEFINE VARIABLE lCreated       AS LOGICAL              NO-UNDO INITIAL FALSE.
 
    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
    
    // Obtem os demais dados
    cCodIdioma    = oBody:getCharacter("codIdioma") NO-ERROR.
    cDesIdioma    = oBody:getCharacter("desIdioma") NO-ERROR.
    cCodIdiomPadr = oBody:getCharacter("codIdiomPadr") NO-ERROR.

    // Cria o registro na tabela IDIOMA
    DO  TRANSACTION
        ON ERROR UNDO, LEAVE:
        FIND FIRST idioma
            WHERE idioma.cod_idioma = cCodIdioma
            NO-LOCK NO-ERROR.
        IF  NOT AVAILABLE idioma THEN DO:
            CREATE idioma.
            ASSIGN idioma.cod_idioma      = cCodIdioma
                   idioma.des_idioma      = cDesIdioma
                   idioma.cod_idiom_padr  = cCodIdiomPadr
                   idioma.dat_ult_atualiz = TODAY
                   idioma.hra_ult_atualiz = STRING(TIME,"HH:MM:SS")
                   rIdioma                = RECID(idioma)
                   lCreated               = TRUE.
        
            // Realiza a chamada da UPC Progress para a criacao do 
            // registro customizado. Nao utilizaremos o retorno da UPC
            // neste caso. 
            {include/i-epcrest.i &endpoint=create &event=afterCreate &jsonVar=oBody}    
        
            RELEASE idioma.
        END.
    END.

    // Retorna o ID e se foi criado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', rIdioma).
    oBody:add('created', (IF lCreated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/** Procedure que atualiza o conteudo do registro pelo ID **/ 
PROCEDURE pUpdateById:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oBody          AS JsonObject           NO-UNDO. 
    DEFINE VARIABLE oRequest       AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse      AS JsonAPIResponse      NO-UNDO.
    DEFINE VARIABLE oIdioma        AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oId            AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cId            AS CHARACTER            NO-UNDO.
    
    DEFINE VARIABLE cCodIdioma     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cDesIdioma     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cCodIdiomPadr  AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE datUltAtualiz  AS DATE                 NO-UNDO.
    DEFINE VARIABLE hraUltAtualiz  AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE lUpdated       AS LOGICAL              NO-UNDO INITIAL FALSE.

    // Le os parametros e os dados enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
   
    // Obtem o ID
    cId      = oRequest:getPathParams():getCharacter(1).

    // Obtem os demais dados
    cCodIdioma    = oBody:getCharacter("codIdioma") NO-ERROR.
    cDesIdioma    = oBody:getCharacter("desIdioma") NO-ERROR.
    cCodIdiomPadr = oBody:getCharacter("codIdiomPadr") NO-ERROR.
    
    // Atualiza o registro na tabela IDIOMA pelo ID (recid)
    DO  TRANSACTION
        ON ERROR UNDO, LEAVE:
        FIND FIRST idioma 
            WHERE RECID(idioma) = INT(cId)
            EXCLUSIVE-LOCK NO-ERROR.
        IF  AVAILABLE idioma THEN DO:
            ASSIGN idioma.des_idioma      = cDesIdioma
                   idioma.cod_idiom_padr  = cCodIdiomPadr
                   idioma.dat_ult_atualiz = TODAY
                   idioma.hra_ult_atualiz = STRING(TIME,"HH:MM:SS")
                   lUpdated               = TRUE.
            
            // Realiza a chamada da UPC Progress para atualizar o registro
            // na tabela cutomizada ou nao. Nao utilizaremos o retorno da UPC
            // neste caso. 
            {include/i-epcrest.i &endpoint=update &event=afterUpdate &jsonVar=oBody}    
        END.
    END.
   
    // Retorna o ID e se foi atualizado com sucesso
    oBody = NEW JsonObject().
    oBody:add('id', cId).
    oBody:add('updated', (IF lUpdated THEN 'OK' ELSE 'NOK')).   

    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oBody).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/** Procedure que atualiza o conteudo do registro pelo ID **/ 
PROCEDURE pDeleteById:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oObj           AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oRequest       AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse      AS JsonAPIResponse      NO-UNDO.
    DEFINE VARIABLE oArray         AS JsonArray            NO-UNDO.
    DEFINE VARIABLE oIdioma        AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oId            AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cId            AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE lDeleted       AS LOGICAL              NO-UNDO INITIAL FALSE.
    DEFINE VARIABLE ix             AS INTEGER              NO-UNDO.

    // Le os parametros enviados pela interface HTML
    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    
    // Eliminacao de registro individual
    IF  oRequest:getPathParams():length > 0 THEN DO:
        // Obtem o ID
        cId = oRequest:getPathParams():getCharacter(1).
        
        RUN piDeleteRecord (cId).
        ASSIGN lDeleted = (RETURN-VALUE = "OK").
    END.
    ELSE DO:
        // Eliminacao de registros em lote
        // Obtem a lista de IDs diretamente do oJsonInput onde vem um JsonArray
        // oArray = oJsonInput:getJsonArray('payload').
        oArray = oRequest:getPayloadArray().
        DO  ix = 1 TO oArray:length:
            oObj = oArray:getJsonObject(ix).
            cId = STRING(oObj:getInteger('id')).
        
            RUN piDeleteRecord (cId).
            IF  lDeleted = FALSE THEN 
                ASSIGN lDeleted = (RETURN-VALUE = "OK").
        END.
    END.
    
    // Retorna o ID e se foi criado com sucesso
    oObj = NEW JsonObject().
    IF  oRequest:getPathParams():length > 0 THEN DO:
        oObj:add('id', cId).
    END.
    oObj:add('deleted', (IF lDeleted THEN 'OK' ELSE 'NOK')).
    
    // Retorna o oBody montado para a interface HTML
    oResponse   = NEW JsonAPIResponse(oObj).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

PROCEDURE piDeleteRecord:
    DEFINE INPUT PARAMETER cId AS CHARACTER NO-UNDO.

    DEFINE VARIABLE oObj           AS JsonObject           NO-UNDO.
    DEFINE VARIABLE lDeleted       AS LOGICAL NO-UNDO INITIAL FALSE.

    LOG-MANAGER:WRITE-MESSAGE("Eliminando registro -> " + cId, ">>>>>").

    // Elimina o registro na tabela IDIOMA pelo ID (recid)
    DO  TRANSACTION
        ON ERROR UNDO, LEAVE:
        FIND FIRST idioma 
            WHERE RECID(idioma) = INT(cId)
            EXCLUSIVE-LOCK NO-ERROR.
        IF AVAILABLE idioma THEN DO:
            // Monta a chave estrangeira para enviar para UPC
            // poder elominar o registro da tabela customizada
            oObj = NEW JsonObject().
            oObj:add('codIdioma', idioma.cod_idioma).
            
            // Realiza a chamada da UPC Progress para a eliminacao do 
            // registro customizado. Nao utilizaremos o retorno da UPC
            // neste caso. 
            {include/i-epcrest.i &endpoint=delete &event=beforeDelete &jsonVar=oObj}    

            DELETE idioma.
           
            ASSIGN lDeleted = TRUE.
        END.
    END.
    
    RETURN (IF lDeleted THEN "OK" ELSE "NOK").
END PROCEDURE.

PROCEDURE pValidateForm:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oRequest   AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse  AS JsonAPIResponse      NO-UNDO.
    DEFINE VARIABLE oBody      AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cProp      AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oValue     AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cValue     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cId        AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oNewValue  AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oNewFields AS JsonArray            NO-UNDO.
    DEFINE VARIABLE cFocus     AS CHARACTER            NO-UNDO.

    DEFINE VARIABLE oRet       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oObj       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oMessages  AS JsonArray            NO-UNDO.

    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
   
    // obtem o nome da propriedade que ocorreu o LEAVE para validacao
    cProp      = oBody:getCharacter("property")     NO-ERROR.
    oValue     = oBody:getJsonObject("value")       NO-ERROR.
    cValue     = STRING(oValue:GetCharacter(cProp)) NO-ERROR.
    cId        = oValue:getCharacter("id")          NO-ERROR.
    
    /* Recebemos do HTML o JSON abaixo
    {
        "property": "codAcoes",
        "value": {
            "codIdiomPadr": "01 Português",
            "codIdioma": "12345678",
            "desIdioma": "12345678901234567890",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "id": 6,
            "codAcoes": "FocoDesIdioma"
        }
    }
    */

    // Novas Acoes sobre os campos da tela
    
    // oNewValue guarda os valores a serem especificados para os campos
    ASSIGN oNewValue = NEW JsonObject().
    
    // oNewFields guarda a lista de campos que serao alterados/modificados
    ASSIGN oNewFields = NEW JsonArray().
    
    // cFocus especifica em qual campo sera feito o focus
    ASSIGN cFocus = cProp.

    // oMessages guarda as mensagens de retorno formato 
    // { code: '00', message: 'texto', detailedMessage: 'detalhes da mensagem' }
    ASSIGN oMessages = NEW JsonArray().
   
    CASE cProp:
        WHEN "codAcoes" THEN DO:
            CASE cValue:
                WHEN 'focoCodIdiomPadr' THEN DO:
                    // setamos o focus para o campo desejado
                    ASSIGN cFocus = 'codIdiomPadr'.
                END.
                WHEN 'FocoDesIdioma' THEN DO:
                    // setamos o focus para o campo desejado
                    ASSIGN cFocus = 'desIdioma'.
                END.
                WHEN 'DesabilitaCodIdiomaPadrao' THEN DO:
                    // criamos um novo field para desabilitar
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'codIdiomPadr').
                    oObj:add('disabled', TRUE).
                    oNewFields:add(oObj).
                END.
                WHEN 'HabilitaCodIdiomaPadrao' THEN DO:
                    // criamos um novo field para habilitar
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'codIdiomPadr').
                    oObj:add('disabled', FALSE).
                    oNewFields:add(oObj).
                END.
                WHEN 'MascaraCPF' THEN DO:
                    // IMPORTANTE:
                    // Quando alteramos o valor do radio-set tipUsuario por aqui,
                    // O value-changed dele que especifica qual a mascara
                    // sera  utilizada NAO sera disparado, pois ele e‚ dinamico e 
                    // estamos validando o campo codAcoes, sendo necessario 
                    // fazermos a formatacao da mascara aqui tambem. 
                    // A mesma regra e valida para o CNPJ
                    
                    // mudamos os valores dos campos desejados
                    oNewValue:add('tipUsuario', 'f').
                    oNewValue:add('codCpfCnpj', FILL('0',11)).

                    // criamos um novo field para mudar a mascara
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'codCpfCnpj').
                    oObj:add('mask', '999.999.999-99').
                    oNewFields:add(oObj).
                END.
                WHEN 'MascaraCNPJ' THEN DO:
                    // IMPORTANTE:
                    // Quando alteramos o valor do radio-set tipUsuario por aqui,
                    // O value-changed dele que especifica qual a mascara
                    // sera  utilizada NAO sera disparado, pois ele e‚ dinamico e 
                    // estamos validando o campo codAcoes, sendo necessario 
                    // fazermos a formatacao da mascara aqui tambem. 
                    // A mesma regra e valida para o CPF

                    // alteramos os valores dos campos desejados
                    oNewValue:add('tipUsuario', 'j').
                    oNewValue:add('codCpfCnpj', FILL('0',15)).

                    // criamos um novo field para mudar a mascara
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'codCpfCnpj').
                    oObj:add('mask', '99.999.999/9999-99').
                    oNewFields:add(oObj).
                END.
                WHEN 'TrocaValorDesIdioma' THEN DO:
                    // alteramos o conteudo de um campo qualquer
                    oNewValue:add('desIdioma', "Valor retornado do backend de validacao").
                END.
                WHEN 'EsconderDesIdioma' THEN DO:
                    // criamos um novo field para tornar invisivel o campo
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'desIdioma').
                    oObj:add('visible', FALSE).
                    oNewFields:add(oObj).
                END.
                WHEN 'AparecerDesIdioma' THEN DO:
                    // criamos um novo field para tornar visivel o campo
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'desIdioma').
                    oObj:add('visible', TRUE).
                    oNewFields:add(oObj).
                END.
                WHEN 'mudaLabelDesIdioma' THEN DO:
                    // criamos um novo field para mudar o label
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('property', 'desIdioma').
                    oObj:add('label', 'Label alterado da descricao').
                    oNewFields:add(oObj).
                END.
                WHEN 'showErrorMessage' THEN DO:
                    // criamos uma mensagem de erro
                    ASSIGN oObj = NEW JsonObject().
                    oObj:add('code', '33').
                    oObj:add('message', 'A Descricao do idioma nao foi preenchida corretamente'). 
                    oObj:add('detailedMessage', 'Detalhe da mensagem de erro').
                    oMessages:add(oObj).
                END.
            END CASE.
        END.
        WHEN "tipUsuario" THEN DO:
            // setamos o focus para o campo desejado
            ASSIGN cFocus = 'codCpfCnpj'.

            // criamos um field para mudar o informar o campo e a nova mascara
            ASSIGN oObj = NEW JsonObject().
            oObj:add('property', "codCpfCnpj").
            IF  cValue = "j" THEN DO:
                //definido um novo valor para o CNPJ
                oNewValue:add('codCpfCnpj', FILL('0',15)).
                // ‚ alterado o formato da mascara de edicao
                oObj:add('mask', '99.999.999/9999-99').
            END.
            IF  cValue = "f" THEN DO:
                //definido um novo valor para o CPF
                oNewValue:add('codCpfCnpj', FILL('0',11)).
                //alterado o formato da mascara de edicao
                oObj:add('mask', '999.999.999-99').
            END.
            oNewFields:add(oObj).
        END.
    END CASE.
    
    ASSIGN oRet = NEW JsonObject().
    // value -> contem todos os valores dos campos de tela
    oRet:add('value', oNewValue).
    // fields -> contem a lista de campos com suas novas propriedades
    oRet:add('fields', oNewFields).
    // focus -> especifica em qual campo o cursor vai ficar posicionado
    oRet:add('focus', cFocus).
    // _messages -> contem uma lista de mensagens que vao aparecer como notificacoes
    oRet:add('_messages', oMessages).
    
    // encapsulamos o retorno para enviar para a UPC
    oObj = NEW JsonObject().
    oObj:add("property", cProp).
    oObj:add("originalValues", oValue).
    oObj:add("root", oRet).

    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=validateForm &event=validateForm &jsonVar=oObj}    

    // obtem o retorno customizado, onde o mesmo foi alterado e retornado na tag root 
    oRet = oObj:getJsonObject("root").

    /* JSON de retorno para o HTML      
    value: {
      desIdioma: 'teste de escrita',
      hraUltAtualiz: '17:18:19'
    },
    fields: [
      {
        property: 'codCpfCnpj', 
        mask: '99.999.999/9999-99' 
      }
    ],
    focus: 'hraUltAtualiz',
    _messages: [ 
        { 
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        } 
    ]
    */
    
    // Retorna a colecao de campos customizados ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oRet).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

PROCEDURE pValidateField:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject NO-UNDO.

    DEFINE VARIABLE oRequest   AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oResponse  AS JsonAPIResponse      NO-UNDO.
    DEFINE VARIABLE oBody      AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cProp      AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oValue     AS JsonObject           NO-UNDO.
    DEFINE VARIABLE cValue     AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE cId        AS CHARACTER            NO-UNDO.
    DEFINE VARIABLE oNewValue  AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oNewField  AS JsonObject           NO-UNDO.
    DEFINE VARIABLE lFocus     AS LOGICAL              NO-UNDO INITIAL FALSE.

    DEFINE VARIABLE oRet       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oObj       AS JsonObject           NO-UNDO.
    DEFINE VARIABLE oMessages  AS JsonArray            NO-UNDO.

    oRequest = NEW JsonAPIRequestParser(oJsonInput).
    oBody    = oRequest:getPayload().
   
    // obtem o nome da propriedade que ocorreu o LEAVE para validacao
    cProp      = oBody:getCharacter("property")     NO-ERROR.
    cValue     = oBody:getCharacter("value")        NO-ERROR.
    
    /* Recebemos do HTML o JSON abaixo
    {
        "property": "codAcoes",
        "value": "FocoDesIdioma"
    }
    */

    // Novas Acoes sobre os campos da tela
    
    // oNewField guarda o objeto que sera alterado/modificado
    ASSIGN oNewField = NEW JsonObject().
    
    // oMessages guarda as mensagens de retorno formato 
    // { code: '00', message: 'texto', detailedMessage: 'detalhes da mensagem' }
    ASSIGN oMessages = NEW JsonArray().

    IF  cProp = "tipUsuario" THEN DO:
        ASSIGN lFocus = TRUE.
        
        oNewField:add('property', "codCpfCnpj").
        IF  cValue = "j" THEN DO:
            //alterado o formato da mascara de edicao
            oNewField:add('mask', '99.999.999/9999-99').
        END.
        IF  cValue = "f" THEN DO:
            //alterado o formato da mascara de edicao
            oNewField:add('mask', '999.999.999-99').
        END.

        ASSIGN oObj = NEW JsonObject().
        oObj:add('code', '33').
        oObj:add('message', 'A mascara do CPF/CNPJ foi ajustada'). 
        oObj:add('detailedMessage', 'Ocorreu um ajusta na mascara do CPF/CNPJ, favor verificar se os dados estao corretos').
        oMessages:add(oObj).
    END.
    
    ASSIGN oRet = NEW JsonObject().
    // value -> contem todos os valores dos campos de tela
    oRet:add('value', cValue).
    // field -> contem os novos atributos do campo atual
    oRet:add('field', oNewField).
    // focus -> especifica se o campo recebe o focu
    oRet:add('focus', lFocus).
    // _messages -> contem uma lista de mensagens que vao aparecer como notificacoes
    oRet:add('_messages', oMessages).
    
    // encapsulamos o retorno para enviar para a UPC
    oObj = NEW JsonObject().
    oObj:add("property", cProp).
    oObj:add("root", oRet).

    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=validateField &event=validateField &jsonVar=oObj}    

    // obtem o retorno customizado, onde o mesmo foi alterado e retornado somente 
    // o conteudo da tag return
    oRet = oObj:getJsonObject("root").

    /* JSON de retorno para o HTML      
    value: 'teste de escrita',
    field: {
        mask: '99.999.999/9999-99',
        required: true 
    },
    focus: true,
    _messages: [
        {
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        }
    ]
    */
    
    // Retorna a colecao de campos customizados ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oRet).
    oJsonOutput = oResponse:createJsonResponse().
END PROCEDURE.

/**
    Recupera as literais
*/
PROCEDURE pIdiomas:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject    NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject    NO-UNDO.
    
    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=i18n &event=i18n &jsonVar=oJsonInput}
    
    ASSIGN oJsonOutput = oJsonInput.
END PROCEDURE.


/* fim */


Programa UPC:

Abaixo temos um exemplo de uma UPC criada para a API REST:

Exemplo de UPC da API REST - idiomas_upc.p
/**************************************************************************
** idiomas_upc.p - Exemplo de epc para Endpoints REST 
** 18/05/2020 - Menna - Criado exemplo
***************************************************************************/

USING PROGRESS.json.*.
USING PROGRESS.json.ObjectModel.*.
USING com.totvs.framework.api.*.

DEFINE INPUT        PARAMETER pEndPoint AS CHARACTER  NO-UNDO.
DEFINE INPUT        PARAMETER pEvent    AS CHARACTER  NO-UNDO.
DEFINE INPUT        PARAMETER pAPI      AS CHARACTER  NO-UNDO.
DEFINE INPUT-OUTPUT PARAMETER jsonIO    AS JSONObject NO-UNDO.

DEFINE VARIABLE jObj            AS JsonObject NO-UNDO.
DEFINE VARIABLE oOriginalValues AS JSonObject NO-UNDO.
DEFINE VARIABLE oReturn         AS JSonObject NO-UNDO.
DEFINE VARIABLE oValues         AS JSonObject NO-UNDO.
DEFINE VARIABLE oFieldObj       AS JSonObject NO-UNDO.

DEFINE VARIABLE oFields         AS JSonArray  NO-UNDO.
DEFINE VARIABLE oMessages       AS JSonArray  NO-UNDO.

DEFINE VARIABLE ix              AS INTEGER    NO-UNDO.
DEFINE VARIABLE iTot            AS INTEGER    NO-UNDO.

DEFINE VARIABLE cCodIdioma      AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cCodUsuario     AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cNomUsuario     AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cCodDialet      AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cProp           AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cFocus          AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cOriginalValue  AS CHARACTER  NO-UNDO.
DEFINE VARIABLE cValue          AS CHARACTER  NO-UNDO.

DEFINE VARIABLE lFocus          AS LOGICAL    NO-UNDO INITIAL FALSE.

/* ***************************  Main Block  *************************** */

LOG-MANAGER:WRITE-MESSAGE("UPC EndPoint = " + pEndPoint, ">>>>").
LOG-MANAGER:WRITE-MESSAGE("UPC Event = " + pEvent, ">>>>").

// Carrega as definicoes dos campos customizados da tabela
IF  pEndPoint = "getMetaData"
AND pEvent    = "getMetaData" THEN DO ON STOP UNDO, LEAVE:
    RUN piGetMetaData.
END.

// Carrega os valores dos campos customizados das tabelas
IF  pEndPoint = "findAll"
AND pEvent    = "findAll" THEN DO ON STOP UNDO, LEAVE:
    RUN piFindAll.
END.

IF  pEndPoint = "findById"
AND pEvent    = "findById" THEN DO ON STOP UNDO, LEAVE:
    RUN piFindById.
END.

IF  pEndPoint = "create"
AND pEvent    = "afterCreate" THEN DO ON STOP UNDO, LEAVE:
    RUN piCreate.
END.

IF  pEndPoint = "update"
AND pEvent    = "afterUpdate" THEN DO ON STOP UNDO, LEAVE:
    RUN piUpdate.
END.

IF  pEndPoint = "delete"
AND pEvent    = "beforeDelete" THEN DO ON STOP UNDO, LEAVE:
    RUN piDelete.
END.

IF  pEndPoint = "validateForm"
AND pEvent    = "validateForm" THEN DO ON STOP UNDO, LEAVE:
    RUN piValidateForm.
END.

IF  pEndPoint = "validateField"
AND pEvent    = "validateField" THEN DO ON STOP UNDO, LEAVE:
    RUN piValidateField.
END.

IF  pEndPoint = "i18n"
AND pEvent    = "i18n" THEN DO ON STOP UNDO, LEAVE:
    RUN piI18N.
END.

RETURN "OK".

PROCEDURE piGetMetaData: 
    // Obtem a lista de campos e valores    
    ASSIGN oFields = jsonIO:getJsonArray('root').

    // Cria os novos campos na lista
    ASSIGN jObj = NEW JsonObject().
    jObj:add('divider', "Itens da UPC").
    jObj:add('property', 'codUsuario').
    jObj:add('label', '~{~{user~}~}').
    jObj:add('visible', TRUE).
    jObj:add('required', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    oFields:add(jObj).
    
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'nomUsuario').
    jObj:add('label', '~{~{name~}~}').
    jObj:add('visible', TRUE).
    jObj:add('required', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    oFields:add(jObj).

    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'codDialet').
    jObj:add('label', '~{~{dialect~}~}').
    jObj:add('visible', FALSE). // <- Remove o item da tela de todos seus correspondentes (Form, View, Table)
    jObj:add('required', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    oFields:add(jObj).
    
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'testeValidacaoRegEx').
    jObj:add('label', '~{~{regexTestValidation~}~}').
    jObj:add('gridColumns', 6).
    jObj:add('pattern', "[0-9]~{2~}"). // <- Validacao RegEx
    jObj:add('errorMessage', 'Obrigatório mínimo 2 n£meros consecutivos.').
    oFields:add(jObj).

    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'numberRangeValidate').
    jObj:add('label', '~{~{cpfMaskApply~}~}').
    jObj:add('mask', '999.999.999-99').  // <-- Mascara CPF
    jObj:add('visible', TRUE).
    jObj:add('required', FALSE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    oFields:add(jObj).
    
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'numberValidate').
    jObj:add('label', '~{~{onlyNumbers~}~}').
    jObj:add('visible', TRUE).
    jObj:add('required', FALSE).
    jObj:add('minValue', 1).
    jObj:add('maxValue', 9).
    jObj:add('errorMessage', 'Somente números de 1 a 9'). // <- Mensagem de erro 1-9
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('integer')). // <- Restringe a digitacao somente numeros
    jObj:add('gridColumns', 6).
    oFields:add(jObj).
    
    // Retorna a nova lista com os campos customizados
    jsonIO:Set("root", oFields).
END PROCEDURE.

PROCEDURE piFindAll:
    // Obtem a lista de campos e valores    
    ASSIGN oFields = jsonIO:getJsonArray('root').

    LOG-MANAGER:WRITE-MESSAGE("UPC FINDALL", ">>>>").

    FIND FIRST usuar_mestre NO-LOCK NO-ERROR.

    // Armazena o tamanho da lista em variavel para evitar LOOP devido a adicionar novos itens na lista
    ASSIGN iTot = oFields:length.

    DO  ix = 1 TO iTot:
        ASSIGN jObj = oFields:GetJsonObject(ix).
        
        // Alimenta os novos dados
        IF  AVAILABLE usuar_mestre THEN DO:
            jObj:add('codUsuario', usuar_mestre.cod_usuario) NO-ERROR.
            jObj:add('nomUsuario', usuar_mestre.nom_usuario) NO-ERROR.
            jObj:add('codDialet', usuar_mestre.cod_dialet) NO-ERROR.
        END.
        
        // Atualiza o objeto na lista
        oFields:set(ix, jObj).
        
        FIND NEXT usuar_mestre NO-LOCK NO-ERROR.
    END.

    // Retorna o json ROOT a lista nova com novos dados customizados 
    jsonIO:Set("root", oFields).
END PROCEDURE.

PROCEDURE piFindById:
    // Obtem as informacoes necessarias da API para retornar dados    
    cCodIdioma  = jsonIO:getCharacter("codIdioma"). // chave estrangeira

    LOG-MANAGER:WRITE-MESSAGE("UPC FINDBYID cod_idioma= " + cCodIdioma, ">>>>").

    // Adiciona os valores da tabela customizada no retorno
    FIND FIRST usuar_mestre NO-LOCK NO-ERROR.
    IF  AVAILABLE usuar_mestre THEN DO:
        jsonIO:add('codUsuario', usuar_mestre.cod_usuario) NO-ERROR.
        jsonIO:add('nomUsuario', usuar_mestre.nom_usuario) NO-ERROR.
        jsonIO:add('codDialet', usuar_mestre.cod_dialet) NO-ERROR.
    END.
END PROCEDURE.

PROCEDURE piCreate:
    // Obtem as informacoes necessarias da API para criacao do registro    
    cCodIdioma  = jsonIO:getCharacter("codIdioma") NO-ERROR. // chave estrangeira
    cCodUsuario = jsonIO:getCharacter("codUsuario") NO-ERROR.
    cNomUsuario = jsonIO:getCharacter("nomUsuario") NO-ERROR.
    cCodDialet  = jsonIO:getCharacter("codDialet") NO-ERROR.

    LOG-MANAGER:WRITE-MESSAGE("UPC CREATE cod_idioma= " + cCodIdioma, ">>>>").
    LOG-MANAGER:WRITE-MESSAGE("UPC CREATE cod_usuario= " + cCodUsuario, ">>>>").
    
    // logica de CREATE
    /* Em comentario a logica para nao criar registros desnecessariamente
    FIND FIRST usuar_mestre
        WHERE usuar_mestre.cod_usuario = cCodUsuario
        EXCLUSIVE-LOCK NO-ERROR.
    IF  NOT AVAILABLE usuar_mestre THEN DO:
        ASSIGN usuar_mestre.nom_usuario = cNomUsuario
               usuar_mestre.cod_dialet  = cCodDialet. 
    END.
    */
END PROCEDURE.

PROCEDURE piUpdate:
    // Obtem as informacoes necessarias da API para atualizacao    
    cCodIdioma  = jsonIO:getCharacter("codIdioma") NO-ERROR. // chave estrangeira
    cCodUsuario = jsonIO:getCharacter("codUsuario") NO-ERROR.
    cNomUsuario = jsonIO:getCharacter("nomUsuario") NO-ERROR.
    cCodDialet  = jsonIO:getCharacter("codDialet") NO-ERROR.
    
    LOG-MANAGER:WRITE-MESSAGE("UPC UPDATE cod_idioma= " + cCodIdioma, ">>>>").
    LOG-MANAGER:WRITE-MESSAGE("UPC UPDATE cod_usuario= " + cCodUsuario, ">>>>").

    // logica de UPDATE
    /* Em comentario a logica para nao alterar tabelas desnecessariamente
    FIND FIRST usuar_mestre
        WHERE usuar_mestre.cod_usuario = cCodUsuario
        EXCLUSIVE-LOCK NO-ERROR.
    IF  AVAILABLE usuar_mestre THEN DO:
        ASSIGN usuar_mestre.nom_usuario = cNomUsuario
               usuar_mestre.cod_dialet  = cCodDialet. 
    END.
    */
END PROCEDURE.

PROCEDURE piDelete:
    // obtem as informacoes necessarias da API para eliminacao    
    cCodIdioma  = jsonIO:getCharacter("codIdioma"). // chave estrangeira
    
    LOG-MANAGER:WRITE-MESSAGE("UPC DELETE cod_idioma= " + cCodIdioma, ">>>>").

    // logica de DELETE
    /* Em comentario a logica para nao eliminar o registro desnecessariamente
    FIND FIRST usuar_mestre
        WHERE usuar_mestre.cod_usuario = cCodUsuario
        EXCLUSIVE-LOCK NO-ERROR.
    IF  AVAILABLE usuar_mestre THEN DO:
        delete usuar_mestre.
    END.
    */
END PROCEDURE.

PROCEDURE piValidateForm:
    cProp = jsonIO:getCharacter("property") NO-ERROR. // o cProp contem o nome da propriedade que esta sendo validada
    oOriginalValues = jsonIO:getJsonObject("originalValues") NO-ERROR. // obtem os valores dos campos que vieram da tela html

    LOG-MANAGER:WRITE-MESSAGE("UPC ValidateForm property= " + cProp, ">>>>").

    oReturn = jsonIO:getJsonObject("root") NO-ERROR. // obtem o retorno que sera enviado para a tela html
    oValues = oReturn:getJsonObject("value") NO-ERROR. // obtem os valores dos campos ja ajustados
    oFields = oReturn:getJsonArray("fields") NO-ERROR. // obtem as propriedades dos campos a serem alteradas
    cFocus = oReturn:getCharacter("focus") NO-ERROR. // obtem o campo de focus a ser retornado para a tela html
    oMessages = oReturn:getJsonArray("_messages") NO-ERROR. // obtem as mensagens a serem retornados para a tela html
    
    /* Exemplo de JSON que veio para a UPC
    { 
        property: 'codAcao',
        originalValues: {
            "codIdiomPadr": "01 Português",
            "codIdioma": "12345678",
            "desIdioma": "12345678901234567890",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "id": 6,
            "codAcoes": "FocoDesIdioma"
        },
        root: {
            value: {
              desIdioma: 'teste de escrita',
              hraUltAtualiz: '17:18:19'
            },
            fields: [
              {
                property: 'codCpfCnpj', 
                mask: '99.999.999/9999-99' 
              }
            ],
            focus: 'hraUltAtualiz',
            _messages: [ 
                { 
                    code: '01', 
                    message: 'Mensagem do erro que aconteceu', 
                    detailedMessage: 'detalhes do erro acontecido' 
                } 
            ]
        }
    }
    */

    IF cProp = "desIdioma" THEN DO:
        cCodIdioma  = oOriginalValues:getCharacter("codIdioma"). // chave estrangeira
        IF  cCodIdioma = "12345678" THEN DO:
            oValues:add("desIdioma", "Valor customizado na UPC").
            oValues:add("hraUltAtualiz", "17:18:19").
            
            // criamos um novo field para desabilitar
            ASSIGN jObj = NEW JsonObject().
            jObj:add('property', 'codIdiomPadr').
            jObj:add('disabled', TRUE).
            oFields:add(jObj).
            
            ASSIGN cFocus = "desIdioma".
            
            ASSIGN jObj = NEW JsonObject().
            jObj:add('code', '44').
            jObj:add('message', 'A UPC alterou algumas caracteristica da tela.'). 
            jObj:add('detailedMessage', 'Na execução da UPC, houveram alterações nos campos de tela.').
            oMessages:add(jObj).
        END.
    END.
    
    /* Exemplo de JSON de retorno para o HTML      
    value: {
      desIdioma: 'Valor customizado na UPC'
      hraUltAtualiz: '17:18:19'
    },
    fields: [
      {
        property: 'codCpfCnpj', 
        mask: '99.999.999/9999-99' 
      }
    ],
    focus: 'hraUltAtualiz',
    _messages: [ 
        { 
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        } 
    ]
    */

    // atribui os valores de volta para a tela HTML
    jsonIO = NEW JSonObject().
    
    oReturn = NEW JSonObject().
    oReturn:add("value", oValues). // seta os valores dos campos ja ajustados
    oReturn:add("fields", oFields). // seta as propriedades dos campos a serem alteradas
    oReturn:add("focus", cFocus). // seta o campo de focus a ser retornado para a tela html
    oReturn:add("_messages", oMessages). // seta as mensagens a serem retornadas para a tela html

    jsonIO:add("root", oReturn).
END PROCEDURE.

PROCEDURE piValidateField:
    cProp = jsonIO:getCharacter("property") NO-ERROR. // o cProp contem o nome da propriedade que esta sendo validada

    LOG-MANAGER:WRITE-MESSAGE("UPC ValidateField property= " + cProp, ">>>>").

    oReturn = jsonIO:getJsonObject("root") NO-ERROR. // obtem o retorno que sera enviado para a tela html
    cValue = oReturn:getCharacter("value") NO-ERROR. // pega o novo valor do campo atual
    oFieldObj = oReturn:getJsonObject("field") NO-ERROR. // obtem as propriedades dos campos a serem alteradas
    lFocus = oReturn:getLogical("focus") NO-ERROR. // obtem se o focus ficara sobre o mesmo campo ao retornar para a tela html
    oMessages = oReturn:getJsonArray("_messages") NO-ERROR. // obtem as mensagens a serem retornados para a tela html
    
    /* Exemplo de JSON que veio para a UPC
    { 
        property: 'codAcao',
        root: {
            value: '',
            field: {
                mask: '99.999.999/9999-99' 
            },
            focus: false,
            _messages: [ 
                { 
                    code: '01', 
                    message: 'Mensagem do erro que aconteceu', 
                    detailedMessage: 'detalhes do erro acontecido' 
                } 
            ]
        }
    }
    */

    IF cProp = "codAcoes" THEN DO:
        oFieldObj = NEW JsonObject().
        oFieldObj:add('label', 'Novo label').
        oFieldObj:add('required', TRUE).

        ASSIGN lFocus = TRUE
               cValue = "FocoDesIdioma".
        
        ASSIGN jObj = NEW JsonObject().
        jObj:add('code', '44').
        jObj:add('message', 'A UPC alterou algumas caracteristica da tela.'). 
        jObj:add('detailedMessage', 'Na execução da UPC, houveram alterações nos campos de tela.').
        oMessages:add(jObj).
    END.
    
    /* Exemplo de JSON de retorno para o HTML      
    value: 'FocoDesIdioma',
    field: {
        label: 'Novo Label', 
        required: true 
    },
    focus: true,
    _messages: [ 
        { 
            code: '44', 
            message: 'A UPC alterou algumas caracteristica da tela.', 
            detailedMessage: 'Na execução da UPC, houveram alterações nos campos de tela.' 
        } 
    ]
    */

    // atribui os valores de volta para a tela HTML
    jsonIO = NEW JSonObject().
    
    oReturn = NEW JSonObject().
    oReturn:add("value", cValue). // mantem o valor seta os valores dos campos ja ajustados
    oReturn:add("field", oFieldObj). // seta as propriedades dos campos a serem alteradas
    oReturn:add("focus", lFocus). // seta o focus a ser retornado para a tela html
    oReturn:add("_messages", oMessages). // seta as mensagens a serem retornadas para a tela html

    jsonIO:add("root", oReturn).
END PROCEDURE.

PROCEDURE piI18N:
    DEFINE VARIABLE oParser      AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oQueryParams AS JsonObject           NO-UNDO.
    DEFINE VARIABLE pIdioma      AS CHARACTER            NO-UNDO.
    
    ASSIGN 
        oParser      = NEW JsonAPIRequestParser(jsonIO) 
        oQueryParams = oParser:GetQueryParams()
        pIdioma      = oQueryParams:GetJsonArray("language"):GetCharacter(1).
    
    IF (pIdioma = "pt-BR") THEN DO:
        jsonIO = NEW JsonObject().
        jsonIO:Add("user", "Usuário").
        jsonIO:Add("name", "Nome").
        jsonIO:Add("regexTestValidation", "Teste Validação REGEX").
        jsonIO:Add("cpfMaskApply", "Aplicação Máscara CPF").
        jsonIO:Add("onlyNumbers", "Somente Números").
    END.
    ELSE IF (pIdioma = "en-US") THEN DO:
        jsonIO = NEW JsonObject().
        jsonIO:Add("user", "User").
        jsonIO:Add("name", "Name").
        jsonIO:Add("regexTestValidation", "REGEX Test Validation").
        jsonIO:Add("cpfMaskApply", "CPF Apply Mask").
        jsonIO:Add("onlyNumbers", "Only Numbers").
    END.
END PROCEDURE.

/* fim */

Resultado ao chamar ao API tendo uma UPC cadastrada:

Ao fazer as requisições, virão os seguintes resultados na UPC.

Resultado de leituras no backend Progress
Busca do METADADOS onde foram adicionados os novos campos codUsuario, nomUsuario e codDialet:

POST - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas/metadata

{
    "total": 9,
    "hasNext": false,
    "items": [
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "codIdioma",
            "label": "Idioma",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "desIdioma",
            "label": "Descrição",
            "type": "string",
            "required": true,
            "validate": "/api/trn/v1/idiomas/validateField"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "codIdiomPadr",
            "label": "Idioma Padrão",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "datUltAtualiz",
            "format": "dd/MM/yyyy",
            "label": "Última Atualização",
            "type": "date"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "hraUltAtualiz",
            "label": "Hora Última Atualização",
            "type": "string"
        },
        {
            "visible": false,
            "property": "id",
            "type": "number",
            "key": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "codUsuario",
            "label": "Usuário",
            "type": "string",
            "required": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "nomUsuario",
            "label": "Nome",
            "type": "string",
            "required": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "codDialet",
            "label": "Dialeto",
            "type": "string",
            "required": true
        }
    ]
}

Busca dos dados onde foram adicionados novos valores:

GET - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas

{
    "total": 3,
    "hasNext": false,
    "items": [
        {
            "codIdiomPadr": "99 Outros",
            "codDialet": "Pt",
            "codIdioma": "ale",
            "codUsuario": "super",
            "desIdioma": "Alemão",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Super",
            "id": 4580144
        },
        {
            "codIdiomPadr": "99 Outros",
            "codDialet": "PT",
            "codIdioma": "EN",
            "codUsuario": "joao",
            "desIdioma": "Ingles",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Joao da Silva",
            "id": 194736
        },
        {
            "codIdiomPadr": "03 Espanhol",
            "codDialet": "PT",
            "codIdioma": "ES",
            "codUsuario": "Manoel",
            "desIdioma": "Espanhol",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Manoel da Silva",
            "id": 2968898
        }
    ]
}

Front-End PO-UI

Introdução:

Para este exemplo vamos criar um CRUD com template dinâmico, onde serão mostrados os dados de acordo com o que o back-end retornar.

O desenvolvimento do frontend utilizando este campo componente se divide basicamente em três partes:

  • Routes:
    • Na definição da rota é onde vamos definir todos os caminhos dos componentes;
  • HTML
    • No HTML basta colocarmos os componentes, pois o metadados irá retornar o que precisamos para renderizar o componente;
  • TypeScript
    • No Typescript do componente vamos realizar uma pequena lógica para o tratamento dos dados de acordo com metadado;


Abaixo vamos mostrar como ficaram a parte de Listagem, Edição e Detalhe do nosso CRUD dinâmico.

Routes:

Abaixo segue exemplo de como ficará o arquivo de rotas de nossa aplicação CRUD.

app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { IdiomaDetailComponent } from './idioma/detail/idioma-detail.component';
import { IdiomaEditComponent } from './idioma/edit/idioma-edit.component';
import { IdiomaListComponent } from './idioma/list/idioma-list.component';

const routes: Routes = [
  { path: 'idiomas/create', component: IdiomaEditComponent },
  { path: 'idiomas/edit/:id', component: IdiomaEditComponent },
  { path: 'idiomas/detail/:id', component: IdiomaDetailComponent },
  { path: 'idiomas', component: IdiomaListComponent },
  { path: '', redirectTo: '/idiomas', pathMatch: 'full' },
  { path: '**', component: IdiomaListComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Listagem:

É a tela inicial da nossa aplicação e mostra a lista de dados da tabela Idioma, onde foram adicionados através de customização três campos da tabela usuar_mestre. Esta tela dará acesso às outras funcionalidades como edição e detalhamento.

Listagem - idioma-list.componente.html
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-dynamic-table
  p-auto-router
  [p-title]="cTitle"
  [p-actions]="actions"
  [p-breadcrumb]="breadcrumb"
  [p-fields]="fields"
  [p-service-api]="serviceApi">
</po-page-dynamic-table>
Listagem - idioma-lista.component.ts
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { PoBreadcrumb } from '@po-ui/ng-components';
import { PoPageDynamicTableActions } from '@po-ui/ng-templates';

import { IdiomaService } from './../resources/idioma.service';

@Component({
  selector: 'app-idioma-list',
  templateUrl: './idioma-list.component.html',
  styleUrls: ['./idioma-list.component.css']
})

export class IdiomaListComponent implements OnInit {
  // Definicao das variaveis utilizadas
  public cTitle = 'Manutenção de Idiomas';
  public serviceApi: string;
  public fields: Array<any> = [];
  public showLoading = false;

  public readonly actions: PoPageDynamicTableActions = {
    new: '/idiomas/create',
    detail: '/idiomas/detail/:id',
    edit: '/idiomas/edit/:id',
    remove: true,
    removeAll: true
  };

  public readonly breadcrumb: PoBreadcrumb = {
    items: [
      { label: 'Home', link: '/' },
      { label: 'Idiomas'}
    ]
  };

  // Construtor da classe
  constructor(
    private service: IdiomaService,
    private route: Router
  ) { }

  // Load do componente
  public ngOnInit(): void {
    this.fields = [];
    this.serviceApi = this.service.getUrl();
    this.showLoading = true;
    this.service.getMetadata().subscribe(resp => {
      this.fields = resp['items'];
      this.service.setFieldList(this.fields);
      this.showLoading = false;
    });
  }
}

Edição: 

Esta tela permite a inclusão de um novo registro na tabela Idioma e também a alteração de registros já existentes.

Edição: idioma-edit.component.html
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-edit
  [p-title]="cTitle"
  [p-breadcrumb]="breadcrumb"
  [p-disable-submit]="formEdit.form.invalid"
  (p-cancel)="cancelClick()"
  (p-save)="saveClick()">
  <po-dynamic-form
    #formEdit
    p-auto-focus="string"
    [p-fields]="fields"
    p-validate="/api/trn/v1/idiomas/validateForm"
    [p-value]="record">
  </po-dynamic-form>
</po-page-edit>
Edição - idioma-edit.component.ts
import { Component, OnInit, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb, PoDialogService, PoNotificationService } from '@po-ui/ng-components';

import { IdiomaService } from './../resources/idioma.service';

@Component({
  selector: 'app-idioma-edit',
  templateUrl: './idioma-edit.component.html',
  styleUrls: ['./idioma-edit.component.css']
})

export class IdiomaEditComponent implements OnInit {
  // Define as variaveis a serem utilizadas
  public cTitle: string;
  public currentId: string;
  public record = {};
  public fields: Array<any> = [];
  public isUpdate = false;
  public showLoading = false;

  public breadcrumb: PoBreadcrumb;

  // Obtem a referencia do componente HTML
  @ViewChild('formEdit', { static: true })
  formEdit: NgForm;

  // Construtor da classe com os servicos necessarios
  constructor(
    private service: IdiomaService,
    private activatedRoute: ActivatedRoute,
    private route: Router,
    private poDialog: PoDialogService,
    private poNotification: PoNotificationService
  ) { }

  // Load do componente
  public ngOnInit(): void {
    this.isUpdate = false;
    this.showLoading = true;

    // Carrega o registro pelo ID
    this.activatedRoute.params.subscribe(pars => {
      this.currentId = pars['id'];

      // Se nao tiver o ID definido sera um CREATE
      if (this.currentId === undefined) {
        this.isUpdate = false;
        this.cTitle = 'Inclusão de Idioma';
      } else {
        this.isUpdate = true;
        this.cTitle = 'Alteração de Idioma';
      }

      // Atualiza o breadcrumb de acordo com o tipo de edicao
      this.breadcrumb = {
        items: [
          { label: 'Home', action: this.beforeRedirect.bind(this) },
          { label: 'Idiomas', action: this.beforeRedirect.bind(this) },
          { label: this.cTitle }
        ]
      };

      // Se for uma alteracao, busca o registro a ser alterado
      if (this.isUpdate) {
        this.service.getById(this.currentId).subscribe(resp => {
          Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

          // Em alteracao temos que receber o registro para depois buscar a lista de campos
          this.getMetadata();
        });
      } else {
        // Se for create, pega a lista de campos
        this.getMetadata();
      }
    });
  }

  // Retorna a lista de campos
  private getMetadata() {
    let fieldList: Array<any> = [];

    // Carrega a lista de campos, trabalhando com um cache da lista de campos
    fieldList = this.service.getFieldList(this.isUpdate);
    if (fieldList === null || fieldList.length === 0) {
      this.service.getMetadata().subscribe(resp => {
        this.service.setFieldList(resp['items']);
        this.fields = this.service.getFieldList(this.isUpdate);
        this.showLoading = false;
      });
    } else {
      this.fields = fieldList;
      this.showLoading = false;
    }
  }

  // Redireciona via breadcrumb
  private beforeRedirect(itemBreadcrumbLabel) {
    if (this.formEdit.valid) {
      this.route.navigate(['/']);
    } else {
      this.poDialog.confirm({
        title: `Confirma o redirecionamento para ${itemBreadcrumbLabel}`,
        message: `Existem dados que não foram salvos ainda. Você tem certeza que quer sair ?`,
        confirm: () => this.route.navigate(['/'])
      });
    }
  }

  // Grava o registro quando clicado no botao Salvar
  public saveClick(): void {
    this.showLoading = true;
    if (this.isUpdate) {
      // Altera um registro ja existente
      this.service.update(this.currentId, this.record).subscribe(resp => {
        this.poNotification.success('Idioma alterado com sucesso');
        this.showLoading = false;
        this.route.navigate(['/idiomas']);
      });
    } else {
      // Cria um registro novo
      this.service.create(this.record).subscribe(resp => {
        this.poNotification.success('Idioma criado com sucesso');
        this.showLoading = false;
        this.route.navigate(['/idiomas']);
      });
    }
  }

  // Cancela a edicao e redireciona ao clicar no botao Cancelar
  public cancelClick(): void {
    this.poDialog.confirm({
      title: 'Confirma cancelamento',
      message: 'Existem dados que não foram salvos ainda. Você tem certeza que quer cancelar ?',
      confirm: () => this.route.navigate(['/'])
    });
  }
}

Detalhe:

Esta tela apresenta os detalhes de um registro de Idioma, com suas customizações.

Detalhe: idioma-detail.component.html
<po-loading-overlay
  [hidden]="!showLoading">
</po-loading-overlay>

<po-page-detail
  [p-title]="cTitle"
  [p-breadcrumb]="breadcrumb"
  (p-edit)="editClick()"
  (p-back)="goBackClick()">
  <po-dynamic-view 
    [p-fields]="fields"
    [p-value]="record">
  </po-dynamic-view>
</po-page-detail>
Detalhe: idioma-detail.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PoBreadcrumb } from '@po-ui/ng-components';

import { IdiomaService } from './../resources/idioma.service';

@Component({
  selector: 'app-idioma-detail',
  templateUrl: './idioma-detail.component.html',
  styleUrls: ['./idioma-detail.component.css']
})

export class IdiomaDetailComponent implements OnInit {
  // definicao das variaveis utilizadas
  public cTitle = 'Detalhe do Idioma';
  public currentId: string;
  public fields: Array<any> = [];
  public record = {};
  public showLoading = false;

  public readonly breadcrumb: PoBreadcrumb = { items: [
      { label: 'Home', link: '/' },
      { label: 'Idiomas', link: '/idiomas' },
      { label: 'Detail' } ]
  };

  // construtor com os servicos necessarios
  constructor(
    private service: IdiomaService,
    private activatedRoute: ActivatedRoute,
    private route: Router
  ) { }

  // load do componente
  public ngOnInit(): void {
    this.activatedRoute.params.subscribe(pars => {
      this.showLoading = true;

      // carrega o registro pelo ID
      this.currentId = pars['id'];
      this.service.getById(this.currentId).subscribe(resp => {
        Object.keys(resp).forEach((key) => this.record[key] = resp[key]);

        // carrega a lista de campos somente apos receber o registro a ser apresentado
        this.fields = this.service.getFieldList(false);
        if (this.fields === null || this.fields.length === 0) {
          this.service.getMetadata().subscribe(data => {
            this.fields = data['items'];
            this.service.setFieldList(this.fields);
            this.showLoading = false;
          });
        }
        this.showLoading = false;
      });
    });
  }

  // Redireciona quando clicar no botao Edit
  public editClick(): void {
    this.route.navigate(['/idiomas', 'edit', this.currentId]);
  }

  // Redireciona quando clicar no botao Voltar
  public goBackClick(): void {
    this.route.navigate(['/idiomas']);
  }
}

06. VALIDAÇÃO DE COMPONENTES

Para os itens a seguir, são apresentados algumas formas de interação com os componentes presentes na interface, bem como possíveis validações sobre os mesmos. 

Esconder ou visualizar os campos

Como a geração da tela dinâmica é automática, para esconder determinados campos basta setar o atributo visible para FALSE na montagem do JsonObject de retorno do metadados. 

Esconder campos na interface
... 
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'codDialet').
    jObj:add('label', 'Dialeto').
    jObj:add('visible', FALSE). // <- Remove o item da tela de todos seus correspondentes (Form, View, Table)
    jObj:add('required', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    jObj:add('validate', '/api/trn/v1/idiomas/validateField').
    jAList:add(jObj).
...

Validação de componentes na interface

Uma boa prática em desenvolvimento de telas é a validação de alguns campos na própria interface, cujo intuito é reduzir requisições desnecessárias ao 'Back-End'. As funcionalidades apresentadas a seguir podem ser utilizadas conjunto com a validação do próprio Form ([p-disable-submit]="formEdit.form.invalid"), no qual pode desabilitar o botão de confirmação enquanto houver campos inválidos.

Utilização do pattern (RegEx)

Para a validação de campos textos, pode ser utilizado o atributo pattern qualquer expressão regular, caso não atenda ao RegEx, uma mensagem de erro definida em errorMessage é apresentada em tela. 

Utilização de pattern
... 
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'testeValidacaoRegEx').
    jObj:add('label', 'Teste Validação RegEx').
    jObj:add('gridColumns', 6).
    jObj:add('pattern', "[0-9]~{2~}"). // <- Validacao RegEx
    jObj:add('errorMessage', 'Obrigatório mínimo 2 números consecutivos.').
    jAList:add(jObj).
...

Utilização de limites em numeração

Com a utilização dos atributos minValue e maxValue, é possível efetuar a restrição de períodos da numeração que pode ser utilizado em conjunto com o type para restringir a digitação em somente números. Caso houver números inválidos, a mensagem definida em errorMessage é apresentada na tela.

Utilização intervalo de valores
... 
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'numberValidate').
    jObj:add('label', 'Somente números').
    jObj:add('visible', TRUE).
    jObj:add('required', FALSE).
    jObj:add('minValue', 1).
    jObj:add('maxValue', 9).
    jObj:add('errorMessage', 'Somente números de 1 a 9'). // <- Mensagem de erro 1-9
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('integer')). // <- Restringe a digitacao somente numeros
    jObj:add('gridColumns', 6).
    jAList:add(jObj).
...

Utilização de máscaras para os campos

Quando é definida uma máscara mask, ocorre a restrição de digitação no próprio campo. Para o exemplo abaixo, é permitido digitar somente números e ao efetuar a digitação, a máscara será aplicada automaticamente.

Utilização de máscaras
...   
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'numberRangeValidate').
    jObj:add('label', 'Aplicação de máscara CPF').
    jObj:add('mask', '999.999.999-99').  // <-- Mascara CPF
    jObj:add('visible', TRUE).
    jObj:add('required', FALSE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    jAList:add(jObj).
...

Validações no back-end

O po-dynamic-form permite dois tipos de validações, a validação do formulário completo ou por campo.

A validação do formulário completo vamos detalhar mais a frente.

A validação por campo é feito através da validação campo-a-campo, onde você conseguirá alterar algumas características do campo que esta sendo validado.

Nas validações de campos é possível somente alterar as características do próprio campo validado, onde não é permitido alterar nas características de outros campos.

Utilização de validação de campo
...   
    ASSIGN jObj = NEW JsonObject().
    jObj:add('property', 'nomCampo').
    jObj:add('label', 'Label do campo').
    jObj:add('visible', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('validate', '/api/trn/v1/idiomas/validateField'
    jObj:add('gridColumns', 6).
    jAList:add(jObj).
...


A adição da tag validate na definição do campo, onde deverá ser especificado a URL de validação, fará com que sempre que o campo for alterado pelo usuário, será enviado uma requisição para o back-end para realizar a validação desse campo.

JSon que recebemos da tela HTML

O componente PO-DYNAMIC-FORM, quando ocorre alguma alteração em seus campos, no LEAVE do campo, ele enviará um JSon com o seguinte formato para o back-end:

TagTipoDescrição
propertyCharacterContêm o nome do campo que houve a alteração para ser validado.
valueCharacterContêm o valor atual do campo para ser validado.


Onde o back-end receberá o seguinte JSon:

JSON enviado para o back-end de validação do campo
{
    "property": "campoValidado",
    "value": "valorAtualDoCampoValidado"
}


JSon que retornamos para a tela HTML

A validação do campo, aguarda o seguinte formato de JSon:

TagTipoDescrição
valueCharacterContêm o novo valor para o campo.
fieldJSonObjectContêm uma lista de propriedades que serão alteradas. OBS: Estas propriedades tem efeito somente sobre o campo que está sendo validado.
focusLogicalInforma se o campo validado deverá ou não receber o focus.
_messagesJSonArrayContêm uma lista de mensagens "de erro" que podem ser apresentadas ao voltar para o HTML.


O back-end, após processar e realizar a validação necessária, retornará para o front-end o seguinte JSon:

JSON de retorno do back-end da validação de campo
{
    value: 'novoValorDoCampoValidado',
    field: {
        mask: '99.999.999/9999-99',
        required: true 
    },
    focus: true,
    _messages: [
        {
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        }
    ]
}


Para utilizarmos a UPC de customização, tivemos que encapsular o JSon recebido no back-end, dentro da procedure pValidateField, e tambem o JSon a ser enviado para a tela HTML, conforme o exemplo abaixo:

JSon encapsulado para UPC de Validação de Campos
// encapsulamos o retorno para enviar para a UPC
oObj = NEW JsonObject().
oObj:add("property", cProp).
oObj:add("root", oRet).

// Realiza a chamada da UPC Progress
{include/i-epcrest.i &endpoint=validateField &event=validateField &jsonVar=oObj}    

// obtem o retorno customizado, onde o mesmo foi alterado e retornado na tag root 
oRet = oObj:getJsonObject("root").


Neste nosso exemplo, nós dividimos o JSon a ser enviado para UPC em três partes, que são:

TagTipoDescrição
propertyCharacterComtêm o nome do campo que esta sendo validado.
rootJSonObjectContêm um JSonObject com que será retornado para o HTML e que poderá ser customizado na UPC. Tudo que for customizado deverá estar dentro desta tag.


Exemplo de JSon recebido pela UPC:

JSon recebido na UPC para Validação do Campo
{ 
    property: 'nomeDoCampoValidado',
    root: {
        value: '',
        field: {
            mask: '99.999.999/9999-99' 
        },
        focus: false,
        _messages: [ 
            { 
                code: '01', 
                message: 'Mensagem do erro que aconteceu', 
                detailedMessage: 'detalhes do erro acontecido' 
            } 
       ]
    }
}


Após a customização pela UPC, sera devolvido para a API Rest apenas a tag root, com as customizações necessárias, conforme o exemplo abaixo onde temos o resultado de uma customização:

JSon retornado pela UPC para Validações
{
    value: '',
    field: {
        mask: '99.999.999/9999-99',
       label: 'Novo Label' 
    },
    focus: false,
    _messages: [ 
        { 
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        } 
   ]
}

07. VALIDAÇÃO DE FORMULÁRIOS

O quê deve ser alterado no componente PO-DYNAMIC-FORM

Para validarmos um formulário, temos que configurar primeiro o nosso componente po-dynamic-form para ficar apto a enviar as ocorrências de validações. Para isso temos que especificar no componente po-dynamic-form a tag p-validate, onde informamos a URL que fará a validação do form, neste exemplo será "/api/trn/v1/idiomas/validateForm".

Tag p-validate no po-dynamic-form
  <po-dynamic-form
    #formEdit
    p-auto-focus="string"
    [p-fields]="fields"
    p-validate="/api/trn/v1/idiomas/validateForm"
    [p-value]="record">
  </po-dynamic-form>

As validações de formulário validam somente os campos já existentes, onde não é permitido adicionar novos campos. Para adicionar novos campos deve-se utilizar a tag p-load, que é executada logo após a inicialização do componente.


JSon que recebemos da tela HTML

O componente PO-DYNAMIC-FORM, quando ocorre alguma alteração em seus campos, no LEAVE do campo, ele enviará um JSon com o seguinte formato para o back-end:

TagTipoDescrição
propertyCharacterContêm o nome do campo que houve a alteração para ser validado.
valueJSonObjectContêm uma tag para cada campo da tela com os seus respectivos valores atuais.


A cada campo que for alterado e ocorrer um LEAVE, será enviado pelo PO-UI um Json que contém o campo que teve o seu valor alterado junto com todos os valores dos demais campos da tela HTML, conforme o exemplo abaixo:

JSon enviado pelo HTML para validação dos campos
{
    "property": "codAcoes",
    "value": {
        "codIdiomPadr": "01 Português",
        "codIdioma": "12345678",
        "desIdioma": "12345678901234567890",
        "hraUltAtualiz": "",
        "datUltAtualiz": null,
        "id": 6,
        "codAcoes": "FocoDesIdioma"
    }
}


JSon que teremos que retornar para a tela HTML

As validações do formulário, aguardam o seguinte formato de JSon:

TagTipoDescrição
valueJSonObjectContêm uma tag que representa o nome do campo em tela e o seu novo valor. Se não for especificado um novo valor, não é necessario adicionar a tag referente ao campo
fieldsJSonArrayContêm uma lista de campos com as suas propriedades que serão alteradas. Se não for alterada nenhuma propriedade de nenhum campo, não é necessário informar essa tag.
focusCharacterContêm o campo que receberá o foco ao voltar para a tela HTML.
_messagesJSonArrayContêm uma lista de mensagens "de erro" que deverão ser apresentadas ao voltar para o HTML.


Para retornar as informações para o PO-UI, temos que devolver o seguinte JSon:

JSon de retorno do backend para o HTML
{    
    value: {
      desIdioma: 'teste de escrita',
      hraUltAtualiz: '17:18:19'
    },
    fields: [
      {
        property: 'codCpfCnpj', 
        mask: '99.999.999/9999-99' 
      }
    ],
    focus: 'hraUltAtualiz',
    _messages: [ 
        { 
            code: '01', 
            message: 'Mensagem do erro que aconteceu', 
            detailedMessage: 'detalhes do erro acontecido' 
        } 
    ]
 }


Para utilizarmos a UPC de customização, tivemos que encapsular o JSon recebido no back-end, dentro da procedure pValidateForm, e tambem o JSon a ser enviado para a tela HTML, conforme o exemplo abaixo:

JSon empacsulado para UPC de Validação de Formulários
// encapsulamos o retorno para enviar para a UPC
oObj = NEW JsonObject().
oObj:add("property", cProp).
oObj:add("originalValues", oValue).
oObj:add("root", oRet).

// Realiza a chamada da UPC Progress
{include/i-epcrest.i &endpoint=validateForm &event=validateForm &jsonVar=oObj}    

// obtem o retorno customizado, onde o mesmo foi alterado e retornado na tag root 
oRet = oObj:getJsonObject("root").


Neste nosso exemplo, nós dividimos o JSon a ser enviado para UPC em três partes, que são:

TagTipoDescrição
propertyCharacterComtêm o nome do campo que esta sendo validado.
originalValuesJsonObjectContêm um JSonObject com propriedades contendo os nomes dos campos e os seus respectivos valores.
rootJSonObjectContêm um JSonObject com que será retornado para o HTML e que poderá ser customizado na UPC. Tudo que for customizado deverá estar dentro desta tag.


Exemplo de JSon recebido pela UPC:

JSon recebido na UPC para Validação do Formulário
{ 
    property: 'codAcao',
    originalValues: {
        "codIdiomPadr": "01 Português",
        "codIdioma": "12345678",
        "desIdioma": "12345678901234567890",
        "hraUltAtualiz": "",
        "datUltAtualiz": null,
        "id": 6,
        "codAcoes": "FocoDesIdioma"
    },
    root: {
        value: {
            desIdioma: 'teste de escrita',
            hraUltAtualiz: '17:18:19'
        },
        fields: [
            {
                property: 'codCpfCnpj', 
                mask: '99.999.999/9999-99' 
            }
        ],
        focus: 'hraUltAtualiz',
        _messages: [ 
            { 
                code: '01', 
                message: 'Mensagem do erro que aconteceu', 
                detailedMessage: 'detalhes do erro acontecido' 
            } 
        ]
    }
}


Após a customização pela UPC, sera devolvido para a API Rest apenas a tag root, com as customizações necessárias, conforme o exemplo abaixo onde temos o resultado de uma customização:

JSon retornado pela UPC para Validações
{
    "_messages": [
        {
            "detailedMessage": "Na execução da UPC, houveram alterações nos campos de tela.",
            "code": "44",
            "message": "A UPC alterou algumas caracteristica da tela."
        }
    ],
    "focus": "desIdioma",
    "fields": [
        {
            "property": "codIdiomPadr",
            "disabled": true
        }
    ],
    "value": {
        "desIdioma": "Valor customizado na UPC"
    }
}

08. FACILITADORES PROGRESS

Criamos facilitadores para auxiliar no desenvolvimento das API's, ficam localizados na classe Progress "com.totvs.framework.api.JsonAPIUtils":

MétodoDescriçãoAssinatura/Exemplo
convertAblTypeToHtmlTypeConverte os tipos nativos do Progress para os tipos esperados pelo PO-UI

Assinatura:

convertAblTypeToHtmlType (INPUT cType AS CHARACTER)

Exemplo:

ASSIGN cType = JsonAPIUtils:convertAblTypeToHtmlType ("integer").

O retorno no cType será "number", que é um formato reconhecido pelo PO-UI.

convertToCamelCaseConverter os nomes dos campos lidos da tabela, normalmente com "_", para "camel case", que é o mais comum utilizado em Json's.   

Assinatura:

convertToCamelCase (INPUT cKey AS CHARACTER)

Exemplo:

ASSIGN cField= JsonAPIUtils:convertToCamelCase ("cod_e_mail_usuar").

O retorno no cField será "codEMailUsuar", que é o campo em Camel Case.

getIdFieldRetorna um campo do tipo ID para ser adicionado na lista de campos do Metadata. Este campo serve como chave do registro nos tratamentos de CRUD na parte HTML.

Assinatura:

getIdField()

Exemplo:

oIdiomas:add( JsonAPIUtils:getIdField() ).

09. TÉCNICA PARA TRADUÇÃO

A técnica para tradução de label, possui como base as recomendações de i18n do PO UI (https://po-ui.io/documentation/po-i18n) com algumas características adicionais. A seguir serão apresentadas alguns trechos de código que representam a utilização desta técnica de tradução em conjunto com formulários dinâmicos.

Para diferenciar a label que devem ser traduzida, deve-se inserir a key de tradução entre os caracteres chaves. Exemplo: { key }

Trecho código - idiomas_upc.p
...

    ASSIGN jObj = NEW JsonObject().
    jObj:add('divider', "Itens da UPC").
    jObj:add('property', 'codUsuario').
    jObj:add('label', '~{~{user~}~}').
    jObj:add('visible', TRUE).
    jObj:add('required', TRUE).
    jObj:add('type', JsonAPIUtils:convertAblTypeToHtmlType('character')).
    jObj:add('gridColumns', 6).
    oFields:add(jObj).

...


Conforme a arquitetura de customização apresentada anteriormente, deve-se aguardar o resultado da requisição ao serviço metadata e posteriormente, efetuar os seus devidos tratamentos de tradução. 

Trecho código - idioma-list.components.ts
...

    this.service.getMetadata().subscribe(resp => {
      this.service.setFieldList(resp['items']);
      this.fields = this.service.getFieldList(false, this.literals);
      this.showLoading = false;
    });

...


Para que o ponto de tradução seja único e compatível com as técnicas recomendadas pelo PO UI, deve-se efetuar o tratamento no Front-end. Recomenda-se que seja realizado na função que retorna a lista de fields pois neste momento, são carregados todos os campos correspondentes à tela. 

Trecho código - idioma.service.ts
...

public getFieldList(update, literals) {
    // ajusta alista de campos para habilitar ou nao a chave primaria se for CREATE
    let fields: Array<any> = [];
    if (this.fieldList.length > 0) {
      this.fieldList.forEach((data) => {
        if (data['label'] !== undefined) {
          const key = data['label'].replace('{{', '').replace('}}', '');
          if (literals[key] !== undefined) {
            data['label'] = literals[key];
          }
        }
        if (data['options'] !== undefined) {
          let options = data['options'];
          options.forEach((option) => {
            const key = option['label'].replace('{{', '').replace('}}', '');
            if (literals[key] !== undefined) {
              option['label'] = literals[key];
            }
          });
        }
        fields.push(data);
      });
    }
    return fields;
}

...


A arquitetura de tradução do PO UI cita: "... Existe também a possibilidade de utilizar ambos, onde será feito a busca das literais nas constantes e depois efetua a busca no serviço ...". Portanto pode-se configurar os arquivos de tradução com um serviço (URL) que retorna as literais adicionais desenvolvidas no "Back-end", sendo assim, um complemento ao arquivo.

Observação: O exemplo da URL abaixo não segue as recomendações do PO UI (api/translations/idiomas). Foi desenvolvido em uma estrutura diferente para facilitar os códigos a conceito de POC.  

Trecho código - app.module.ts
...

const i18nConfig: PoI18nConfig = {
  default: {
    language: 'pt-BR',
    context: 'general',
    cache: false
  },
  contexts: {
    general: {
      'pt-BR': generalPt,
      'en-US': generalEn,
      url: 'api/trn/v1/idiomas/translations'
    }
  }
};

...


Para o endpoint de tradução, pode ser utilizado uma chamada UPC conforme trecho de código a seguir:

Trecho código - idiomas.p
...

{utp/ut-api-action.i pIdiomas GET /translations/~* }

... 

/**
    Recupera as literais
*/
PROCEDURE pIdiomas:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject    NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject    NO-UNDO.
    
    // Realiza a chamada da UPC Progress
    {include/i-epcrest.i &endpoint=i18n &event=i18n &jsonVar=oJsonInput}
    
    ASSIGN oJsonOutput = oJsonInput.
END PROCEDURE.


A Procedure correspondente ao serviço (URL) citado anteriormente, deve retornar um objeto no formato JSON de acordo com o idioma enviado por parâmetro.

Nota

O exemplo a seguir efetua o tratamento somente do parâmetro language. Segundo a documentação do PO UI, outros parâmetros (context, literals) podem ser enviados, sendo necessária uma futura implementação de seu recebimento no Back-End.

Trecho código - idiomas_upc.p
...

IF  pEndPoint = "i18n"
AND pEvent    = "i18n" THEN DO ON STOP UNDO, LEAVE:
    RUN piI18N.
END.

...

PROCEDURE piI18N:
    DEFINE VARIABLE oParser      AS JsonAPIRequestParser NO-UNDO.
    DEFINE VARIABLE oQueryParams AS JsonObject           NO-UNDO.
    DEFINE VARIABLE pIdioma      AS CHARACTER            NO-UNDO.
    
    ASSIGN 
        oParser      = NEW JsonAPIRequestParser(jsonIO) 
        oQueryParams = oParser:GetQueryParams()
        pIdioma      = oQueryParams:GetJsonArray("language"):GetCharacter(1).
    
    IF (pIdioma = "pt-BR") THEN DO:
        jsonIO = NEW JsonObject().
        jsonIO:Add("user", "Usuário").
        jsonIO:Add("name", "Nome").
        jsonIO:Add("regexTestValidation", "Teste Validação REGEX").
        jsonIO:Add("cpfMaskApply", "Aplicação Máscara CPF").
        jsonIO:Add("onlyNumbers", "Somente Números").
    END.
    ELSE IF (pIdioma = "en-US") THEN DO:
        jsonIO = NEW JsonObject().
        jsonIO:Add("user", "User").
        jsonIO:Add("name", "Name").
        jsonIO:Add("regexTestValidation", "REGEX Test Validation").
        jsonIO:Add("cpfMaskApply", "CPF Apply Mask").
        jsonIO:Add("onlyNumbers", "Only Numbers").
    END.
END PROCEDURE.

10. COMO DOCUMENTAR A TELA QUE PERMITE CUSTOMIZAÇÃO

Conteúdo interno!

Abaixo segue o link da documentação que ira auxiliar a documentar a tela que permite a customização e assim manter o padrão de desenvolvimento tanto na parte técnica, quanto na documentação das telas:

Documentação Customização HTML PO-UI  

e a pagina com exemplo de como deve ficar a documentação:

https://tdn.totvs.com/display/public/EN/Exemplo 


11. LINKS ÚTEIS


Conteúdo interno!

Link da documentação: desenvolvimento HTML Estatico x Dinamico

https://tdninterno.totvs.com/pages/releaseview.action?pageId=834824110


Documentação API Datasul:

Desenvolvimento de APIs para o produto Datasul

PO-UI:

12. CONCLUSÃO

A ideia era apresentar uma técnica para que possibilita-se as áreas de negócio, de forma segura e simples, disponibilizarem pontos de customização em suas API’s REST.

Acreditamos que a técnica apresentada permite que o back-end Progress acompanhe a evolução dos componentes PO-UI.

Com a parte de validação do formulário é possível tratar e validar os campos de acordo cum a lógica de negócio que ocorre no back-end, onde sempre será recebido na API REST o campo que está sendo alterado e todos os valores dos demais campos da tela.

Na parte de validação por campo, é recebido na API REST somente o nome do campo e o seu valor atual. Acho importante informar que não é enviado o ID do registro, onde teremos que tomar cuidado para não perder o contexto do registro que estamos validando.

Contamos com seu feedback e sugestões para manter a melhoria continua nas documentações.