Árvore de páginas



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.O código de API abaixo pode possuir diferenças de estrutura comparado com a forma utilizada pelas equipes de negócio. Algumas equipes, por exemplo

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.

Esta documentação pode ser usada tanto pelos desenvolvedores internos a fim de disponibilizar a customização no produto padrão, quanto pelos desenvolvedores externos que necessitam criar customizações nessas telas HTML. Porém, em algumas seções da documentação podem conter links disponíveis apenas para desenvolvedores internos. 

02. VISÃO GERAL

No PO-UI, uma tela dinâmica trabalha recebendo uma lista de informações que serão utilizadas para apresentar os componentes da tela, e uma outra lista contendo os dados que serão apresentados nestes componentes.

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 passo, que é a preparação do endpoint da API para a customização.

Esta API deverá ser inserida 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
pEndPointINPUTCHARACTERContém o nome do endpoint que está sendo executado.
pEventINPUTCHARACTERContém o nome do evento que está sendo executado.
pAPIINPUTCHARACTERContém o nome da API que está sendo executada.
jsonIOINPUT-OUTPUTJsonObjectContém 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.
  • Caso o PASOE esteja em um servidor Linux, no campo "Programa UPC" deve-se utilizar "/"(barra) entre os diretórios no lugar de "\"(contrabarra), ou o programa não será encontrado causando erro na execução.
  • Caso seja utilizado um caminho relativo, exemplo "upc/nome_upc.p", deve ser ajustado o propath do PASOE para conter o diretório raiz, pois o programa upc será executado no PASOE.


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


Acessar a opção "Grupo de segurança" e configurar os grupos de segurança que terão acesso à API que está sendo cadastrada.

API Rest com chamada de UPC:

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

  • pGetMetaData que retorna os metadados das telas;
  • 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:

Atenção

O código de API apresentado a seguir pode possuir diferenças de estrutura comparado com a forma utilizada pelas equipes de negócio. Algumas equipes, por exemplo, criam um programa para as regras de negócio e fazem chamadas a esse programa nos métodos das APIs.
O importante aqui é atentar-se às técnicas utilizadas para disponibilizar os pontos de acesso à UPC, para que seja possível customizar a tela. 

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

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

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

