Este exemplo mostra como declarar um DTO de filtro que:
@Filterable para mapear campos para Specifications
Spring Data.@UISchema para que o frontend renderize
os componentes corretos./schemas/filtered.package com.example.hr.employee.filter;
import org.praxisplatform.uischema.FieldControlType;
import org.praxisplatform.uischema.NumericFormat;
import org.praxisplatform.uischema.extension.annotation.UISchema;
import org.praxisplatform.uischema.filter.annotation.Filterable;
import org.praxisplatform.uischema.filter.dto.GenericFilterDTO;
import java.math.BigDecimal;
import java.time.LocalDate;
public class EmployeeFilterDTO implements GenericFilterDTO {
@Filterable(operation = Filterable.FilterOperation.LIKE, relation = "person.name")
@UISchema(
label = "Nome",
placeholder = "Busque por nome ou parte",
controlType = FieldControlType.INPUT,
order = 10
)
private String name;
@Filterable(operation = Filterable.FilterOperation.EQUAL, relation = "department.id")
@UISchema(
label = "Departamento",
controlType = FieldControlType.SEARCHABLE_SELECT,
endpoint = "/api/human-resources/departamentos/options/filter",
valueField = "id",
displayField = "label",
order = 20
)
private Long departmentId;
@Filterable(operation = Filterable.FilterOperation.IN, relation = "communicationChannels.id")
@UISchema(
label = "Canais",
controlType = FieldControlType.SELECTION_LIST,
endpoint = "/api/human-resources/communication-channels/options/filter",
valueField = "id",
displayField = "label",
multiple = true,
order = 25
)
private java.util.List<Long> channelIds;
@UISchema(
label = "Visualização",
controlType = FieldControlType.BUTTON_TOGGLE,
options = "[{\"label\":\"Ativos\",\"value\":\"ACTIVE\"},{\"label\":\"Todos\",\"value\":\"ALL\"}]",
order = 27
)
private String viewMode;
@Filterable(operation = Filterable.FilterOperation.GREATER_OR_EQUAL, relation = "admissionDate")
@UISchema(
label = "Admitido a partir de",
controlType = FieldControlType.DATE_PICKER,
order = 30
)
private LocalDate admissionDateFrom;
@Filterable(operation = Filterable.FilterOperation.GREATER_OR_EQUAL, relation = "salary")
@UISchema(
label = "Salário mínimo",
controlType = FieldControlType.CURRENCY_INPUT,
numericFormat = NumericFormat.CURRENCY,
numericStep = "0.01",
order = 40
)
private BigDecimal salaryMin;
@UISchema(
label = "Cor da etiqueta",
controlType = FieldControlType.COLOR_INPUT,
order = 45
)
private String tagColor;
// getters e setters omitidos
}Quando o CustomOpenApiResolver processa esse DTO:
components.schemas.EmployeeFilterDTO.properties.<campo>.x-ui
com as configurações acima;@NotNull) seriam
convertidas automaticamente em x-ui.validation;GET /schemas/filtered?path=/api/human-resources/funcionarios/filter&operation=post&schemaType=request
retornará o schema de request do filtro;GET /schemas/filtered?path=/api/human-resources/funcionarios/all&operation=get&schemaType=response
retornará o schema de response da listagem.No Angular, o GenericCrudService normaliza esse contrato
para a forma canônica de runtime:
endpoint -> resourcePathdisplayField -> optionLabelKeyvalueField -> optionValueKeyfilter -> filterCriteriaOu seja: no backend Java o contrato anotado continua sendo
endpoint/displayField/valueField,
mas a UI Praxis passa a operar internamente com
resourcePath e chaves option*Key, revalidando
o schema com If-None-Match e consumindo ETag e
X-Schema-Hash.
Observações de maturidade do cenário atual:
SELECTION_LIST já tem runtime Angular utilizável, mas
ainda não cobre totalmente toda a superfície declarada de metadata; para
filtros enterprise que dependam fortemente de searchable ou
selectAll, prefira validar o comportamento final antes de
padronizar esse controle em larga escala.COLOR_INPUT é a escolha padrão para cor simples: a
heurística automática do starter já o infere para
format=color e nomes contendo cor/color.
Reserve COLOR_PICKER para contratos que precisem de
paleta/presets ou picker rico.order) para que o frontend mantenha a
consistência visual.endpoint para publicar combos dinâmicos no
x-ui, preferencialmente apontando para
/{resource}/options/filter; no Angular esse campo será
normalizado para resourcePath.valueField e displayField
explicitamente para catálogos remotos; a UI Praxis os normaliza para
optionValueKey e optionLabelKey.Filterable.FilterOperation com regras de
domínio quando precisar de comportamentos além dos operadores
padrão.controlType com a família
canônica INLINE_* em vez de depender de convenções
implícitas de filtro.Para filtros monetários, prefira um objeto com nomes explícitos em vez de lista posicional:
{
"salaryRange": {
"minPrice": 6500,
"maxPrice": 15000,
"currency": "BRL"
}
}Contrato OpenAPI publicado para ranges:
@Filterable com operação de range
publica oneOf com duas variantes aceitas:
BETWEEN/NOT_BETWEEN/OUTSIDE_RANGE:
[min], [min,max] ou
[null,max];[min,null] é aceito e
normalizado para [min] no backend;BETWEEN_EXCLUSIVE: exige exatamente dois limites não
nulos ([min,max]).{ minPrice, maxPrice, currency? } para
monetário ou { startDate, endDate } para datas), com regra
OpenAPI para obrigar ao menos um limite (ou ambos no
BETWEEN_EXCLUSIVE).Regras recomendadas:
minPrice e maxPrice opcionais
(null permitido) para operações não exclusivas.>= min ou
<= max).[null, maxPrice] no payload canônico para preservar
semântica.[null],
[null,null] ou objeto sem nenhum limite efetivo.[a,b,c].1500 diretamente, use lista ou objeto canônico.null sem
contrapartida válida também são inválidos e devem retornar
400.BETWEEN_EXCLUSIVE é estrito: exige os dois limites
preenchidos.minPrice > maxPrice, normalizar (swap) e
registrar aviso de validação.NumericFormat.PERCENT ou
format: percent), o metadata publica
rangeSlider com mode=range e defaults
min=0, max=100, step=0.01.currency pode ser enviado como contexto de UX, mas o
backend considera apenas os limites para construir o predicado.between[] apenas como compatibilidade com
legado.controlType de range no schema devem ser
colapsados para formato canônico.Compatibilidade legada controlada por configuração:
praxis.filter.range.allow-scalar-payload=false), rejeita
payload escalar (400).praxis.filter.range.allow-scalar-payload=true para aceitar
escalar e normalizar para lista.praxis.filter.range.log-legacy-scalar-payload=true (padrão)
registra uso de payload escalar compatível.400 com
errors[].properties.code = FILTER_PAYLOAD_INVALID.