Temos como objetivo, implementar técnicas para facilitar a personalização de telas TOTVS - Linha Datasul de forma lowcode, apenas com cadastro de campos por parte do cliente.
A partir da release 12.1.31, são disponibilizados as técnicas e cadastros para implementar a personalização em telas HTML da linha Datasul.
Nesta técnica de personalização, o desenvolvedor deverá realizar o cadastro dos campos a serem personalizados e criar alguns componentes em PO-UI que utilizem os componentes: PO-DYNAMIC-FORM, PO-DYNAMIC-VIEW e PO-PAGE-DYNAMIC-TABLE (este último somente se for necessário). Criar também um endpoint progress que servirá como fonte de dados para os campos personalizados.
Para utilização desta técnica será necessário ter um conhecimento de desenvolvimento com: APIs REST em Progress, Angular, TypeScript e PO-UI.
A Técnica de personalização de telas HTML com PO-UI contempla os seguintes objetos:
1) Endpoint progress do Framework - Serve para obter a lista de campos personalizados, que devem ser cadastrados na tela de Personalização em HTML;
Neste item, deverá ser utilizado o endpoint progress /api/btb/v1/personalizationView/metadata/ + código_do_programa ,onde deve ser passado o Código do Programa Datasul que conterá a lista de campos personalizados.
Exemplo: Se formos criar uma personalização para o programa pedido-execucao-monitor, chamaremos o endpoint "/api/btb/v1/personalizationView/metadata/pedido-execucao-monitor"
{ "fields": [ { "visible": true, "gridColumns": 6, "disable": false, "property": "codIdioma", "label": "Idioma", "type": "string" }, { "visible": true, "gridColumns": 6, "disable": false, "property": "codIdiomPadr", "label": "Idioma Padrão", "type": "string" }, { "visible": true, "gridColumns": 6, "disable": false, "property": "desIdioma", "label": "Descrição", "type": "string" }, { "visible": false, "property": "id", "type": "number", "key": true } ] } |
2) Endpoint progress da área de negócio - Serve para obter os dados que serão apresentados nos campos personalizados;
Abaixo temos uma tabela que mostra os componentes dinâmicos do PO-UI com as respectivas requisições à API REST
Componente PO-UI | Endpoint | Tipo Requisição | Descrição |
---|---|---|---|
po-dynamic-view po-dynamic-form | /byid/nome_do_programa/id | GET | Serve para retornar um registro único, onde recebera no PathParms o "nome do programa" e o "id". |
po-page-dynamic-table | /nome_do_programa | GET | Serve para retornar uma lista de registros, onde receberá no PathParams o "nome do programa". |
po-dynamic-form | /validateForm/nome_do_programa | POST | Serve para validar o formulário, onde receberá no PathParams o "nome do programa". |
po-dynamic-form | /nome_do_programa | POST | Serve para criar um novo registro, onde receberá no PathParams o "nome do programa". |
po-dynamic-form | /nome_do_programa/id | PUT | Serve para alterar um registro, onde receberá no PathParams o "nome do programa" e o "id". |
po-dynamic-form | /nome_do_programa/id | DELETE | Serve para eliminar um registro, onde receberá no PathParams o "nome do programa" e o "id". |
Observação: Em todas os componentes dinâmicos da tabela acima farão uma requisição para obter a lista de campos personalizados na API REST do framework, através da requisição "/api/btb/v1/personalizationView/metadata/" + código_do_programa |
Foi criado no progress o utilitário btb/personalizationUtil.p, com seu include btb/personalizationUtil.i, que serverá para retornar para a área de negócio a lista de campos personalizáveis de um determonado programa, isso facilita para que seja enviado somente os valores dos campos personalizáveis. Por característica do PO-UI dinâmico, caso seja enviado os dados de campos que nao estão na lista de campos, o PO-UI irá apresentar o valor do campo com um label sendo o mesmo nome do campo. Obtendo essa lista de campos, podemos evitar o envio de informações que estão fora dessa lista de campos personalizados.
Include btb/personalizationUtil.i
DEFINE TEMP-TABLE ttPersonalization NO-UNDO FIELD codProgDtsul AS CHARACTER FIELD codField AS CHARACTER FIELD codType AS CHARACTER FIELD codLabel AS CHARACTER FIELD codValid AS CHARACTER FIELD logReadOnly AS LOGICAL INITIAL FALSE FIELD logEnable AS LOGICAL INITIAL TRUE INDEX codigo IS PRIMARY codProgDtsul codField. |
Procedures disponíveis no programa btb/personalizationUtil.p:
Procedure | Parâmetros | Descrição/Exemplo | |
---|---|---|---|
piGetTTPersonalizaton | INPUT cProg AS CHARACTER OUTPUT TABLE ttPersonalization | Retorna a temp-table ttPersonalization com a lista de campos personalizáveis de um determinado programa. Exemplo:
| |
piGetFieldList | INPUT cProg AS CHARACTER OUTPUT cList AS CHARACTER OUTPUT cTypeList AS CHARACTER | Retorna duas listas caracter, uma contendo a lista de campos e uma lista dos seus respectivos tipos, de um determinado programa. OBS: As listas usam como separador a vírgula ",". Exemplo:
|
3) Criação pela área de negócio de um componente PO-DYNAMIC-FORM e PO-DYNAMIC-VIEW. Se desejar, criar também um componente PO-PAGE-DYNAMIC-TABLE, que servirá para navegar nos registros e permitir a visualização e edição dos registros personalizados.
Todos os componentes dinâmicos do PO-UI realizam, no mínimo, duas requisições REST, uma para obter a lista de campos personalizáveis e outra para obter os dados a serem apresentados nesses campos.
Abaixo temos o papel de cada componente dinâmico que podemos utilizar:
Na nossa técnica, em todas as requisições REST, será enviado
2) Para buscar os valores dos dados a serem apresentados, será enviado o código do programa personalizado e também um id do "registro corrente" para obtenção dos valores;
3) Para buscar a lista de campos personalizados utilizando o endpoint progress fornecido pelo framework, que é o /api/btb/v1/personalizationView/metadata/ + codigo_do_programa.
A seguir são apresentados as telas necessárias para a realização do cadastro dos campos personalizados.
Ao localizar no menu o programa "Campos personalizados (html.personalization-metadata)", é apresentada uma tela em formato de 'lista' que conterá todos os campos (metadados) cadastrados no produto Datasul. Para cadastrar um campo que será utilizado na personalização, basta clicar no botão +Adicionar.
A tela a seguir apresenta o cadastro do 'metadado' relacionado a um campo que pode ser apresentado no programa como personalizado.
Campo | Descrição | Obrigatório | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Código Programa Datasul | Código do programa "base" que podem ser aplicadas as técnicas de personalização
| Sim | ||||||||||||||||||||||
Identificador Campo | Identificador único do campo (por programa), necessário para a geração da tela personalizada (código do campo) | Sim | ||||||||||||||||||||||
Nome Campo | Nome do campo que será apresentado na tela (label do campo) Caso o campo não seja informado, o nome do campo apresentado será o informado no identificador. | Não | ||||||||||||||||||||||
Tipo Campo | Tipo do campo cadastrado Caso o campo não seja informado, será considerado que o campo é do tipo string. Tipos de campos permitidos:
| Não | ||||||||||||||||||||||
Validação Campo | Caso o campo possuir alguma validação de máscaras, restrição de valores, é necessário informar neste campo (máscara de formatação do campo) Exemplos: Em um campo personalizado CNPJ, utilizamos o formato de exibição '99.999.999/9999-99'. | Não | ||||||||||||||||||||||
Somente Leitura | Opção para que o campo seja apresentado como 'somente leitura' (torna o campo readOnly) | Sim | ||||||||||||||||||||||
Habilita personalização | Opção para habilitar ou desabilitar a apresentação da personalização por campo (torna o campo visível) | Sim |
Após cadastrar o campo, o mesmo é apresentado na tela inicial onde pode ser realizado filtros sobre seus resultados, bem como efetuar ações de edição ou exclusão.
Ao clicar na opção de editar, não será possível modificar o código do programa Datasul vinculado e também seu identificador. Os demais campos estão habilitados para edição.
Abaixo temos os códigos da parte HTML e TypeScript, que são:
1) Componente HTML (personalization-detail.component.html)
<po-loading-overlay [hidden]="!showLoading"> </po-loading-overlay> <po-page-detail p-title="Detalhe do Idioma" [p-breadcrumb]="breadcrumb" (p-edit)="editClick()" (p-back)="goBackClick()"> <po-dynamic-view [p-fields]="fields" [p-value]="record"> </po-dynamic-view> </po-page-detail |
2) Componente TypeScript (personalization-detail.component.ts)
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components'; import { PersonalizationService } from '../personalization.service'; @Component({ selector: 'app-personalization-detail', templateUrl: './personalization-detail.component.html', styleUrls: ['./personalization-detail.component.css'] }) export class PersonalizationDetailComponent implements OnInit { public static cProg = 'html.aplicativos-eai'; // definicao das variaveis utilizadas public currentId: string; public fields: Array<any> = []; public record = {}; public showLoading = false; public breadcrumb: PoBreadcrumb = { items: [] }; public breadcrumbItem: PoBreadcrumbItem; // construtor com os servicos necessarios constructor( private service: PersonalizationService, private activatedRoute: ActivatedRoute, private route: Router ) { } // load do componente public ngOnInit(): void { this.activatedRoute.params.subscribe(pars => { this.showLoading = true; this.record = {}; // Carrega o registro pelo ID // tslint:disable-next-line:no-string-literal this.currentId = pars['id']; // busca os valores dos dados a serem apresentados this.service.loadValuesById(this.currentId).subscribe(resp => { Object.keys(resp).forEach((key) => this.record[key] = resp[key]); // carrega a lista de campos somente apos receber os dados a serem apresentados this.service.loadMetadata().subscribe(metadata => { // tslint:disable-next-line:no-string-literal this.fields = metadata['fields']; this.showLoading = false; }); }); }); this.setBreadcrumb(); } private setBreadcrumb(): void { this.breadcrumbItem = { label: 'Home', link: '/' }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); this.breadcrumbItem = { label: 'Listagem de Idiomas' , link: '/personalization' }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); this.breadcrumbItem = { label: 'Detalhe do Idioma' }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); } // Redireciona quando clicar no botao Edit public editClick(): void { this.route.navigate(['/personalization', 'edit', this.currentId]); } // Redireciona quando clicar no botao Voltar public goBackClick(): void { this.route.navigate(['/personalization']); } } |
3) Componente HTML (personalization-edit.component.html)
<po-loading-overlay [hidden]="!showLoading"> </po-loading-overlay> <po-page-edit [p-title]="cTitle" [p-breadcrumb]="breadcrumb" [p-disable-submit]="formEdit.form.invalid" (p-cancel)="cancelClick()" (p-save)="saveClick()"> <po-dynamic-form #formEdit p-auto-focus="string" [p-fields]="fields" [p-validate]="validationUrl" [p-value]="record"> </po-dynamic-form> </po-page-edit> |
4) Componente TypeScript (personalization-edit.component.ts)
import { Component, OnInit, ViewChild } from '@angular/core'; import { NgForm } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { PoBreadcrumb, PoBreadcrumbItem, PoDialogService, PoNotificationService } from '@po-ui/ng-components'; import { PersonalizationService } from './../personalization.service'; @Component({ selector: 'app-personalization-edit', templateUrl: './personalization-edit.component.html', styleUrls: ['./personalization-edit.component.css'] }) export class PersonalizationEditComponent implements OnInit { // Define as variaveis a serem utilizadas public cTitle: string; public currentId: string; public record = {}; public fields: Array<any> = []; public isUpdate = false; public showLoading = false; public validationUrl = this.service.getUrlAreaValidation(); public breadcrumb: PoBreadcrumb = { items: [] }; public breadcrumbItem: PoBreadcrumbItem; // Obtem a referencia do componente HTML @ViewChild('formEdit', { static: true }) formEdit: NgForm; // Construtor da classe com os servicos necessarios constructor( private service: PersonalizationService, 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 => { // tslint:disable-next-line:no-string-literal this.currentId = pars['id']; // Se nao tiver o ID definido sera um CREATE if (this.currentId === undefined) { this.isUpdate = false; this.cTitle = 'Novo Idioma'; } else { this.isUpdate = true; this.cTitle = 'Edição do Idioma'; } // Atualiza o breadcrumb de acordo com o tipo de edicao this.setBreadcrumb(); // Se for uma alteracao, busca o registro a ser alterado if (this.isUpdate) { this.service.loadValuesById(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(); } }); } private setBreadcrumb(): void { this.breadcrumbItem = { label: 'Home', action: this.beforeRedirect.bind(this) }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); this.breadcrumbItem = { label: 'Listagem de Idiomas', action: this.beforeRedirect.bind(this) }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); this.breadcrumbItem = { label: this.cTitle }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); } // Retorna a lista de campos private getMetadata() { this.service.loadMetadata().subscribe(metadata => { // tslint:disable-next-line:no-string-literal this.fields = metadata['fields']; this.showLoading = false; }); } // Redireciona via breadcrumb private beforeRedirect(itemBreadcrumbLabel) { if (this.formEdit.valid) { this.route.navigate(['/']); } else { this.poDialog.confirm({ title: 'Cancelamento de edição', message: 'Os dados ainda não foram gravados, confirma redirecinamento ?', confirm: () => this.route.navigate(['/']) }); } } // Grava o registro quando clicado no botao Salvar public saveClick(): void { this.showLoading = true; if (this.isUpdate) { // Altera um registro ja existente this.service.update(this.currentId, this.record).subscribe(resp => { this.poNotification.success('Dados atualizados com sucesso'); this.showLoading = false; this.route.navigate(['/personalization']); }); } else { // Cria um registro novo this.service.create(this.currentId, this.record).subscribe(resp => { this.poNotification.success('Dados criados com sucesso'); this.showLoading = false; this.route.navigate(['/personalization']); }); } } // Cancela a edicao e redireciona ao clicar no botao Cancelar public cancelClick(): void { this.poDialog.confirm({ title: 'Confirmar cancelamento', message: 'Voce deseja realmente cancelar a edição?', confirm: () => this.route.navigate(['/personalization']) }); } } |
5) Componente HTML (personalization-list.component.html)
<po-loading-overlay [hidden]="!showLoading"> </po-loading-overlay> <po-page-dynamic-table p-auto-router p-title="Listagem de Idiomas" [p-actions]="actions" [p-breadcrumb]="breadcrumb" [p-fields]="fields" [p-service-api]="serviceApi"> </po-page-dynamic-table> |
6) Componente TypeScript (personalization-list.component.ts)
import { Component, OnInit } from '@angular/core'; import { PoBreadcrumb, PoBreadcrumbItem } from '@po-ui/ng-components'; import { PoPageDynamicTableActions } from '@po-ui/ng-templates'; import { PersonalizationService } from './../personalization.service'; @Component({ selector: 'app-personalization-list', templateUrl: './personalization-list.component.html', styleUrls: ['./personalization-list.component.css'] }) export class PersonalizationListComponent implements OnInit { // Definicao das variaveis utilizadas public serviceApi: string; public fields: Array<any> = []; public showLoading = false; public literals; public readonly actions: PoPageDynamicTableActions = { new: '/personalization/create', detail: '/personalization/detail/:id', edit: '/personalization/edit/:id', remove: true }; public breadcrumb: PoBreadcrumb = { items: [] }; public breadcrumbItem: PoBreadcrumbItem; // Construtor da classe constructor( private service: PersonalizationService ) { } // Load do componente public ngOnInit(): void { this.fields = []; this.serviceApi = this.service.getUrlArea(); this.showLoading = true; this.service.loadMetadata().subscribe(metadata => { // tslint:disable-next-line:no-string-literal this.fields = metadata['fields']; this.showLoading = false; }); this.setBreadcrumb(); } private setBreadcrumb(): void { this.breadcrumbItem = { label: 'Home', link: '/' }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); this.breadcrumbItem = { label: 'Listagem de Idiomas' }; this.breadcrumb.items = this.breadcrumb.items.concat(this.breadcrumbItem); } } |
7) Componente de serviço (personalization.service.ts)
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PoNotificationService } from '@po-ui/ng-components'; const httpOptions: object = { headers: new HttpHeaders({ 'Content-Type': 'application/json', // 'Authorization': 'Basic ' + btoa('super:super@123'), // 'Access-Control-Allow-Origin': 'http://localhost:4200', // 'Access-Control-Allow-Headers': 'Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Headers, X-Requested-With', // 'returnFormatVersion': '2', }) }; @Injectable({ providedIn: 'root' }) export class PersonalizationService { public progCode = 'html.aplicativos-eai'; // Endpoint progress do framework para obtencao da lista de campos personalizados private urlMetadata = '/api/btb/v1/personalizationView/metadata/'; // Endpoint progress da area de negocio para obtencao dos valores dos campos personalizados private urlArea = '/api/trn/v1/idiomaValues/'; constructor( private http: HttpClient, private poNotification: PoNotificationService, ) { } public loadMetadata() { return this.http.post<any[]>(this.urlMetadata + this.progCode, httpOptions).pipe(); } public loadValuesById(cId) { // tslint:disable-next-line:whitespace return this.http.get<any[]>(this.urlArea + 'byid/' + this.progCode + '/' + cId, httpOptions).pipe(); } public loadAllValues() { return this.http.get<any[]>(this.urlArea + this.progCode + '/', httpOptions).pipe(); } public create(cId, record) { return this.http.post<any[]>(this.urlArea + this.progCode + '/' + cId, record, httpOptions).pipe(); } public update(cId, record) { return this.http.put<any[]>(this.urlArea + this.progCode + '/' + cId, record, httpOptions).pipe(); } public delete(cId, record) { return this.http.put<any[]>(this.urlArea + this.progCode + '/' + cId, record, httpOptions).pipe(); } public getUrlArea(): string { return this.urlArea + this.progCode; } public getUrlAreaValidation(): string { return this.urlArea + 'validateForm/' + this.progCode; } } |
Abaixo temos o endpoint progress que deverá ser criado pela área de negócio para obtenção dos valores a serem apresentados nos campos personalizados.
Endpoint progress da área de negócio para obtenção dos valores dos campos personalizados (idiomaValues.p)
{utp/ut-api.i} {utp/ut-api-action.i pGetData GET /~* } {utp/ut-api-notfound.i} /** Procedure que retorna os valores **/ PROCEDURE pGetData: 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 oObj AS JsonObject NO-UNDO. DEFINE VARIABLE cProg AS CHARACTER NO-UNDO. DEFINE VARIABLE cId AS CHARACTER NO-UNDO. oObj = NEW JsonObject(). // Le os parametros enviados pela interface HTML oRequest = NEW JsonAPIRequestParser(oJsonInput). // Obtem o programa e o codigo do registro corrente cProg = oRequest:getPathParams():getCharacter(1) NO-ERROR. cId = oRequest:getPathParams():getCharacter(2) NO-ERROR. LOG-MANAGER:WRITE-MESSAGE("getData - cProg = " + cProg, ">>>>>"). LOG-MANAGER:WRITE-MESSAGE("getData - cId = " + cId, ">>>>>"). FIND FIRST prog_dtsul WHERE prog_dtsul.cod_prog_dtsul = cProg NO-LOCK NO-ERROR. LOG-MANAGER:WRITE-MESSAGE("getData - prog found = " + string(AVAILABLE prog_dtsul), ">>>>>"). IF AVAILABLE prog_dtsul THEN LOG-MANAGER:WRITE-MESSAGE("getData - permite personalizacao = " + string(prog_dtsul.log_permite_perzdo), ">>>>>"). IF AVAILABLE prog_dtsul AND prog_dtsul.log_permite_perzdo = TRUE THEN DO: FIND FIRST idioma WHERE idioma.cod_idioma = cId NO-LOCK NO-ERROR. LOG-MANAGER:WRITE-MESSAGE("getData - idioma found = " + string(AVAILABLE idioma), ">>>>>"). IF AVAILABLE idioma THEN DO: oObj:add("codIdioma", idioma.cod_idioma). oObj:add("desIdioma", idioma.des_idioma). oObj:add("codIdiomPadr", idioma.cod_idiom_padr). END. LOG-MANAGER:WRITE-MESSAGE("getData - oObj = " + String(oObj:getJsonText()), ">>>>>"). END. oResponse = NEW JsonAPIResponse(oObj). oJsonOutput = oResponse:createJsonResponse(). END PROCEDURE. /* fim */ |
{ "codIdiomPadr": "03 Español", "codIdioma": "ESP", "desIdioma": "Espanhol" } |
Caso seja enviado valores da área de negócio que não estejam cadastrados como campos personalizados, o PO-UI por padrão adicionará essa informação extra na tela, onde será apresentado como String sem um label válido. |
fwk-tools-jille/DATASUL/personalization-poui/ ( https://github.com/totvs/fwk-tools-jille )
A ideia era apresentar uma técnica de construção para a personalização de um programa TOTVS da Liinha Datasul, de forma segura e simples.
Esta documentação trata-se de um MVP, que está sendo continuamente evoluída em nossas Sprints (SQUAD TOOLS).
<!-- esconder o menu --> <style> div.theme-default .ia-splitter #main { margin-left: 0px; } .ia-fixed-sidebar, .ia-splitter-left { display: none; } #main { padding-left: 10px; padding-right: 10px; overflow-x: hidden; } .aui-header-primary .aui-nav, .aui-page-panel { margin-left: 0px !important; } .aui-header-primary .aui-nav { margin-left: 0px !important; } </style> |