Histórico da Página
CONTEÚDO
- Introdução / Objetivo
- Visão Geral
- Pré-Requisitos
- Técnicas
- Back-End Progress
- Front-End PO-UI
- Exemplo de utilização
- Back-End Progress
- Front-End PO-UI
- Facilitadores Progress
- Links Úteis
- Conclusão
Índice
01. INTRODUÇÃO / OBJETIVO
...
Informações | ||
---|---|---|
| ||
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 (https://tdninterno.totvs.com/display/public/FRAMJOI/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.
04. TÉCNICAS
Técnica Back-End Progress:
Introdução:
A técnica Back-end Progress é formada pelos passos abaixo:
Construção de API
...
Construção de API REST para tela customizada:
...
Informações | ||
---|---|---|
| ||
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:
Parametro | Tipo | Tipo de Dados | Descrição |
---|---|---|---|
pEndPoint | INPUT | CHARACTER | Contem o nome do endpoint que está sendo executado. |
pEvent | INPUT | CHARACTER | Contem o nome do evento que está sendo executado. |
pAPI | INPUT | CHARACTER | Contem o nome da API que está sendo executada. |
jsonIO | INPUT-OUTPUT | JSONObject | Contem 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.
...
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.
...
Bloco de código | ||||||
---|---|---|---|---|---|---|
| ||||||
Busca do METADADOS onde foram adicionados os novos campos codUsuario, nomUsuario e codDialet: POST - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas/metadata { "total": 9, "hasNext": false, "items": [ { "visible": true, "gridColumns": 6, "disable": true, "property": "codIdioma", "label": "Idioma", "type": "string" }, { "visible": true, "gridColumns": 6, "property": "desIdioma", "label": "Descrição", "type": "string", "required": true }, { "visible": true, "gridColumns": 6, "property": "codIdiomPadr", "label": "Idioma Padrão", "type": "string" }, { "visible": true, "gridColumns": 6, "disable": true, "property": "datUltAtualiz", "format": "dd/MM/yyyy", "label": "Última Atualização", "type": "date" }, { "visible": true, "gridColumns": 6, "disable": true, "property": "hraUltAtualiz", "label": "Hora Última Atualização", "type": "string" }, { "visible": false, "property": "id", "type": "number", "key": true }, { "visible": true, "gridColumns": 6, "property": "codUsuario", "label": "Usuário", "type": "string", "required": true }, { "visible": true, "gridColumns": 6, "property": "nomUsuario", "label": "Nome", "type": "string", "required": true }, { "visible": true, "gridColumns": 6, "property": "codDialet", "label": "Dialeto", "type": "string", "required": true } ] } Busca dos dados onde foram adicionados novos valores: GET - http://localhost:8180/dts/datasul-rest/resources/prg/trn/v1/idiomas { "total": 3, "hasNext": false, "items": [ { "codIdiomPadr": "99 Outros", "codDialet": "Pt", "codIdioma": "ale", "codUsuario": "super", "desIdioma": "Alemão", "hraUltAtualiz": "", "datUltAtualiz": null, "nomUsuario": "Super", "id": 4580144 }, { "codIdiomPadr": "99 Outros", "codDialet": "PT", "codIdioma": "EN", "codUsuario": "joao", "desIdioma": "Ingles", "hraUltAtualiz": "", "datUltAtualiz": null, "nomUsuario": "Joao da Silva", "id": 194736 }, { "codIdiomPadr": "03 Espanhol", "codDialet": "PT", "codIdioma": "ES", "codUsuario": "Manoel", "desIdioma": "Espanhol", "hraUltAtualiz": "", "datUltAtualiz": null, "nomUsuario": "Manoel da Silva", "id": 2968898 } ] } |
Front-End PO-UI
Introdução:
Para este exemplo vamos criar um CRUD com template dinâmico, onde serão mostrados os dados de acordo com o que o back-end retornar.
...
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.
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
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.
...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
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.
...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
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.
...
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
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ções 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.
Bloco de código | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
...
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).
jAList:add(jObj).
... |
Validação de componentes na interface
Uma boa prática em desenvolvimento de telas é a validação de alguns campos na própria interface, cujo intuito é reduzir requisições desnecessárias ao 'Back-End'. As funcionalidades apresentadas a seguir podem ser utilizadas conjunto com a validação do próprio Form ([p-disable-submit]="formEdit.form.invalid"), no qual pode desabilitar o botão de confirmação enquanto houver campos inválidos.
Utilização do pattern (RegEx)
Para a validação de campos textos, pode ser utilizado o atributo pattern qualquer expressão regular, caso não atenda ao RegEx, uma mensagem de erro definida em errorMessage é apresentada em tela.
Bloco de código | ||||||
---|---|---|---|---|---|---|
| ||||||
...
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.
Bloco de código | ||||||
---|---|---|---|---|---|---|
| ||||||
...
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.
Bloco de código | ||||||
---|---|---|---|---|---|---|
| ||||||
...
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).
... |
07. Facilitadores Progress
Criamos facilitadores para auxiliar no desenvolvimento das API's, ficam localizados na classe Progress "com.
...
Criamos facilitadores para auxiliar no desenvolvimento das API's, ficam localizados na classe Progress "com.totvs.framework.api.JsonAPIUtils":
Método | Descrição | Assinatura/Exemplo |
---|---|---|
convertAblTypeToHtmlType | Converte 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. |
convertToCamelCase | Converter 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. |
getIdField | Retorna 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() ). |
...
08. Links Úteis
Documentação API Datasul:
PO-UI:
- Migração THF PO-UI (https://po-ui.io/guides/migration-thf-to-po-ui)
- Dynamic-Form (https://po-ui.io/documentation/po-dynamic-form);
- Dynamic-View (https://po-ui.io/documentation/po-dynamic-view);
- Page-Dynamic-Detail (https://po-ui.io/documentation/po-page-dynamic-detail);
- Page-Dymic-Edit (https://po-ui.io/documentation/po-page-dynamic-edit);
- Page-Dynamic-Search (https://po-ui.io/documentation/po-page-dynamic-search);
- Page-Dynamic-Table (https://po-ui.io/documentation/po-page-dynamic-table);
GIT Projeto:
fwk-tools-jille/DATASUL/customization-poui/ ( https://github.com/totvs/fwk-tools-jille )
...
09. 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.
...