{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".

/** Procedure que retorna o metadata **/
PROCEDURE pGetMetadata:
    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 c-evento   AS CHAR init "list" NO-UNDO.
    DEFINE VARIABLE c-versao   AS CHAR NO-UNDO.
    
    /*Realiza a busca dos parametro enviados na URL da requisicao: 
        - type: Tipo do metadado que deve ser retornado (list,new,copy,edit,detail).
        - version: Versao do metadado (se existir)*/
    ASSIGN oRequest = NEW JsonAPIRequestParser(oJsonInput).
    IF oRequest:getQueryParams():has("type") AND oRequest:getQueryParams():getJsonArray("type") NE ? THEN
        ASSIGN c-evento = oRequest:getQueryParams():getJsonArray("type"):getCharacter(1).
    IF oRequest:getQueryParams():has("version") AND oRequest:getQueryParams():getJsonArray("version") NE ? THEN
        ASSIGN c-versao = oRequest:getQueryParams():getJsonArray("version"):getCharacter(1).
        
    /* Busca o metadata e executa o ponto de UPC conforme o queryParam "type" */
    CASE c-evento:
        WHEN "list":U THEN DO:
            //Chamar o metodo que retorna o metadado de listagem
            RUN pGetMetadataList(INPUT oJsonInput,
                                 OUTPUT oJsonOutput).
                                 
            // Realiza a chamada da UPC Progress
            {include/i-epcrest.i &endpoint=getMetaData &event=list &jsonVar=oJsonOutput}    
 
        END.
        WHEN "new":U OR WHEN "copy":U THEN DO:
            //Chamar o metodo que retorna o metadado de criacao
            /*RUN pGetMetadataNew(INPUT oJsonInput,
                                  OUTPUT oJsonOutput).*/
                                  
            IF c-evento = "copy" THEN DO:
                //Seta a chave do registro no objeto que vai para a UPC                         
                RUN setMetadataEntityKey(INPUT oRequest,
                                         INPUT-OUTPUT oJsonOutput).  
            END.
            {include/i-epcrest.i &endpoint=getMetaData &event=new &jsonVar=oJsonOutput}
        END.
        WHEN "edit":U THEN DO:
            
            //Chamar o metodo que retorna o metadado de edicao
            /*RUN pGetMetadataEdit(INPUT oJsonInput,
                                   OUTPUT oJsonOutput).*/
                                   
            //Seta a chave do registro no objeto que vai para a UPC
            RUN setMetadataEntityKey(INPUT oRequest,
                                     INPUT-OUTPUT oJsonOutput).
            
            {include/i-epcrest.i &endpoint=getMetaData &event=edit &jsonVar=oJsonOutput}
        END.
        WHEN "detail":U THEN DO:
            //Chamar o metodo que retorna o metadado de detalhe
            /*RUN pGetMetadataDetail(INPUT oJsonInput,
                                     OUTPUT oJsonOutput).*/
            
            //Seta a chave do registro no objeto que vai para a UPC                         
            RUN setMetadataEntityKey(INPUT oRequest,
                                     INPUT-OUTPUT oJsonOutput).                         
                                     
            {include/i-epcrest.i &endpoint=getMetaData &event=detail &jsonVar=oJsonOutput}
        END.
    END CASE.
    
    // Retorna a colecao de campos customizados ou nao para a interface HTML
    oResponse   = NEW JsonAPIResponse(oJsonOutput).
    oJsonOutput = oResponse:createJsonResponse().
    
END PROCEDURE.

PROCEDURE setMetadataEntityKey:
/*No caso da edicao, copia e detalhe e preciso receber a chave do registro 
como pathParam no endPoint metadata e repassá-la para a UPC. Isso para caso 
o cliente queira alterar propriedades dos campos com base no registro, visto 
que o validateForm não é executado na abertura da tela. 
Adicionar a chave recebida no pathParams no atributo "entityKey" 
do objeto enviado para a upc */
    DEFINE INPUT PARAMETER oRequest AS JsonAPIRequestParser NO-UNDO.
    DEFINE INPUT-OUTPUT  PARAMETER oObj AS JsonObject NO-UNDO.
    
    DEFINE VARIABLE c-key AS CHAR NO-UNDO.
    
    ASSIGN c-key = oRequest:getPathParams():GetCharacter(2).
    
    IF oObj <> ? THEN
        oObj:ADD("entityKey",c-key).
    
END PROCEDURE.

PROCEDURE pGetMetadataList:
    DEFINE INPUT  PARAMETER oJsonInput  AS JsonObject NO-UNDO.
    DEFINE OUTPUT PARAMETER oJsonOutput AS JsonObject 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 de campos a propriedade "fields" de um JsonObject  
    //para enviar para a UPC
    oObj        = NEW JsonObject().
    oObj:add('fields', oIdiomas).
    
    oJsonOutput = oObj.
    
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.
    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(1).
    
    // Localiza o registro na tabela IDIOMA pela chave
    FIND FIRST idioma 
        WHERE idioma.cod_idioma = cId
        NO-LOCK NO-ERROR.
    IF AVAILABLE idioma THEN DO:
        BUFFER-COPY idioma TO ttIdiomas.
    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 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:
            
        //Antes de executar a regra de negocio chama o ponto UPC beforeCreate  
        {include/i-epcrest.i &endpoint=create &event=beforeCreate &jsonVar=oBody}
        
        /*Caso a UPC retorne NOK significa que a execucao de ser abortada 
        Nesse caso, o objeto retornado pela UPC deve constar uma mensagem de erro para ser retornado no 
        corpo da requisicao. O statusCode nesse caso sera 500. Deve ser realizado o 
        tratamento abaixo para interrompes o processamento.*/
        IF RETURN-VALUE = "NOK":U THEN DO:
            ASSIGN oJsonOutput = JsonApiResponseBuilder:ok(oBody, 500).
            RETURN.
        END.      
            
        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")
                   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('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 a chave do registro
    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 pela chave
    DO  TRANSACTION
        ON ERROR UNDO, LEAVE:
            
        //Antes de executar a regra de negocio chama o ponto UPC beforeUpdate  
        {include/i-epcrest.i &endpoint=update &event=beforeUpdate &jsonVar=oBody}

        /*Caso a UPC retorne NOK significa que a execucao de ser abortada 
        Nesse caso, o objeto retornado pela UPC deve constar uma mensagem de erro para ser retornado no 
        corpo da requisicao. O statusCode nesse caso sera 500. Deve ser realizado o 
        tratamento abaixo para interrompes o processamento.*/
        IF RETURN-VALUE = "NOK":U THEN DO:
            ASSIGN oJsonOutput = JsonApiResponseBuilder:ok(oBody, 500).
            RETURN.
        END.  
            
        FIND FIRST idioma 
             WHERE idioma.cod_idioma = 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 (INPUT cId,
                            OUTPUT oObj).
        IF RETURN-VALUE = "NOK":U THEN DO:
            ASSIGN oJsonOutput = JsonApiResponseBuilder:ok(oObj, 500).
            RETURN.
        END. 
        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 (INPUT cId,
                                OUTPUT oObj).
            IF RETURN-VALUE = "NOK":U THEN DO:
                ASSIGN oJsonOutput = JsonApiResponseBuilder:ok(oObj, 500).
                RETURN.
            END. 
        
            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 OUTPUT PARAMETER 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 pela chave
    DO  TRANSACTION
        ON ERROR UNDO, LEAVE:
        // Monta um objeto com a chave para enviar para UPC
        // poder eliminar o registro da tabela customizada
        oObj = NEW JsonObject().
        oObj:add('codIdioma', cId).
        //Antes de executar a regra de negocio chama o ponto UPC beforeDelete
        {include/i-epcrest.i &endpoint=delete &event=beforeDelete &jsonVar=oObj}
        
        /*Caso a UPC retorne NOK significa que a execucao de ser abortada 
        Nesse caso, o objeto retornado pela UPC deve constar uma mensagem de erro para ser retornado no 
        corpo da requisicao. O statusCode nesse caso sera 500. Deve ser realizado o 
        tratamento abaixo para interromper o processamento.*/
        IF RETURN-VALUE = "NOK":U THEN RETURN "NOK".         
            
        FIND FIRST idioma 
             WHERE idioma.cod_idioma = cId
            EXCLUSIVE-LOCK NO-ERROR.
        IF AVAILABLE idioma THEN DO:

            DELETE idioma.
           
            ASSIGN lDeleted = TRUE.
        END.
        
        // Realiza a chamada da UPC Progress para a eliminacao do 
        // registro customizado. 
        {include/i-epcrest.i &endpoint=delete &event=afterDelete &jsonVar=oObj}    
    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 Portugues",
            "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 back-end 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.
        
    /* Recebemos do HTML o JSON abaixo
    {
        "property": "codIdioma",
        "value": ""
    }
    */
    
    // oNewField guarda o objeto que sera alterado/modificado
    ASSIGN oNewField = NEW JsonObject()
           oRet = NEW JsonObject().
    
    // oMessages guarda as mensagens de retorno formato 
    // { code: '00', message: 'texto', detailedMessage: 'detalhes da mensagem' }
    ASSIGN oMessages = NEW JsonArray().

    IF  cProp = "codIdioma" THEN DO:
        ASSIGN cValue = oBody:getCharacter("value")
               lFocus = TRUE.
        IF cValue = '' THEN DO:
            oNewField:add('property', cProp).
            
            //alterado o campo para obrigatorio
            oNewField:add('required', true).
            //alterado o campo para apresentar a informacao que ìe obrigatorio
            oNewField:add('showRequired', true).
            

            ASSIGN oObj = NEW JsonObject().
            oObj:add('code', '99').
            oObj:add('message', 'O campo codigo do idioma ìe obrigatorio.'). 
            oObj:add('detailedMessage', 'Insira uma informacao valida para o campo codigo do idioma.').
            oMessages:add(oObj).

            // value -> valor que deve ser repassado para o campo
            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).
        END.
    END.
    
    // encapsulamos o retorno para enviar para a UPC
    oObj = CAST(oBody:clone(), jsonObject).
    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: '',
    field: {
        property: 'codIdioma'
        required: true,
        showRequired: true 
    },
    focus: true,
    _messages: [
        {
            code: '99', 
            message: 'O campo codigo do idioma e obrigatorio.', 
            detailedMessage: 'Insira uma informacao valida para o campo codigo do idioma.' 
        }
    ]
    */
    
    // 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 - upc-idioma.p
/**************************************************************************
** upc-idioma.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 aFields         AS JSonArray  NO-UNDO.
DEFINE VARIABLE aMessages       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    = "list" THEN DO ON STOP UNDO, LEAVE:
    RUN piGetMetaDataList.
END.

// Carrega as definicoes dos campos customizados da tela de inclusão
IF  pEndPoint = "getMetaData"
AND pEvent    = "new" THEN DO ON STOP UNDO, LEAVE:
    //RUN piGetMetaDataNew.
END.

// Carrega as definicoes dos campos customizados da tela de edição
IF  pEndPoint = "getMetaData"
AND pEvent    = "edit" THEN DO ON STOP UNDO, LEAVE:
    //RUN piGetMetaDataEdit.
END.

// Carrega as definicoes dos campos customizados da tela de detalhe
IF  pEndPoint = "getMetaData"
AND pEvent    = "detail" THEN DO ON STOP UNDO, LEAVE:
    //RUN piGetMetaDataDetail.
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    = "beforeCreate" THEN DO ON STOP UNDO, LEAVE:
    RUN piBeforeCreate.
END.

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

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

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

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

IF  pEndPoint = "delete"
AND pEvent    = "afterDelete" THEN DO ON STOP UNDO, LEAVE:
    RUN piAfterDelete.
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 piGetMetaDataList: 
    // Obtem a lista de campos e valores    
    ASSIGN aFields = jsonIO:getJsonArray('fields').

    // 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).
    aFields: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).
    aFields: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).
    aFields: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.').
    aFields: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).
    aFields: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).
    aFields:add(jObj).
    
    // Retorna a nova lista com os campos customizados
    jsonIO:Set("fields", aFields).
END PROCEDURE.

PROCEDURE piFindAll:
    // Obtem a lista de campos e valores    
    ASSIGN aFields = 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 = aFields:length.

    DO  ix = 1 TO iTot:
        ASSIGN jObj = aFields: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
        aFields: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", aFields).
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 piBeforeCreate:
    /*Permite realizar validações e bloquear a criação do registro.*/
    
    cCodUsuario = jsonIO:getCharacter("codUsuario") NO-ERROR.
    
    //Exemplo de validação
    IF NOT CAN-FIND(FIRST usuar_mestre
                    WHERE usuar_mestre.cod_usuario = cCodUsuario) THEN DO:
        ASSIGN jObj = NEW JsonObject()
               aMessages = NEW JsonArray().
        //Cria a mensagem de erro e acrescenta ao objeto retornado pela UPC
        jObj:add('code', '02').
        jObj:add('message', 'Não encontrado registro para o usuário informado.'). 
        jObj:add('detailedMessage', 'Não é possível criar o registro.').
        aMessages:add(jObj).
        
        jsonIO = NEW JsonObject().
        jsonIO:Add("_messages",jObj).
                      
        //Retorna NOK para que a API faça o tratamento de retornar o statusCode:500
        //juntamente com a mensagem no response da requisição.
        RETURN "NOK".
    END.
    RETURN "OK".
END PROCEDURE.
    
PROCEDURE piAfterCreate:
    // 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 piBeforeCreate:
    /*Permite realizar validações e bloquear a alteração do registro.*/
    
    cCodUsuario = jsonIO:getCharacter("codUsuario") NO-ERROR.
    
    //Exemplo de validação
    IF NOT CAN-FIND(FIRST usuar_mestre
                    WHERE usuar_mestre.cod_usuario = cCodUsuario) THEN DO:
        ASSIGN jObj = NEW JsonObject()
               aMessages = NEW JsonArray().
        //Cria a mensagem de erro e acrescenta ao objeto retornado pela UPC
        jObj:add('code', '02').
        jObj:add('message', 'Não encontrado registro para o usuário informado.'). 
        jObj:add('detailedMessage', 'Não é possível alterar o registro.').
        aMessages:add(jObj).
        
        jsonIO = NEW JsonObject().
        jsonIO:Add("_messages",jObj).
                      
        //Retorna NOK para que a API faça o tratamento de retornar o statusCode:500
        //juntamente com a mensagem no response da requisição.
        RETURN "NOK".
    END.
    RETURN "OK".
END PROCEDURE.

PROCEDURE piAfterUpdate:
    // 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 piBeforeCreate:
    /*Permite realizar validações e bloquear a eliminação do registro.*/
    
    cCodUsuario = jsonIO:getCharacter("codUsuario") NO-ERROR.
    
    //Exemplo de validação
    IF NOT CAN-FIND(FIRST usuar_mestre
                    WHERE usuar_mestre.cod_usuario = cCodUsuario) THEN DO:
        ASSIGN jObj = NEW JsonObject()
               aMessages = NEW JsonArray().
        //Cria a mensagem de erro e acrescenta ao objeto retornado pela UPC
        jObj:add('code', '02').
        jObj:add('message', 'Não encontrado registro para o usuário informado.'). 
        jObj:add('detailedMessage', 'Não é possível alterar o registro.').
        aMessages:add(jObj).
        
        jsonIO = NEW JsonObject().
        jsonIO:Add("_messages",jObj).
                      
        //Retorna NOK para que a API faça o tratamento de retornar o statusCode:500
        //juntamente com a mensagem no response da requisição.
        RETURN "NOK".
    END.
    RETURN "OK".
END PROCEDURE.


PROCEDURE piAfterDelete:
    // 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
    aFields = 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
    aMessages = 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).
            aFields: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.').
            aMessages: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", aFields). // 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", aMessages). // 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").  // obtem o retorno que sera enviado para a tela html
    oFieldObj = oReturn:getJsonObject("field"). // obtem as propriedades dos campos a serem alteradas
    lFocus = oReturn:getLogical("focus"). // obtem se o focus ficara sobre o mesmo campo ao retornar para a tela html
    aMessages = oReturn:getJsonArray("_messages"). // obtem as mensagens a serem retornados para a tela html
    
    /* Exemplo de JSON que veio para a UPC
    { 
        property: 'codIdioma',
        value: '',
        root: {
            value: '',
            field: {
                property: 'codIdioma',
                required: true,
                showRequired: true 
            },
            focus: true,
            _messages: [ 
                { 
                    code: '99', 
                    message: 'O campo código do idioma é obrigatório.', 
                    detailedMessage: 'Insira uma informação válida para o campo código do idioma.' 
                } 
            ]
        }
    }
    */

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

        cValue = oReturn:getCharacter("value"). // pega o novo valor do campo

        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.').
        IF aMessages = ? THEN
            aMessages = NEW JsonArray().
        aMessages:add(jObj).

        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", aMessages). // seta as mensagens a serem retornadas para a tela html
    END.
    
    /* Exemplo de JSON de retorno para o HTML      
    value: 'FocoDesIdioma',
    field: {
        required: true,
        showRequired: true,
        label: 'Novo Label',  
    },
    focus: true,
    _messages: [ 
        { 
            code: '99', 
            message: 'O campo código do idioma é obrigatório.', 
            detailedMessage: 'Insira uma informação válida para o campo código do idioma.' 
        },
        { 
            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().
    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 à 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 de listagem onde foram adicionados os novos campos codUsuario, nomUsuario e codDialet:

GET - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas/metadata?type=list  
{
    "fields": [
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "codIdioma",
            "label": "{{language}}",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "desIdioma",
            "label": "{{description}}",
            "type": "string",
            "required": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "codIdiomPadr",
            "label": "{{defaultLanguage}}",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "datUltAtualiz",
            "format": "dd/MM/yyyy",
            "label": "{{lastUpdate}}",
            "type": "date"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "disable": true,
            "property": "hraUltAtualiz",
            "label": "{{hourLastUpdate}}",
            "type": "string"
        },
        {
            "visible": true,
            "gridColumns": 12,
            "property": "codAcoes",
            "options": [
                {
                    "label": "{{languageDefaultFocus}}",
                    "value": "focoCodIdiomPadr"
                },
                {
                    "label": "{{descriptionFocus}}",
                    "value": "FocoDesIdioma"
                },
                {
                    "label": "{{languageDefaultDisable}}",
                    "value": "DesabilitaCodIdiomaPadrao"
                },
                {
                    "label": "{{languageDefaultEnable}}",
                    "value": "HabilitaCodIdiomaPadrao"
                },
                {
                    "label": "{{cpfMask}}",
                    "value": "MascaraCPF"
                },
                {
                    "label": "{{cnpjMask}}",
                    "value": "MascaraCNPJ"
                },
                {
                    "label": "{{changeValueLanguage}}",
                    "value": "TrocaValorDesIdioma"
                },
                {
                    "label": "{{languageHide}}",
                    "value": "EsconderDesIdioma"
                },
                {
                    "label": "{{languageShow}}",
                    "value": "AparecerDesIdioma"
                },
                {
                    "label": "{{errorMessageShow}}",
                    "value": "showErrorMessage"
                },
                {
                    "label": "{{languageLabelChange}}",
                    "value": "mudaLabelDesIdioma"
                }
            ],
            "label": "{{screenActions}}",
            "type": "string",
            "validate": "/api/trn/v1/idiomas/validateField"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "tipUsuario",
            "options": [
                {
                    "label": "{{PF}}",
                    "value": "f"
                },
                {
                    "label": "{{PJ}}",
                    "value": "j"
                }
            ],
            "label": "{{userType}}",
            "type": "string",
            "validate": "/api/trn/v1/idiomas/validateField"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "codCpfCnpj",
            "label": "{{documentOptions}}",
            "type": "string",
            "mask": "999.999.999-99"
        },
        {
            "visible": false,
            "property": "id",
            "type": "number",
            "key": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "divider": "Itens da UPC",
            "property": "codUsuario",
            "label": "{{user}}",
            "type": "string",
            "required": true
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "nomUsuario",
            "label": "{{name}}",
            "type": "string",
            "required": true
        },
        {
            "visible": false,
            "gridColumns": 6,
            "property": "codDialet",
            "label": "{{dialect}}",
            "type": "string",
            "required": true
        },
        {
            "gridColumns": 6,
            "property": "testeValidacaoRegEx",
            "pattern": "[0-9]{2}",
            "errorMessage": "Obrigatório mínimo 2 n£meros consecutivos.",
            "label": "{{regexTestValidation}}"
        },
        {
            "visible": true,
            "gridColumns": 6,
            "property": "numberRangeValidate",
            "label": "{{cpfMaskApply}}",
            "type": "string",
            "required": false,
            "mask": "999.999.999-99"
        },
        {
            "minValue": 1,
            "visible": true,
            "gridColumns": 6,
            "maxValue": 9,
            "property": "numberValidate",
            "errorMessage": "Somente números de 1 a 9",
            "label": "{{onlyNumbers}}",
            "type": "number",
            "required": false
        }
    ]
}

Busca dos dados onde foram adicionados novos valores:

GET - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas  
{
    "total": 4,
    "hasNext": false,
    "items": [
        {
            "codIdiomPadr": "02 Inglês",
            "codDialet": "",
            "codIdioma": "eng",
            "codUsuario": "",
            "desIdioma": "English",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Teste Branco - teste",
            "id": 13084566
        },
        {
            "codIdiomPadr": "03 Espanhol",
            "codDialet": "pt",
            "codIdioma": "esp",
            "codUsuario": "testeteste",
            "desIdioma": "Espanhol",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "teste",
            "id": 13456727
        },
        {
            "codIdiomPadr": "01 Português",
            "codDialet": "Pt",
            "codIdioma": "por",
            "codUsuario": "alunox",
            "desIdioma": "portugues",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Adm Datasul EMS",
            "id": 493863
        },
        {
            "codIdiomPadr": "99 Outros",
            "codDialet": "Pt",
            "codIdioma": "spa",
            "codUsuario": "Amarildo",
            "desIdioma": "espanhol",
            "hraUltAtualiz": "",
            "datUltAtualiz": null,
            "nomUsuario": "Amarildo",
            "id": 8335959
        }
    ]
}

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 front-end 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-validate-fields]="metadata?.validateFields"
    [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 em 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.
valueAnyContêm o valor atual do campo para ser validado. O tipo é o conforme o campo validado.


Onde o back-end receberá o seguinte JSon:

JSON enviado para o back-end de validação do campo
{
   "property": "campoValidado",
   "value": "valorAtualDoCampoValidado" //Exemplo de uma propriedade de tipo caractere
}

ou 

{
   "property": "campoValidado",
   "value": true //Exemplo de uma propriedade de tipo lógico
}


JSon que retornamos para a tela HTML

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

TagTipoDescrição
valueAnyContêm o novo valor para o campo. O tipo é o conforme o campo validado.
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 também o JSon a ser enviado para a tela HTML, conforme o exemplo abaixo:

JSon encapsulado para UPC de Validação de Campos
// Realizamos a cópia do objeto recebido por parâmetro
oObj = CAST(oBody:clone(), jsonObject).
// adicionamos a propriedade root para encapsular o retorno e enviar para a UPC 
oObj:add("root", oRet).

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

// obtêm o retorno customizado, onde o mesmo foi alterado e retornado na tag root 
// Portanto o resultado da customização deve estar dentro da propriedade 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
propertyCharacterContém o nome do campo que esta sendo validado.
valueAnyContém o valor atual do campo para ser validado. O tipo é o conforme o campo 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',
	value: '',
    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, será devolvida 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 algumas propriedades no componente po-dynamic-form, são elas:

  • p-validate: onde informamos a URL que fará a validação do formulário, neste exemplo será "/api/trn/v1/idiomas/validateForm".
  • p-validate-fields: somente o p-validate não fará com que as requisições de validação sejam executadas, é preciso definir a propriedade p-validate-fields que recebe um array de strings (Ex.: ['codIdioma','desIdioma']) com os campos que irão disparar a requisição ao serviço indicado na propriedade validate, quando estes forem alterados. Essa informação deve vir no metadata de edição para que seja possível customizar as validações de formulário.
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-validate-fields]="metadata?.validateFields"
    [p-value]="record">
  </po-dynamic-form>

As validações de formulário validam somente os campos já existentes no formulário, sejam eles campos padrões ou inseridos nas customizações, não é permitido adicionar novos campos através da validação de formulário. Para adicionar novos campos deve-se utilizar a inserção no back-end alterando os retornos dos endPoints de metadata e dados da API, seja alterando o produto padrão ou via customização.

JSon que recebemos da tela HTML

O componente PO-DYNAMIC-FORM, quando ocorre alguma alteração nos campos indicados no validateFields e ao sair destes campos, realizará uma requisição ao endPoint de validateForm e enviará um JSon com o seguinte formato para o back-end:

TagTipoDescrição
propertyCharacterContém o nome do campo que houve a alteração.
valueJSonObjectContém os valores atuais de todos os atributos utilizados pelos campos do formulário.


Exemplo de Json:

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
valueJSonObjectObjeto com os valores que devem ser alterados. Todos os campos que devem ter seu valor alterado no formulário devem ser informados nesse objeto, aqueles que não devem ser alterados não precisam estar referenciados.
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. Informar o atributo property do campo.
_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, será devolvido para a API Rest apenas o conteúdo da propriedade 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": {
        "desIdioma": "Valor customizado na UPC"
    },     
    "fields": [
        {
            "property": "codIdiomPadr",
            "disabled": true
        }
    ],     
    "focus": "desIdioma",
 	"_messages": [
        {
            "detailedMessage": "Na execução da UPC, houveram alterações nos campos de tela.",
            "code": "44",
            "message": "A UPC alterou algumas caracteristica da tela."
        }
    ]
}

Trabalhando com vários formulários (Dynamic Form)

Em algumas situações precisamos utilizar vários componentes Dynamic Form em uma tela, quando precisamos agrupar campos seja utilizando um po-tabs ou po-accordion, por exemplo.
Nesse caso, há uma questão com relação a validação de formulários. Quando um campo dispara uma validação que altera características de um campo que está em outro formulário, essa validação não é realizada, sendo necessário realizar um tratamento diferente no front-end para o correto funcionamento.

A técnica consiste em atribuir uma função typescript ao p-validate do formulário, nessa função realizar a requisição ao back-end para o endpoint de validação e com o resultado dessa validação atualizar as variáveis de modelo utilizadas em todos os formulários.
Para facilitar esse tratamento foi criada a classe ValidateService no pacote de utilitários Dts-Backoffice-Utils. Você pode utilizar esse pacote para facilitar o desenvolvimento, ou caso prefira, pode copiar o tratamento realizado para o seu projeto.

Segue abaixo os links do projeto:

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. DICAS PARA DESENVOLVER UMA CUSTOMIZAÇÃO

As informações do que pode ser customizado na tela, em qual API REST deve ser cadastrada a UPC, quais os pontos e eventos existentes, entre outras informações, podem ser localizadas no documento de customização da tela. 
Esse documento geralmente é disponibilizado juntamente com o documento de referência da tela.
Porém, em alguns casos pode ainda não existir essa documentação mesmo algumas telas permitindo alguma customização, então abaixo seguem algumas dicas de como podem ser obtidas algumas informações.

  1. Acessando a tela, clicando com o botão direito do mouse, pode-se utilizar a opção "Inspecionar" do navegador (F12). Essa opção irá abrir a "ferramenta do desenvolvedor". 
  2. Na opção Rede (Network) podem ser visualizadas as chamadas feitas pela tela e dentre elas estão as requisições para a API REST. Porém, atente-se ao fato de que, somente serão apresentadas as requisições executadas após o acionamento da funcionalidade de inspeção (F12). Portanto, caso a requisição que você deseja visualizar seja feita na abertura da tela, acione o F12 antes mesmo de executar a tela. 
  3. Uma tela desenvolvida com PO UI que permite customização, geralmente vai executar uma requisição para o endPoint "metadata". Procure por essa chamada na lista apresentada.
  4. Ao clicar na requisição do "metadata", serão apresentadas as informações dessa requisição. Na aba Cabeçalhos (Headers) será possível verificar a URL da API utilizada.
  5. Na aba Resposta (Response) é possível verificar o conteúdo retornado pela API com as informações que compõem a tela.

  1. Cadastre uma UPC para a API Rest conforme apresentado no inicio dessa documentação.
  2.  Adicione linhas de logs na sua UPC para que sejam impressos os parâmetros recebidos por ela, conforme exemplo abaixo.

    UPC
    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 jsonContent AS LONGCHAR NO-UNDO.
    
    //Repassa o conteúdo do JSON em formato de texto do parâmetro jsonIO para uma variável longchar
    jsonContent = jsonIO:GetJsonText().
    
    LOG-MANAGER:WRITE-MESSAGE("UPC EndPoint = " + pEndPoint, ">>>>").
    LOG-MANAGER:WRITE-MESSAGE("UPC Event = " + pEvent, ">>>>").
    LOG-MANAGER:WRITE-MESSAGE("UPC pAPI= " + pAPI, ">>>>").
    LOG-MANAGER:WRITE-MESSAGE("UPC jsonIO = " + STRING(jsonContent), ">>>>").
  3. Diferente de uma UPC de tela Progress que é executada no client e as mensagens são apresentadas em tela, essa UPC será executada pelo servidor PASOE, portanto precisa estar acessível no PROPATH do PASOE e ao executar a tela Web, as mensagens de log serão impressas no log do PASOE. Portanto, é necessário ter acesso ao arquivo de log do PASOE do ambiente testado. 

12. 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:

13. 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 com 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.