initial
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: vitruvio-adicionar-menu
|
||||
description: >
|
||||
Use ONLY when the user explicitly asks to add a menu item or menu entry in a Vitruvio repository.
|
||||
Triggers: "add menu item", "add to menu", "adicionar ao menu", "criar item de menu",
|
||||
"add panel to menu", "adicionar painel no menu", or any explicit request to register
|
||||
something in the vitruvio.json "menu" array.
|
||||
Do NOT trigger for general panel/process/script creation — menu entries are separate.
|
||||
---
|
||||
|
||||
# Add Vitruvio Menu Item
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are adding an entry to the `menu` array in `vitruvio.json`. Menu entries are **never created automatically** — only when the user explicitly requests it. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Show the current menu structure
|
||||
|
||||
Read `vitruvio.json` and print the menu tree as a readable outline. Example format:
|
||||
|
||||
```
|
||||
menu:
|
||||
[0] menu-root "Meu Módulo" (MENU)
|
||||
[0] menu-panel "Meu Painel" (PAINEL → my-panel)
|
||||
[1] menu-report "Meu Relatório" (RELATORIO → my-report)
|
||||
```
|
||||
|
||||
If the `menu` array is empty or absent, say so.
|
||||
|
||||
## Step 3 — Collect item details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **Type** — one of:
|
||||
- `PAINEL` — links to a panel (`panelKey`)
|
||||
- `RELATORIO` — links to a report (`reportKey`)
|
||||
- `PROCESSO` — links to a process (`processKey`)
|
||||
- `MENU` — a submenu/folder (no artifact link, has `children`)
|
||||
- **Target artifact key** — the `key` of the panel/report/process to link (skip if type is `MENU`)
|
||||
- **Name** — the label shown in the menu
|
||||
- **Key** — unique key for this menu entry (suggest `menu-<artifactKey>` as default)
|
||||
- **Parent** — root level, or inside which existing `MENU` item? Show the options from Step 2.
|
||||
- **Order** — integer position within its parent (suggest next available based on existing siblings)
|
||||
- **Icon** — only relevant for root-level `MENU` items. Vitruvio uses FontAwesome numeric codes (e.g. `61441` = fa-adjust). For children, always `null`.
|
||||
|
||||
## Step 4 — Build the JSON entry
|
||||
|
||||
### Type: PAINEL
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"icon": null,
|
||||
"order": <order>,
|
||||
"type": "PAINEL",
|
||||
"panelKey": "<panelKey>",
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
### Type: RELATORIO
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"icon": null,
|
||||
"order": <order>,
|
||||
"type": "RELATORIO",
|
||||
"reportKey": "<reportKey>",
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
### Type: PROCESSO
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"icon": null,
|
||||
"order": <order>,
|
||||
"type": "PROCESSO",
|
||||
"processKey": "<processKey>",
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
### Type: MENU (submenu / root folder)
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"icon": <iconCode or null>,
|
||||
"order": <order>,
|
||||
"type": "MENU",
|
||||
"children": []
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5 — Insert into vitruvio.json
|
||||
|
||||
- Read `vitruvio.json`.
|
||||
- If `menu` array does not exist, create it as an empty array first.
|
||||
- If the user chose **root level**: append the entry to the top-level `menu` array.
|
||||
- If the user chose a **parent item**: find the parent entry by key inside `menu` (search recursively if needed) and append to its `children` array.
|
||||
- Check that the chosen `key` is not already used anywhere in the menu tree before inserting.
|
||||
- Write the updated file back preserving formatting (2-space indent).
|
||||
|
||||
## Step 6 — Report
|
||||
|
||||
Tell the user:
|
||||
- Added `<type>` entry `<key>` ("Name") at `<location>` with order `<order>`
|
||||
- Remind them: menu order is relative within siblings — reorder adjacent items if needed
|
||||
- Remind them: `icon` values are FontAwesome numeric codes; use `null` for leaf items
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: vitruvio-atualizar-manifesto
|
||||
description: >
|
||||
Use when the user wants to update, rename, or remove an existing artifact registration
|
||||
in vitruvio.json. Triggers: "update the category/description/authMode of", "change the name of",
|
||||
"rename this panel/script/endpoint", "remove this artifact", "unregister", "atualizar manifesto",
|
||||
"mudar categoria", "remover painel", or any request to modify fields of an already-registered artifact.
|
||||
Do NOT trigger for creating new artifacts (use vitruvio-criar-* skills) or adding menu entries (use vitruvio-adicionar-menu).
|
||||
---
|
||||
|
||||
# Update Vitruvio Manifest Entry
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are modifying an existing entry in `vitruvio.json`. No new files — only manifest changes (and optionally file moves/deletions). Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Identify operation and target
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **Operation** — one of:
|
||||
- **A — Update fields**: change metadata on an existing entry (category, description, authMode, allowedGroups, displayOrder, etc.)
|
||||
- **B — Remove entry**: unregister an artifact from `vitruvio.json`
|
||||
- **C — Rename key**: change the artifact's key (breaking change — see Operation C below)
|
||||
- **Target artifact** — the key of the artifact to modify, and its section (panel, script, endpoint, query, report, library, process).
|
||||
|
||||
Read `vitruvio.json` and show the user the current entry before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## Operation A — Update fields
|
||||
|
||||
Ask what fields to change and their new values. Then edit `vitruvio.json` with the updated values, preserving all other fields exactly.
|
||||
|
||||
### Editable fields by artifact type
|
||||
|
||||
**Panel:** `name`, `description`, `category`, `displayOrder`, `showInPresentation`, `openInNewWindow`, `showInMobileList`, `displayTimeInSeconds`, `allowedGroups`, `allowedUsers`, `forms.mobile`, `forms.mobileAlternative`, `defaultState`, `thumbnail`
|
||||
|
||||
**Script:** `name`, `description`, `domain` (`USUARIO` / `SISTEMA`)
|
||||
|
||||
**Endpoint:** `name`, `description`, `authMode` (`PUBLIC`, `STATIC_TOKEN`, `VITRUVIO_WS_USER_AUTH`, `HTTP_BASIC_AUTH`), `active`
|
||||
|
||||
**Query:** `name`, `connection`
|
||||
|
||||
**Report:** `name`, `description`, `category`, `owner`, `orientation`, `parameterForm`, `allowedGroups`, `allowedUsers`
|
||||
|
||||
**Library:** `name`, `authMode`, `mobileEnabled`
|
||||
|
||||
**Process:** `name`, `description`, `forms.mobile`, `forms.mobileAlternative`
|
||||
|
||||
After editing, report each field that changed (old value → new value).
|
||||
|
||||
---
|
||||
|
||||
## Operation B — Remove entry
|
||||
|
||||
1. Show the full current entry from `vitruvio.json` to the user.
|
||||
2. Ask for explicit confirmation before proceeding — do not remove without a yes.
|
||||
3. Remove the entry from its section array in `vitruvio.json`. If the section array becomes empty, remove the array key entirely.
|
||||
4. Ask the user: **"Do you also want to delete the artifact's files from disk?"** — list which files/directories would be deleted. Do not delete anything without an explicit yes.
|
||||
5. If yes, delete the files.
|
||||
6. Report: entry removed from `vitruvio.json`; files deleted if confirmed.
|
||||
|
||||
---
|
||||
|
||||
## Operation C — Rename key
|
||||
|
||||
> **Warning to show the user before proceeding:**
|
||||
> Renaming a key is a breaking change. Any code that references the old key — `engine.showPanel('<old-key>')`, `libService.loadScript('<old-key>')`, menu entries, process variables, external integrations — will break. Search the repo for the old key before confirming.
|
||||
|
||||
1. Show the warning above and ask the user to confirm they understand.
|
||||
2. Ask for the new key (kebab-case). Check it is not already used in the same section of `vitruvio.json`.
|
||||
3. Update the `key` field in `vitruvio.json`.
|
||||
4. Update any `source`, `bpmn`, `template`, `parameterForm`, `files`, or `forms.*` paths inside the same entry that include the old key in their path.
|
||||
5. Rename the artifact's directory or file on disk:
|
||||
- `panels/<old-key>/` → `panels/<new-key>/`
|
||||
- `scripts/<old-key>.js` → `scripts/<new-key>.js`
|
||||
- `endpoints/<old-key>.js` → `endpoints/<new-key>.js`
|
||||
- `queries/<old-key>.sql` → `queries/<new-key>.sql`
|
||||
- `reports/<old-key>/` → `reports/<new-key>/`
|
||||
- `libraries/<old-key>/` → `libraries/<new-key>/`
|
||||
- `processes/<old-key>/` → `processes/<new-key>/`
|
||||
6. Check if a menu entry references the old key (`panelKey`, `reportKey`, `processKey`) and update it too.
|
||||
7. Report: what was renamed in `vitruvio.json` and on disk. Remind the user to search the codebase for any remaining hardcoded references to the old key:
|
||||
|
||||
```bash
|
||||
grep -r "<old-key>" --include="*.xml" --include="*.js" --include="*.json" .
|
||||
```
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: vitruvio-criar-biblioteca
|
||||
description: >
|
||||
Use when the user wants to create a new static file library in a Vitruvio repository.
|
||||
Triggers: "create library", "new library", "criar biblioteca", "nova biblioteca", "add library",
|
||||
"static files", "JS library", "CSS library", or any request to scaffold a libraries/<key>/ directory
|
||||
and register it in vitruvio.json.
|
||||
---
|
||||
|
||||
# Create Vitruvio Library
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating a new static file library inside a Vitruvio repository. Libraries are static file bundles (JS, CSS, images) served by the platform as HTTP resources. They are different from scripts — scripts run server-side on Rhino; libraries are served to clients as-is. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect library details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing from their original request):
|
||||
|
||||
- **Key (sigla)** — kebab-case unique identifier. Used to reference the library and build its endpoint URL. Stable — changing it breaks any code that references it.
|
||||
- **Name** — human-readable label shown in the Vitruvio UI.
|
||||
- **Description** — one sentence about what this library provides (optional).
|
||||
- **Auth mode** — one of:
|
||||
- `PUBLIC` — accessible without authentication (default; use for most JS/CSS bundles)
|
||||
- `STATIC_TOKEN` — token required; use when files must not be publicly accessible
|
||||
- **Mobile enabled?** — `true` if mobile clients need to load these files; `false` otherwise (default: `false`).
|
||||
- **What files will this library contain?** — brief description so you can create a useful starting placeholder (e.g. "custom JS utilities for panels", "CSS theme overrides", "image assets").
|
||||
|
||||
## Step 3 — Scaffold and create files
|
||||
|
||||
```bash
|
||||
vitruvio new library <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates the `libraries/<key>/` directory and registers it in `vitruvio.json` with `authMode: "PUBLIC"` and `mobileEnabled: false`. Then create a placeholder file appropriate to what the user described:
|
||||
|
||||
- For a **JS library**: `libraries/<key>/<key>.js`
|
||||
- For a **CSS library**: `libraries/<key>/<key>.css`
|
||||
- For an **image/mixed library**: `libraries/<key>/README.md` explaining what belongs here
|
||||
|
||||
### JS placeholder
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Library: <name>
|
||||
* Key: <key>
|
||||
* Description: <description>
|
||||
*
|
||||
* These files are served as static HTTP resources.
|
||||
* Access URL: vBibliotecaService.buildEndpointUrl('<key>', '<key>.js')
|
||||
*/
|
||||
|
||||
// Add your client-side JavaScript here.
|
||||
// This runs in the browser — full ES6+ is supported (unlike server-side Rhino scripts).
|
||||
```
|
||||
|
||||
### CSS placeholder
|
||||
|
||||
```css
|
||||
/**
|
||||
* Library: <name>
|
||||
* Key: <key>
|
||||
* Description: <description>
|
||||
*
|
||||
* These files are served as static HTTP resources.
|
||||
* Access URL: vBibliotecaService.buildEndpointUrl('<key>', '<key>.css')
|
||||
*/
|
||||
|
||||
/* Add your styles here */
|
||||
```
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the library entry. The full entry shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"type": "LOCAL",
|
||||
"authMode": "PUBLIC",
|
||||
"authToken": null,
|
||||
"mobileEnabled": false,
|
||||
"files": "libraries/<key>/"
|
||||
}
|
||||
```
|
||||
|
||||
Update only what differs: set `"authMode"` if not `"PUBLIC"`; set `"mobileEnabled": true` if mobile clients need this library; add `"description"` if provided.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- Directory created: `libraries/<key>/`
|
||||
- Placeholder file(s) created
|
||||
- Registered in `vitruvio.json` with key `<key>`
|
||||
- How to get the serving URL at runtime:
|
||||
```javascript
|
||||
var url = vBibliotecaService.buildEndpointUrl('<key>', 'filename.js');
|
||||
```
|
||||
- Remind them: files in this directory are served as-is — client-side JS here can use modern ES6+, unlike server-side Rhino scripts
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Nome: Parâmetro de indicadores
|
||||
* Sigla: dashboard_ia
|
||||
* Descrição: Biblioteca compartilhada dos dashboards de indicadores da Diretoria: preferências de usuário (tema), CSS do topbar e toolbar, helpers de tema.
|
||||
*/
|
||||
(function() {
|
||||
var PREF_NS = 'preferences';
|
||||
|
||||
function loadPreferences(login) {
|
||||
try {
|
||||
var raw = vConfigService.getUserConfigAsString(String(login), PREF_NS);
|
||||
if (raw != null && String(raw) !== '') {
|
||||
return JSON.parse(String(raw));
|
||||
}
|
||||
} catch (e) {}
|
||||
return { dark_mode: true };
|
||||
}
|
||||
|
||||
function savePreferences(login, prefs) {
|
||||
try {
|
||||
vConfigService.saveUserConfig(String(login), PREF_NS, JSON.stringify(prefs));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function getTheme(engine) {
|
||||
return String(engine.getGlobalVariable('theme') || 'dark');
|
||||
}
|
||||
|
||||
function isLight(engine) {
|
||||
return getTheme(engine) === 'light';
|
||||
}
|
||||
|
||||
function applyTheme(themeName, page, btnTheme) {
|
||||
page.getJavaScript().execute(
|
||||
themeName === 'light'
|
||||
? "document.body.classList.add('theme-light-app');"
|
||||
: "document.body.classList.remove('theme-light-app');"
|
||||
);
|
||||
if (btnTheme != null) {
|
||||
btnTheme.setCaption(themeName === 'dark' ? '☽' : '☀');
|
||||
}
|
||||
}
|
||||
|
||||
function addSharedCss(page) {
|
||||
var style =
|
||||
".dash-topbar { background:#0f1923 !important; border-bottom:1px solid #243447; } " +
|
||||
".dash-topbar .v-button { background:transparent !important; border:1px solid transparent !important; color:#556677 !important; border-radius:4px !important; transition:none !important; } " +
|
||||
".dash-topbar .v-button .v-button-caption { color:#556677 !important; } " +
|
||||
".dash-topbar .v-button.dash-nav-active { background:#1a2733 !important; border-color:#4a9edd !important; } " +
|
||||
".dash-topbar .v-button.dash-nav-active .v-button-caption { color:#4a9edd !important; font-weight:bold !important; } " +
|
||||
".dash-topbar .v-caption { color:#8899aa !important; font-size:10px !important; } " +
|
||||
".dash-topbar input.v-textfield, .dash-topbar .v-datefield-textfield, .dash-topbar input.v-filterselect-input { background:#1a2733 !important; color:#ccc !important; border-color:#243447 !important; } " +
|
||||
".dash-topbar .v-datefield-button { background:#1a2733 !important; border-color:#243447 !important; } " +
|
||||
".dash-topbar .v-select-select { background:#1a2733 !important; color:#ccc !important; border-color:#243447 !important; } " +
|
||||
"body.theme-light-app .dash-topbar { background:#f8fafc !important; border-bottom:1px solid #e2e8f0; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button { color:#475569 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button .v-button-caption { color:#475569 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button.dash-nav-active { background:#dbeafe !important; border-color:#2563eb !important; color:#2563eb !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button.dash-nav-active .v-button-caption { color:#2563eb !important; } " +
|
||||
"body.theme-light-app .dash-topbar input.v-textfield, body.theme-light-app .dash-topbar .v-datefield-textfield, body.theme-light-app .dash-topbar input.v-filterselect-input { background:#ffffff !important; color:#334155 !important; border-color:#e2e8f0 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-datefield-button { background:#ffffff !important; border-color:#e2e8f0 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-select-select { background:#ffffff !important; color:#334155 !important; border-color:#e2e8f0 !important; } " +
|
||||
".theme-light.dash-wrapper { background:#f0f2f5; } " +
|
||||
".theme-light .dash-kpi { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " +
|
||||
".theme-light .dash-kpi-label { color:#64748b; } " +
|
||||
".theme-light .dash-kpi-value { color:#1e293b; } " +
|
||||
".theme-light .dash-chart-box { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " +
|
||||
".theme-light .dash-chart-title { color:#64748b; } " +
|
||||
".mov-toolbar { display:flex; align-items:center; gap:8px; margin-bottom:8px; } " +
|
||||
".mov-filter-input { background:#1a2733; border:1px solid #243447; color:#ccc; padding:5px 10px; border-radius:4px; font-size:11px; flex:1; outline:none; font-family:Arial; } " +
|
||||
".mov-filter-input:focus { border-color:#4a9edd; } " +
|
||||
".mov-export-btn { background:#1a2733; border:1px solid #243447; color:#8899aa; padding:5px 12px; border-radius:4px; font-size:11px; cursor:pointer; white-space:nowrap; font-family:Arial; } " +
|
||||
".mov-export-btn:hover { border-color:#4a9edd; color:#4a9edd; } " +
|
||||
".theme-light .mov-filter-input { background:#ffffff; border-color:#e2e8f0; color:#334155; } " +
|
||||
".theme-light .mov-filter-input:focus { border-color:#2563eb; } " +
|
||||
".theme-light .mov-export-btn { background:#ffffff; border-color:#e2e8f0; color:#64748b; } " +
|
||||
".theme-light .mov-export-btn:hover { border-color:#2563eb; color:#2563eb; } ";
|
||||
|
||||
page.getStyles().add(style);
|
||||
}
|
||||
|
||||
return {
|
||||
loadPreferences: loadPreferences,
|
||||
savePreferences: savePreferences,
|
||||
getTheme: getTheme,
|
||||
isLight: isLight,
|
||||
applyTheme: applyTheme,
|
||||
addSharedCss: addSharedCss
|
||||
};
|
||||
})()
|
||||
@@ -0,0 +1,784 @@
|
||||
---
|
||||
name: vitruvio-criar-dashboard-mobile
|
||||
description: >
|
||||
Use when creating the MOBILE version of an existing Vitruvio indicator dashboard panel — a
|
||||
DesktopPanel shell (`<key>-mobile.xml`) that delegates to `<key>-desktop.xml`, plus the mobile
|
||||
detection/adaptation added to the desktop form: collapsible filter sidebar, stacked cards,
|
||||
dark/light theme toggle, and developer mobile preview. Triggers: "criar mobile do dashboard",
|
||||
"adicionar versão mobile ao indicador", "mobile preview do painel indicador",
|
||||
"CriarMobileIndicador". Called by vitruvio-criar-indicador-dashboard for the mobile part; can
|
||||
also be invoked directly with the target `<key>-desktop.xml` path. For the desktop part use
|
||||
vitruvio-criar-dashboard-desktop.
|
||||
---
|
||||
|
||||
# Criar Mobile para Painel Indicador
|
||||
|
||||
> Todas as mensagens ao usuário devem ser em Português.
|
||||
|
||||
Você está criando a versão mobile de um painel Vitruvio existente. O padrão usado neste repositório é o **DesktopPanel delegado**: o `<key>-mobile.xml` é um shell mínimo que delega toda a renderização ao `<key>-desktop.xml`, que detecta o contexto mobile e adapta o layout sozinho.
|
||||
|
||||
Receba o caminho do `<key>-desktop.xml` alvo como argumento (ex: `panels/meu-painel/meu-painel-desktop.xml`).
|
||||
|
||||
---
|
||||
|
||||
## LEITURA OBRIGATÓRIA AO INICIAR — CONTEXT.md
|
||||
|
||||
**Esta é a primeira coisa a fazer, antes de abrir qualquer outro arquivo.**
|
||||
|
||||
Derive o diretório do painel a partir do argumento e leia:
|
||||
|
||||
```
|
||||
panels/<panel-key>/CONTEXT.md
|
||||
```
|
||||
|
||||
**Se o CONTEXT.md existir:**
|
||||
- Leia-o completo primeiro. Ele contém os IDs reais dos campos, layouts, funções JS e o estado atual do painel — tudo que você precisa para criar o mobile corretamente sem reler o XML inteiro.
|
||||
- Use a seção **"IDs de layout e campo"** para preencher `forceLayoutsRender` e `forceFieldsRender` no `<key>-mobile.xml`.
|
||||
- Use a seção **"Funções JavaScript principais"** para entender o que já existe e evitar duplicar.
|
||||
- Informe ao usuário: _"Li o contexto do painel. Vou criar o mobile com base nele."_
|
||||
- Ao final, **atualize o CONTEXT.md**: marque a versão mobile como "concluída", adicione o caminho do `<key>-mobile.xml` e registre a modificação no histórico.
|
||||
|
||||
**Se o CONTEXT.md não existir:**
|
||||
- Informe ao usuário que não há arquivo de contexto para este painel.
|
||||
- Leia o `<key>-desktop.xml` completo para extrair os IDs necessários.
|
||||
- Ao final, crie o CONTEXT.md completo (veja o formato na Fase 8 da skill **vitruvio-criar-dashboard-desktop**).
|
||||
|
||||
---
|
||||
|
||||
## Princípios obrigatórios — leia antes de qualquer coisa
|
||||
|
||||
### Nunca invente — pergunte quando tiver dúvida
|
||||
|
||||
- Se não conhecer um componente, atributo ou comportamento da plataforma: **pergunte ao usuário** antes de inventar. Uma pergunta custa menos do que um bug em produção.
|
||||
- Baseie-se sempre no que já existe: leia o painel alvo e outros painéis deste repositório para entender o padrão adotado. O que já funciona aqui é a referência.
|
||||
- Não suponha assinaturas de serviços, nomes de atributos ou comportamentos de componentes que não estejam evidentes no código existente.
|
||||
|
||||
### Se o painel tem filtros acima do conteúdo — mova-os para uma sidebar colapsável
|
||||
|
||||
Se o painel alvo tem filtros (combos, campos de texto, datas) posicionados **acima** do conteúdo principal (em linha no topo), **não deixe assim no mobile**. Filtros acima ocupam espaço valioso e poluem a tela.
|
||||
|
||||
O padrão deste repositório é a **sidebar de filtros colapsável** — os dois painéis de referência
|
||||
canônicos estão empacotados junto com a skill desktop, leia-os antes de aplicar o padrão:
|
||||
- `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard-contrato-desktop.xml`
|
||||
- `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard-tecnicos-desktop.xml`
|
||||
|
||||
O painel usa a global `dashLib` (carregada pelo `run()` desktop via `libService.loadScript('dashboard_ia')`)
|
||||
para tema/preferências — veja `.claude/skills/vitruvio-criar-dashboard-desktop/dashboard_ia.js` para
|
||||
a API disponível. **É material de consulta apenas: nunca crie, copie ou registre um
|
||||
`scripts/dashboard_ia.js` no repositório de destino.**
|
||||
|
||||
O padrão é:
|
||||
|
||||
**Layout estrutural (XML)**:
|
||||
```
|
||||
rootLayout (VerticalLayout, 100% x 100%)
|
||||
├── topBar (HorizontalLayout) — botão "◄ Filtros" + navegação + spacer + tema
|
||||
└── mainArea (HorizontalLayout, expandRatio=1)
|
||||
├── filterBar (VerticalLayout, width=220px) — os filtros em coluna
|
||||
└── contentPanel (VerticalLayout, expandRatio=1) — o conteúdo principal
|
||||
```
|
||||
|
||||
**Botão toggle no topBar**:
|
||||
```xml
|
||||
<ButtonWidget id="btnToggleFiltros" caption="◄ Filtros">
|
||||
<onClickScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
var fb = engine.getLayout('filterBar');
|
||||
if (!fb) return;
|
||||
var filterLayout = fb.getRootComposition();
|
||||
var estaVisivel = filterLayout.isVisible();
|
||||
filterLayout.setVisible(!estaVisivel);
|
||||
var btn = engine.getWidgetController('btnToggleFiltros').getButton();
|
||||
btn.setCaption(estaVisivel ? '► Filtros' : '◄ Filtros');
|
||||
}
|
||||
]]>
|
||||
</onClickScript>
|
||||
</ButtonWidget>
|
||||
```
|
||||
|
||||
**No mobile**: o `filterBar` começa invisível (escondido no bloco `_mobileRender` do `run()`). O mobile tem seu próprio botão de filtros dentro da `filterBar` (`btnPreviewFiltros`) que só aparece no mobile — ele aciona o mesmo toggle, mantendo consistência sem adicionar nova lógica.
|
||||
|
||||
**Botão de tema dentro da filterBar (obrigatório no mobile)**: adicione um `ButtonWidget id="btnThemeMob"` no fundo do `filterBar` (após o spacer Label), com `visible="false"`. No bloco `_mobileRender` do `run()`, torne-o visível. Ele deve chamar a mesma função `doToggleTheme` que o `btnTheme` do topBar — defina essa função no ScriptWidget InitScript, registre-a com `engine.setGlobalVariable('doToggleTheme', doToggleTheme)` no `init()`, e tenha ambos os botões chamando-a via `engine.getGlobalVariable('doToggleTheme')`. A função atualiza a caption de AMBOS os botões de tema (`btnTheme` e `btnThemeMob`), o CSS do filterBar e o HTML gerado.
|
||||
|
||||
```xml
|
||||
<!-- dentro do filterBar, após o spacer Label -->
|
||||
<ButtonWidget id="btnThemeMob" caption="☽" description="Alternar tema" visible="false" width="100%">
|
||||
<onClickScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
var fn = engine.getGlobalVariable('doToggleTheme');
|
||||
if (fn) fn();
|
||||
}
|
||||
]]>
|
||||
</onClickScript>
|
||||
</ButtonWidget>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// No bloco _mobileRender do run():
|
||||
var _btnThemeMob = engine.getWidgetController('btnThemeMob');
|
||||
if (_btnThemeMob) { _btnThemeMob.getButton().setVisible(true); }
|
||||
// Oculta btnTheme do topBar — no mobile apenas btnThemeMob é visível
|
||||
var _btnThemeTb = engine.getWidgetController('btnTheme');
|
||||
if (_btnThemeTb) { _btnThemeTb.getButton().setVisible(false); }
|
||||
|
||||
// doToggleTheme no ScriptWidget InitScript — atualiza os dois botões:
|
||||
function doToggleTheme() {
|
||||
var dashLib = engine.getGlobalVariable('dashLib');
|
||||
var login = engine.getGlobalVariable('userLogin');
|
||||
var current = String(engine.getGlobalVariable('theme') || 'dark');
|
||||
var next = current === 'dark' ? 'light' : 'dark';
|
||||
engine.setGlobalVariable('theme', next);
|
||||
try {
|
||||
var ui = Packages.com.vaadin.ui.UI.getCurrent();
|
||||
if (dashLib && ui) { dashLib.applyTheme(next, ui.getPage(), null); }
|
||||
if (dashLib && login) { dashLib.savePreferences(login, { dark_mode: next === 'dark' }); }
|
||||
} catch(e) {}
|
||||
var bT = engine.getWidgetController('btnTheme');
|
||||
if (bT) { bT.getButton().setCaption(next === 'dark' ? '☽' : '☀'); }
|
||||
var bM = engine.getWidgetController('btnThemeMob');
|
||||
if (bM) { bM.getButton().setCaption(next === 'dark' ? '☽' : '☀'); }
|
||||
var _fb = engine.getLayout('filterBar');
|
||||
if (_fb) {
|
||||
var fbComp = _fb.getRootComposition();
|
||||
if (next === 'light') { fbComp.removeStyleName('dash-filterbar-dark'); fbComp.addStyleName('dash-filterbar-light'); }
|
||||
else { fbComp.removeStyleName('dash-filterbar-light'); fbComp.addStyleName('dash-filterbar-dark'); }
|
||||
}
|
||||
try {
|
||||
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
|
||||
"var el=document.getElementById('meu-painel-main');" +
|
||||
"if(el){if('" + next + "'==='light')el.classList.add('theme-light');else el.classList.remove('theme-light');}"
|
||||
);
|
||||
} catch(e2) {}
|
||||
var fn = engine.getGlobalVariable('renderView');
|
||||
if (fn) fn();
|
||||
}
|
||||
|
||||
// No init():
|
||||
engine.setGlobalVariable('doToggleTheme', doToggleTheme);
|
||||
```
|
||||
|
||||
**Os campos de filtro dentro da filterBar são os do painel novo** — não copie os campos do `dashboard-contratos`. Leia o painel alvo, identifique seus filtros originais (cmbAno, cmbMes, txtCliente, etc.) e coloque-os dentro do `filterBar` como `VerticalLayout` com `spacing="true" margin="true"`.
|
||||
|
||||
**`forceFieldsRender` no `<key>-mobile.xml`**: inclua todos os campos de filtro da sidebar. Sem isso, `engine.getField('cmbAno')` falha no WebView mobile.
|
||||
|
||||
**Campo de pesquisa/busca do desktop**: se o painel desktop tem um campo de texto para pesquisa (ex: filtrar por cliente, número, palavra-chave), ele **obrigatoriamente deve aparecer no mobile também**, dentro da filterBar. Nunca omita a pesquisa no mobile — o usuário mobile precisa dela tanto quanto o desktop. Se a pesquisa estiver inline acima da tabela no desktop, mova-a para dentro da `filterBar` no mobile (ou mantenha nos dois lugares).
|
||||
|
||||
---
|
||||
|
||||
### Siga o padrão do repositório
|
||||
|
||||
Este repositório já tem painéis funcionando. Antes de criar algo novo:
|
||||
|
||||
- Leia pelo menos um `<key>-desktop.xml` existente deste repo para entender como estão estruturados os componentes, como são chamados os serviços, como é o estilo visual
|
||||
- Reutilize classes CSS já definidas (ex: `dash-kpi`, `dash-kpi-sm`, `mov-card`, `mobile-topbar`) em vez de criar novas
|
||||
- Reutilize padrões de `run()` / `init()` / `renderHtml()` que já existem no painel alvo
|
||||
|
||||
### Botões e campos padronizados
|
||||
|
||||
- **Botões de ação** (`ButtonWidget`): use `style="DEFAULT"` para ações neutras, `style="GREEN"` para confirmar/salvar, `style="RED"` para cancelar/excluir. Nunca crie botões com HTML customizado quando um `ButtonWidget` padrão serve.
|
||||
- **Campos de filtro**: `ComboBox` para listas fechadas, `TextField` para texto livre, `DateField` para datas. Sempre com `id` único e `caption` em português.
|
||||
- **Ícones**: use os já presentes no painel alvo. Se precisar de novo ícone, use os codepoints já usados no repositório (ex: `📱` para mobile, `✕` para fechar) — não invente.
|
||||
- **Labels de valor monetário**: sempre use a função de formatação já existente no painel (ex: `formatBRL(valor)`) — não crie nova.
|
||||
|
||||
### JavaScript — ES5 Rhino somente
|
||||
|
||||
Nunca use ES6+. Veja o que é proibido e o que usar no lugar:
|
||||
|
||||
```javascript
|
||||
// PROIBIDO
|
||||
const x = 1; // use: var x = 1;
|
||||
let y = 2; // use: var y = 2;
|
||||
() => {}; // use: function() {}
|
||||
`texto ${var}`; // use: 'texto ' + var
|
||||
const { a } = obj; // use: var a = obj.a;
|
||||
class Foo {} // use: function Foo() {}
|
||||
async/await // use: callbacks
|
||||
import/export // não disponível
|
||||
for (const x of arr) // use: for (var i = 0; i < arr.length; i++)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Passo 1 — Confirmar repositório e ler o painel alvo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
Se `NOT_A_VITRUVIO_REPO`, pare e peça para o usuário entrar no diretório correto.
|
||||
|
||||
Leia o `<key>-desktop.xml` alvo **completo** antes de escrever qualquer linha. Leia também pelo menos um outro painel do repositório para entender os padrões já adotados. Só depois prossiga.
|
||||
|
||||
Leia o `<key>-desktop.xml` alvo para entender:
|
||||
- O `formKey` do painel
|
||||
- Os layouts declarados (`id` de cada `VerticalLayout`, `HorizontalLayout`, `Panel`, etc.)
|
||||
- Os campos de filtro (`ComboBox`, `TextField`, etc.)
|
||||
- A estrutura do `initScript` / `run()` / `init()` do ScriptWidget
|
||||
- O que é renderizado em HTML (ScriptWidget que chama `renderHtml`)
|
||||
|
||||
Leia o `vitruvio.json` para identificar a entrada do painel (`key`, `forms`).
|
||||
|
||||
---
|
||||
|
||||
## Passo 2 — Criar o `<key>-mobile.xml`
|
||||
|
||||
Crie `panels/<key>/<key>-mobile.xml`. Ele é um shell que delega ao desktop:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<panel-form xmlns="http://www.davinti.com.br/vitruvio/form/mobile/panel"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form/mobile/panel
|
||||
https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-panel-form.xsd">
|
||||
<form formKey="<key>-mobile">
|
||||
<name><NOME DO PAINEL></name>
|
||||
<description><DESCRIÇÃO></description>
|
||||
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// _mobileRender é setado automaticamente pelo DesktopPanel antes
|
||||
// do painel desktop inicializar — o desktop detecta e adapta o layout.
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
|
||||
<components>
|
||||
<VerticalLayout width="100%" height="100%">
|
||||
<DesktopPanel id="<camelCaseId>" panelKey="<key>"
|
||||
layoutId="rootLayout" external="true"
|
||||
forceLayoutsRender="<lista de layout ids separados por vírgula>"
|
||||
forceFieldsRender="<lista de field ids separados por vírgula>" height="100%" />
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
</panel-form>
|
||||
```
|
||||
|
||||
**`forceLayoutsRender`**: liste todos os layouts do desktop que precisam existir no DOM móvel (ex: `topBar, filterBar, mainArea, contentPanel, panelRoot`). Leia o XML do desktop para identificá-los.
|
||||
|
||||
**`forceFieldsRender`**: liste os campos de filtro que o script usa (ex: `cmbAno, cmbMes, cmbOrdem`). Sem eles, `engine.getField('cmbAno')` falha no mobile.
|
||||
|
||||
**`height="100%"` no `VerticalLayout` e no `DesktopPanel`**: os dois níveis do shell usam `100%` — a altura real vem do container mobile que envolve este `<key>-mobile.xml`. Um valor fixo grande (ex: `3000px`/`700px`) já foi usado aqui para forçar os layouts filhos a calcularem porcentagem, mas isso quebrava a tela (espaço em branco/corte); com `100%` nos dois níveis do shell a altura se propaga corretamente do container pai, sem precisar desse artifício.
|
||||
|
||||
---
|
||||
|
||||
## Passo 3 — Atualizar vitruvio.json
|
||||
|
||||
No `vitruvio.json`, na entrada do painel:
|
||||
- Adicione `"mobile": "panels/<key>/<key>-mobile.xml"` dentro de `"forms"`
|
||||
- Defina `"showInMobileList": true`
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"forms": {
|
||||
"desktop": "panels/<key>/<key>-desktop.xml",
|
||||
"mobile": "panels/<key>/<key>-mobile.xml"
|
||||
},
|
||||
"showInMobileList": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Passo 4 — Adaptar o `<key>-desktop.xml`
|
||||
|
||||
Esta é a parte principal. O desktop precisa detectar o contexto mobile e adaptar o HTML gerado.
|
||||
|
||||
### 4.1 — Bloco de inicialização mobile no `run()`
|
||||
|
||||
Dentro do `run()` (initScript), após o bloco de CSS principal, adicione a detecção mobile:
|
||||
|
||||
```javascript
|
||||
if (engine.isGlobalVariableSet('_mobileRender')) {
|
||||
engine.setGlobalVariable('isMobile', true);
|
||||
var _rootLayout = engine.getLayout('rootLayout');
|
||||
if (_rootLayout) {
|
||||
var _rootLayoutComp = _rootLayout.getRootComposition();
|
||||
// 700px é só o placeholder inicial — o server não sabe a altura da tela do
|
||||
// cliente. O script abaixo troca por um valor proporcional a window.screen.height
|
||||
// assim que a página carrega, ANTES do colapso de renderHtml (ver seção 4.6).
|
||||
_rootLayoutComp.setHeight("700px");
|
||||
_rootLayoutComp.addStyleName('mobile-root-frame');
|
||||
page.getJavaScript().execute(
|
||||
"(function(){" +
|
||||
"var el=document.querySelector('.mobile-root-frame');" +
|
||||
"if(!el){return;}" +
|
||||
"var sh=window.screen.height;" +
|
||||
"var byMargin=sh-220;" +
|
||||
"var byRatio=Math.round(sh*0.80);" +
|
||||
"var h=Math.max(320,Math.min(byMargin,byRatio));" +
|
||||
"el.style.height=h+'px';" +
|
||||
"})();"
|
||||
);
|
||||
}
|
||||
// Aplica estilo de topbar mobile ao topBar se existir
|
||||
var _topBar = engine.getLayout('topBar');
|
||||
if (_topBar) { _topBar.getRootComposition().addStyleName('mobile-topbar'); }
|
||||
// Esconde filterBar no mobile (filtros ficam em outro fluxo ou botão)
|
||||
var _filterBar = engine.getLayout('filterBar');
|
||||
if (_filterBar) { _filterBar.getRootComposition().setVisible(false); }
|
||||
// Exibe btnThemeMob (dentro da filterBar) e oculta btnTheme (topBar)
|
||||
// Sem isso o usuário vê dois botões de tema simultaneamente no mobile
|
||||
var _btnThemeMobR = engine.getWidgetController('btnThemeMob');
|
||||
if (_btnThemeMobR) { _btnThemeMobR.getButton().setVisible(true); }
|
||||
var _btnThemeTbR = engine.getWidgetController('btnTheme');
|
||||
if (_btnThemeTbR) { _btnThemeTbR.getButton().setVisible(false); }
|
||||
// CSS específico mobile adicionado via page.getStyles().add()
|
||||
page.getStyles().add(
|
||||
// Tabelas/grids mobile: altura fixa com scroll interno
|
||||
".mobile-view .mov-scroll { height:520px !important; overflow-y:auto !important;" +
|
||||
" -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } "
|
||||
// Adicione aqui outros CSS mobile específicos deste painel
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **Se o painel alvo tem o assistente de IA "Pietro"** (sidebar `chatBar`, veja a seção opcional
|
||||
> 7.11 da skill **vitruvio-criar-dashboard-desktop**): no mesmo bloco `_mobileRender`, oculte
|
||||
> `btnToggleChat` e mostre `btnChatFabMobile` — mesmo tratamento dado a `btnTheme`/`btnThemeMob`
|
||||
> acima. Replique também em `enterMobilePreview`/`exitMobilePreview` (Passo 5.3). Se o painel não
|
||||
> tiver esses IDs, ignore — é um add-on opcional, não parte obrigatória do padrão.
|
||||
|
||||
### 4.2 — Variável `mobileClass` no HTML gerado
|
||||
|
||||
Em toda função que gera HTML (ex: `desenharDashboard`, `renderView`, etc.), adicione:
|
||||
|
||||
```javascript
|
||||
var mobileClass = (engine.isGlobalVariableSet('isMobile') || engine.isGlobalVariableSet('_mobileRender')) ? ' mobile-view' : '';
|
||||
var html = '<div class="dash-wrapper' + mobileClass + '" id="meu-painel-main">';
|
||||
```
|
||||
|
||||
Isso permite que todo o CSS mobile use seletores `.mobile-view .classe` sem afetar o desktop.
|
||||
|
||||
### 4.3 — Layouts horizontais → cards verticais no mobile
|
||||
|
||||
**Regra de ouro**: no mobile NUNCA coloque duas informações lado a lado. Tudo em coluna única.
|
||||
|
||||
**Desktop** (layout horizontal):
|
||||
```javascript
|
||||
html += '<div style="display:flex; gap:16px;">';
|
||||
html += '<div>R$ 100.000</div>';
|
||||
html += '<div>R$ 50.000</div>';
|
||||
html += '</div>';
|
||||
```
|
||||
|
||||
**Mobile** (detectar e converter para vertical):
|
||||
```javascript
|
||||
if (mobileClass) {
|
||||
html += '<div style="display:flex; flex-direction:column; gap:8px;">';
|
||||
html += '<div class="mob-card"><span class="mob-label">Locação</span><span class="mob-value">R$ 100.000</span></div>';
|
||||
html += '<div class="mob-card"><span class="mob-label">Serviço</span><span class="mob-value">R$ 50.000</span></div>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
// layout desktop original
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 — Tabelas (grids) no mobile → cards empilhados
|
||||
|
||||
No desktop: `<table>` com colunas. No mobile: cada linha vira um card com os campos empilhados:
|
||||
|
||||
```javascript
|
||||
if (mobileClass) {
|
||||
html += '<div class="mov-scroll"><div class="mov-cards">';
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var l = linhas[i];
|
||||
html += '<div class="mov-card">';
|
||||
html += '<div class="mov-card-top">';
|
||||
html += ' <span class="mov-card-cliente">' + l.nome + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div class="mov-card-meta">' + l.codigo + ' · ' + l.tipo + '</div>';
|
||||
html += '<div class="mov-card-values">';
|
||||
html += ' <div class="mov-card-val"><span class="mov-card-lbl">Valor</span><span>' + formatBRL(l.valor) + '</span></div>';
|
||||
html += ' <div class="mov-card-val"><span class="mov-card-lbl">Status</span><span>' + l.status + '</span></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div></div>';
|
||||
} else {
|
||||
// tabela desktop
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 — CSS mobile obrigatório
|
||||
|
||||
No `addCss()` (ou no CSS principal adicionado em `run()`), inclua os seletores mobile:
|
||||
|
||||
> **CRÍTICO:** Todo CSS de scroll mobile **deve estar em `addCss()`** — nunca apenas no bloco `_mobileRender`. Se ficar só em `_mobileRender`, o preview mobile do desenvolvedor não funciona (porque `isMobile=true` mas `_mobileRender` não está setado).
|
||||
|
||||
```javascript
|
||||
// ── Scroll containers no mobile — OBRIGATÓRIO em addCss() ──
|
||||
".mobile-view .mov-scroll { max-height:520px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " +
|
||||
".mobile-view .tec-mob-scroll { max-height:420px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; overscroll-behavior:contain; touch-action:pan-y; } " +
|
||||
".mobile-view .hbar-scroll { max-height:280px !important; overflow-y:auto !important; -webkit-overflow-scrolling:touch !important; } " +
|
||||
|
||||
// Tamanhos de fonte mobile — NUNCA menor que o desktop, sempre maior
|
||||
".mobile-view .dash-kpi-sm .dash-kpi-label { font-size:13px !important; } " +
|
||||
".mobile-view .dash-kpi-sm .dash-kpi-value { font-size:17px !important; } " +
|
||||
|
||||
// Cards de movimentação/dados mobile
|
||||
".mov-card { background:#1a2733; border-radius:6px; padding:12px 16px; margin-bottom:8px; } " +
|
||||
".mov-card-cliente { font-weight:bold; color:#fff; font-size:13px; } " +
|
||||
".mov-card-meta { font-size:11px; color:#8899aa; margin-bottom:8px; } " +
|
||||
".mov-card-values { display:flex; flex-wrap:wrap; gap:6px; } " +
|
||||
".mov-card-val { flex:1; min-width:80px; background:#0f1923; border-radius:4px; padding:6px 8px; } " +
|
||||
".mov-card-lbl { display:block; font-size:10px; color:#8899aa; margin-bottom:2px; } " +
|
||||
|
||||
// Mobile: fontes maiores nos cards
|
||||
".mobile-view .mov-card-cliente { font-size:15px !important; } " +
|
||||
".mobile-view .mov-card-meta { font-size:13px !important; } " +
|
||||
".mobile-view .mov-card-lbl { font-size:12px !important; } " +
|
||||
```
|
||||
|
||||
**Classes de scroll padrão:**
|
||||
|
||||
| Classe | Altura | Uso |
|
||||
|---|---|---|
|
||||
| `.mov-scroll` | 520px | Container da lista de movimentação (cards ou tabela) |
|
||||
| `.tec-mob-scroll` | 420px | Container de lista de cards de técnicos/ranking |
|
||||
| `.hbar-scroll` | max 280px | Container de gráfico de barras horizontais (svgHBar) |
|
||||
|
||||
**Padrão de envolvimento obrigatório no mobile:**
|
||||
|
||||
```javascript
|
||||
// ── Movimentação: barra de pesquisa FORA do scroll, cards DENTRO ──
|
||||
html += '<div class="mob-search-bar">...</div>'; // fica fixo no topo
|
||||
html += '<div class="mov-scroll">'; // scroll interno
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
html += '<div class="mov-card">...</div>';
|
||||
}
|
||||
if (linhas.length === 0) html += '<div style="padding:28px;text-align:center;color:#556677;">Sem dados</div>';
|
||||
html += '</div>'; // fecha mov-scroll
|
||||
|
||||
// ── Lista de técnicos/ranking: header fora, cards dentro ──
|
||||
html += '<div class="dash-chart-box">...header com botões de sort...</div>';
|
||||
html += '<div class="tec-mob-scroll">';
|
||||
html += '<div id="tec-mob-list" data-sc="total" data-sd="-1">';
|
||||
for (var i = 0; i < tecArr.length; i++) {
|
||||
html += '<div class="tec-mob-card" ...>...</div>';
|
||||
}
|
||||
html += '</div>'; // fecha tec-mob-list
|
||||
html += '</div>'; // fecha tec-mob-scroll
|
||||
|
||||
// ── Gráficos svgHBar: sempre dentro de hbar-scroll ──
|
||||
html += '<div class="dash-chart-box">';
|
||||
html += '<div class="dash-chart-title" style="font-size:13px;">Título</div>';
|
||||
html += '<div class="hbar-scroll">' + svgHBar(dados, '#4a9edd') + '</div>';
|
||||
html += '</div>';
|
||||
```
|
||||
|
||||
**Tamanhos mínimos de fonte mobile:**
|
||||
|
||||
| Tipo de dado | Desktop | Mobile mínimo |
|
||||
|---|---|---|
|
||||
| Label/rótulo | 10–12px | 12–13px |
|
||||
| Valor principal | 13–16px | 16–18px |
|
||||
| Título de seção | 14–16px | 18–20px |
|
||||
| Texto de detalhe | 11–12px | 13–14px |
|
||||
|
||||
### 4.6 — renderHtml: tratar ScriptWidget para mobile e preview
|
||||
|
||||
**Padrão real (confirmado nos dois painéis de referência):** `renderHtml` distingue **preview de
|
||||
dev** (`isMobile` setado, `_mobileRender` não) do resto — só o preview usa altura natural via
|
||||
`base.setHeight(null)`; mobile real (`_mobileRender`) e desktop normal sempre usam `setSizeFull()`.
|
||||
Não existe nenhuma técnica de "colapsar um elemento de `700px`" — isso é uma versão obsoleta. Depois
|
||||
do render, o único ajuste feito por JS é liberar `overflow:visible` nos containers pais intermediários
|
||||
até encontrar o `.mobile-root-frame` (cuja altura já foi fixada uma única vez no bloco `_mobileRender`
|
||||
do `run()`, via `window.screen.height` — ver seção 4.1), para que o scroll externo não "arraste" a
|
||||
`topBar` junto:
|
||||
|
||||
```javascript
|
||||
function renderHtml(html) {
|
||||
var comp = components.html(html);
|
||||
// isPreviewMode: preview de dev (isMobile=true SEM _mobileRender). Mobile WebView real
|
||||
// (_mobileRender=true) e desktop normal usam setSizeFull() — só o preview precisa de altura
|
||||
// natural para o Panel (contentPanel) enxergar o overflow real e rolar.
|
||||
var isPreviewMode = engine.isGlobalVariableSet('isMobile') && !engine.isGlobalVariableSet('_mobileRender');
|
||||
if (isPreviewMode) {
|
||||
try { base.setHeight(null); } catch(e) {}
|
||||
comp.setWidth("100%");
|
||||
} else {
|
||||
comp.setSizeFull();
|
||||
}
|
||||
base.removeAllComponents();
|
||||
base.addComponent(comp);
|
||||
if (!isPreviewMode) { base.setExpandRatio(comp, 1.0); }
|
||||
Packages.com.vaadin.ui.UI.getCurrent().getPage().getJavaScript().execute(
|
||||
"setTimeout(function(){" +
|
||||
"if(typeof meuPainelApplyTheme==='function')meuPainelApplyTheme();" +
|
||||
// NÃO redimensionar .mobile-root-frame para caber o conteúdo aqui — sua altura é fixa
|
||||
// (definida uma única vez no _mobileRender via window.screen.height). Só libera
|
||||
// overflow:visible nos pais intermediários para o scroll externo funcionar sem
|
||||
// "arrastar" a topBar junto.
|
||||
"var el=document.getElementById('meu-painel-main');" +
|
||||
"if(el){" +
|
||||
"var p=el,n=0;" +
|
||||
"while(p&&n<30){p=p.parentElement;n++;" +
|
||||
"if(!p){break;}" +
|
||||
"if(p.classList&&p.classList.contains('mobile-root-frame')){break;}" +
|
||||
"var cs=window.getComputedStyle(p);" +
|
||||
"if(cs&&(cs.overflowY==='auto'||cs.overflowY==='scroll'||cs.overflowY==='hidden')){p.style.overflowY='visible';}" +
|
||||
"if(cs&&(cs.overflow==='auto'||cs.overflow==='scroll'||cs.overflow==='hidden')){p.style.overflow='visible';}" +
|
||||
"}" +
|
||||
"}" +
|
||||
"},400);"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Passo 5 — Sistema de Preview Mobile (para grupo vi_developer)
|
||||
|
||||
Adicione ao `<key>-desktop.xml` um botão de preview que simula o mobile diretamente no desktop. Isso permite testar sem abrir o WebView mobile.
|
||||
|
||||
### 5.1 — Botão no topBar (XML)
|
||||
|
||||
```xml
|
||||
<ButtonWidget id="btnDevPreview" caption="📱"
|
||||
description="Preview Mobile (apenas desenvolvedores)" visible="false">
|
||||
<onClickScript>
|
||||
function run() { var fn = engine.getGlobalVariable('enterMobilePreview'); if (fn) fn(); }
|
||||
</onClickScript>
|
||||
</ButtonWidget>
|
||||
<ButtonWidget id="btnExitPreview" caption="✕ Sair Preview"
|
||||
description="Voltar ao modo desktop" visible="false">
|
||||
<onClickScript>
|
||||
function run() { var fn = engine.getGlobalVariable('exitMobilePreview'); if (fn) fn(); }
|
||||
</onClickScript>
|
||||
</ButtonWidget>
|
||||
```
|
||||
|
||||
### 5.2 — CSS do preview
|
||||
|
||||
Adicione ao CSS principal:
|
||||
|
||||
```javascript
|
||||
".preview-mobile-frame { margin:0 auto !important; border:2px solid #4a9edd !important;" +
|
||||
" border-radius:12px !important; box-shadow:0 0 40px rgba(74,158,221,0.25) !important; } " +
|
||||
".preview-exit-btn { position:fixed !important; top:10px !important; right:12px !important;" +
|
||||
" z-index:99999 !important; background:#e74c3c !important; border:none !important;" +
|
||||
" border-radius:6px !important; padding:7px 16px !important; font-weight:bold !important;" +
|
||||
" box-shadow:0 2px 12px rgba(0,0,0,0.4) !important; cursor:pointer !important; } " +
|
||||
".preview-exit-btn .v-button-caption { color:#fff !important; font-size:12px !important; } "
|
||||
```
|
||||
|
||||
### 5.3 — Funções enterMobilePreview / exitMobilePreview
|
||||
|
||||
**Padrão real (confirmado nos dois painéis de referência):** o preview usa altura **natural**, não
|
||||
fixa — `base.setHeight(null)` faz o `ScriptWidget` crescer ao tamanho do conteúdo, e o `Panel`
|
||||
(`contentPanel`) que o envolve enxerga o overflow real e rola sozinho. Não há valor fixo (nem
|
||||
`700px` nem `3000px`) nem colapso via JS aqui — essa técnica é só para o `_mobileRender` real
|
||||
(placeholder `700px` recalculado por `window.screen.height`, seção 4.1), que existe porque ali sim
|
||||
há uma tela de celular real para medir. No preview, que roda no navegador desktop do dev, altura
|
||||
natural é suficiente e mais simples.
|
||||
|
||||
Adicione no ScriptWidget, antes do `init()`:
|
||||
|
||||
```javascript
|
||||
function enterMobilePreview() {
|
||||
engine.setGlobalVariable('isMobile', true);
|
||||
// base com height null → cresce ao tamanho do conteúdo → Panel enxerga overflow e rola
|
||||
try { base.setHeight(null); } catch(e) {}
|
||||
var _rl = engine.getLayout('rootLayout');
|
||||
if (_rl) {
|
||||
var _rlComp = _rl.getRootComposition();
|
||||
_rlComp.setWidth("375px");
|
||||
_rlComp.addStyleName('preview-mobile-frame');
|
||||
}
|
||||
var _tBar = engine.getLayout('topBar');
|
||||
if (_tBar) _tBar.getRootComposition().addStyleName('mobile-topbar');
|
||||
// Salva e esconde filterBar
|
||||
var _fBar = engine.getLayout('filterBar');
|
||||
if (_fBar) {
|
||||
var _fBarComp = _fBar.getRootComposition();
|
||||
engine.setGlobalVariable('_prevFilterBarVisible', _fBarComp.isVisible());
|
||||
_fBarComp.setVisible(false);
|
||||
}
|
||||
var _btnDev = engine.getWidgetController('btnDevPreview');
|
||||
if (_btnDev) _btnDev.getButton().setVisible(false);
|
||||
var _btnExit = engine.getWidgetController('btnExitPreview');
|
||||
if (_btnExit) {
|
||||
_btnExit.getButton().setVisible(true);
|
||||
_btnExit.getButton().addStyleName('preview-exit-btn');
|
||||
}
|
||||
// Exibe btnThemeMob e oculta btnTheme — igual ao _mobileRender real
|
||||
var _btnThMob = engine.getWidgetController('btnThemeMob');
|
||||
if (_btnThMob) _btnThMob.getButton().setVisible(true);
|
||||
var _btnThTb = engine.getWidgetController('btnTheme');
|
||||
if (_btnThTb) _btnThTb.getButton().setVisible(false);
|
||||
renderView(); // re-renderiza com mobile-view ativo
|
||||
}
|
||||
|
||||
function exitMobilePreview() {
|
||||
engine.unsetGlobalVariable('isMobile');
|
||||
// Restaura base para height 100% (modo desktop normal)
|
||||
try { base.setHeight("100%"); } catch(e) {}
|
||||
var _rl = engine.getLayout('rootLayout');
|
||||
if (_rl) {
|
||||
var _rlComp = _rl.getRootComposition();
|
||||
_rlComp.setWidth("100%");
|
||||
_rlComp.removeStyleName('preview-mobile-frame');
|
||||
}
|
||||
var _tBar = engine.getLayout('topBar');
|
||||
if (_tBar) _tBar.getRootComposition().removeStyleName('mobile-topbar');
|
||||
// Restaura filterBar — usa loose equality (== ) para coerção de Java Boolean
|
||||
var _fBar = engine.getLayout('filterBar');
|
||||
if (_fBar) {
|
||||
var _wasVisible = engine.getGlobalVariable('_prevFilterBarVisible');
|
||||
_fBar.getRootComposition().setVisible(_wasVisible == true);
|
||||
engine.unsetGlobalVariable('_prevFilterBarVisible');
|
||||
}
|
||||
var _btnExit = engine.getWidgetController('btnExitPreview');
|
||||
if (_btnExit) {
|
||||
_btnExit.getButton().removeStyleName('preview-exit-btn');
|
||||
_btnExit.getButton().setVisible(false);
|
||||
}
|
||||
// Restaura btnTheme do topBar e oculta btnThemeMob ao sair do preview
|
||||
var _btnThMobExit = engine.getWidgetController('btnThemeMob');
|
||||
if (_btnThMobExit) _btnThMobExit.getButton().setVisible(false);
|
||||
var _btnThTbExit = engine.getWidgetController('btnTheme');
|
||||
if (_btnThTbExit) _btnThTbExit.getButton().setVisible(true);
|
||||
// Reexibe botão preview se o usuário for developer
|
||||
var _isDev = engine.getGlobalVariable('isDeveloper');
|
||||
var _btnDev = engine.getWidgetController('btnDevPreview');
|
||||
if (_btnDev) _btnDev.getButton().setVisible(!!_isDev);
|
||||
renderView();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 — Checar grupo vi_developer e exibir botão no init()
|
||||
|
||||
No `init(mapa)` ou no `run()`, após a inicialização principal:
|
||||
|
||||
```javascript
|
||||
// Verifica acesso de desenvolvedor (vi_developer)
|
||||
try {
|
||||
var _dbLib = libService.loadScript('db');
|
||||
var _banco = new _dbLib(_dbLib.VITRUVIO_DATASOURCE);
|
||||
var _devRow = _banco.queryRow(
|
||||
"SELECT COUNT(*) AS CNT FROM nauth.usuario u " +
|
||||
"INNER JOIN nauth.usuario_grupo ug ON u.usuario_id = ug.usuario_fk " +
|
||||
"INNER JOIN nauth.grupo g ON ug.grupo_fk = g.grupo_id " +
|
||||
"WHERE u.login = '" + engine.getLoggedUser().getLogin() + "' " +
|
||||
"AND g.sigla = 'vi_developer'"
|
||||
);
|
||||
if (_devRow && String(_devRow.CNT) !== '0') {
|
||||
engine.setGlobalVariable('isDeveloper', true);
|
||||
var _btnDev = engine.getWidgetController('btnDevPreview');
|
||||
if (_btnDev) _btnDev.getButton().setVisible(true);
|
||||
}
|
||||
} catch(e) {}
|
||||
// Registra funções de preview como variáveis globais (acessíveis pelo XML dos botões)
|
||||
engine.setGlobalVariable('enterMobilePreview', enterMobilePreview);
|
||||
engine.setGlobalVariable('exitMobilePreview', exitMobilePreview);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Passo 6 — Armadilhas conhecidas (não repita estes erros)
|
||||
|
||||
### Java Boolean vs JS boolean
|
||||
`engine.getGlobalVariable()` retorna `Java Boolean`, não JS primitive:
|
||||
```javascript
|
||||
// ERRADO — Java Boolean.FALSE !== JS false (tipos diferentes, strict equality falha)
|
||||
if (_wasVisible !== false) { ... }
|
||||
|
||||
// CERTO — loose equality funciona com Java Boolean
|
||||
if (_wasVisible == true) { ... }
|
||||
// OU
|
||||
if (!!_wasVisible) { ... }
|
||||
```
|
||||
|
||||
### setExpandRatio antes de addComponent
|
||||
```javascript
|
||||
// ERRADO — comp não é filho de base ainda, Vaadin lança IllegalArgumentException
|
||||
comp.setSizeFull();
|
||||
base.setExpandRatio(comp, 1.0); // ← ERRO: comp não está em base
|
||||
base.addComponent(comp);
|
||||
|
||||
// CERTO — setExpandRatio sempre APÓS addComponent
|
||||
comp.setSizeFull();
|
||||
base.removeAllComponents();
|
||||
base.addComponent(comp);
|
||||
base.setExpandRatio(comp, 1.0); // ← OK: comp já é filho
|
||||
```
|
||||
|
||||
### base.setHeight(null) no preview — por que funciona
|
||||
`enterMobilePreview` põe `base` (o container do `ScriptWidget`) em altura natural com `base.setHeight(null)` — sem isso, o container mantém a altura da viewport inteira e o `Panel` (`contentPanel`) não enxerga o conteúdo real para rolar. No `_mobileRender` real esse mesmo `base.setHeight(null)` **não** é usado — lá o `renderHtml` sempre chama `comp.setSizeFull()`, porque quem controla a altura do frame é o cálculo de `window.screen.height` no `rootLayout` (seção 4.1), não o `base`.
|
||||
|
||||
### inline style sobrescreve class CSS
|
||||
Se o HTML gerado tem `style="font-size:12px"` no elemento, CSS de classe não sobrescreve (exceto com `!important`). Mude o valor diretamente na geração do HTML para contexto mobile:
|
||||
```javascript
|
||||
var fontSize = mobileClass ? '14px' : '12px';
|
||||
html += '<div style="font-size:' + fontSize + ';color:#8899aa;">Texto</div>';
|
||||
```
|
||||
|
||||
### Redimensionar o frame mobile a cada render — não faça isso
|
||||
Uma versão antiga recalculava a altura do `.mobile-root-frame` para `el.getBoundingClientRect().bottom` a cada `renderHtml`. Isso fazia o frame crescer para caber TODO o conteúdo (não só o viewport), transformando `topBar` + conteúdo num único bloco comprido que a página externa rolava inteiro — "arrastando" a `topBar` junto. A altura do `.mobile-root-frame` é definida **uma única vez** no bloco `_mobileRender` do `run()` (via `window.screen.height`, seção 4.1) e deve permanecer fixa; `renderHtml` só libera `overflow:visible` nos pais intermediários (seção 4.6), nunca redimensiona o frame em si.
|
||||
|
||||
---
|
||||
|
||||
## Passo 7 — Checklist de entrega
|
||||
|
||||
Antes de declarar pronto, confirme:
|
||||
|
||||
- [ ] Leu o `<key>-desktop.xml` alvo e ao menos um outro painel do repositório antes de escrever qualquer coisa
|
||||
- [ ] Tudo que foi feito tem referência no código existente do repositório — nada inventado
|
||||
- [ ] Não usou nenhuma feature ES6+ (sem `const`, `let`, arrow functions, template literals, destructuring, `class`, `async/await`)
|
||||
- [ ] Botões usam `style` padrão (`DEFAULT`, `GREEN`, `RED`) — sem HTML customizado para botões
|
||||
- [ ] Reutilizou funções de formatação já existentes no painel (ex: `formatBRL`) — sem duplicar
|
||||
- [ ] Reutilizou classes CSS já existentes — sem criar novas desnecessariamente
|
||||
- [ ] `<key>-mobile.xml` criado com `DesktopPanel` apontando para o `panelKey` correto
|
||||
- [ ] `forceLayoutsRender` lista todos os layouts usados pelo desktop
|
||||
- [ ] `forceFieldsRender` lista todos os campos de filtro acessados via `engine.getField()`
|
||||
- [ ] `vitruvio.json`: `"mobile"` adicionado em `"forms"` e `"showInMobileList": true`
|
||||
- [ ] `run()` do desktop detecta `_mobileRender` e adapta rootLayout + filterBar + CSS
|
||||
- [ ] No `_mobileRender` real: `rootLayout` recebe `setHeight("700px")` + `addStyleName('mobile-root-frame')`, seguido de `page.getJavaScript().execute(...)` calculando a altura final a partir de `window.screen.height` (clamp entre 320px e `min(screen.height-220, screen.height*0.80)`) — o `700px` é só o placeholder inicial, não a altura final do mobile real
|
||||
- [ ] HTML gerado usa `mobileClass` e layouts verticais no mobile
|
||||
- [ ] Tabelas/grids do desktop viram cards empilhados no mobile
|
||||
- [ ] Fontes mobile respeitam os tamanhos mínimos (labels ≥ 12px, valores ≥ 16px)
|
||||
- [ ] `renderHtml` distingue preview (`isMobile` sem `_mobileRender`) de mobile real/desktop: só o preview usa `base.setHeight(null)` + `comp.setWidth("100%")`; os outros dois usam `comp.setSizeFull()` + `base.setExpandRatio(comp, 1.0)`
|
||||
- [ ] `enterMobilePreview` usa `base.setHeight(null)` (altura natural) — não copia o `700px` fixo do `_mobileRender` real, que só faz sentido para tela de celular
|
||||
- [ ] `exitMobilePreview` restaura `base.setHeight("100%")`
|
||||
- [ ] JS pós-render (setTimeout 400ms mínimo) só libera `overflow:visible` nos pais até `.mobile-root-frame` — nunca redimensiona o frame em si
|
||||
- [ ] `setExpandRatio` é chamado APÓS `addComponent`
|
||||
- [ ] Funções `enterMobilePreview` / `exitMobilePreview` registradas como global vars
|
||||
- [ ] Botão `btnDevPreview` visível somente para `vi_developer`
|
||||
- [ ] `btnThemeMob` adicionado como **primeiro filho** do `filterBar`, `visible="false"` no XML, visível no bloco `_mobileRender`
|
||||
- [ ] No bloco `_mobileRender` e em `enterMobilePreview`: `btnTheme` (topBar) oculto via `setVisible(false)` — evita dois botões de tema simultâneos
|
||||
- [ ] No `exitMobilePreview`: `btnThemeMob` oculto e `btnTheme` restaurado via `setVisible(true)`
|
||||
- [ ] `doToggleTheme` definido no ScriptWidget, registrado no `init()`, chamado por `btnTheme` e `btnThemeMob`
|
||||
- [ ] `doToggleTheme` atualiza caption de ambos os botões de tema
|
||||
- [ ] Comparação de Java Boolean usa `==` (loose equality), nunca `===`
|
||||
- [ ] CSS de scroll mobile (`.mov-scroll`, `.tec-mob-scroll`, `.hbar-scroll`) está em `addCss()` — **não apenas** no bloco `_mobileRender`
|
||||
- [ ] Lista de cards de movimentação mobile envolvida em `<div class="mov-scroll">`
|
||||
- [ ] Lista de técnicos/ranking mobile envolvida em `<div class="tec-mob-scroll">`
|
||||
- [ ] Gráficos svgHBar no mobile envolvidos em `<div class="hbar-scroll">`
|
||||
|
||||
---
|
||||
|
||||
## Referência rápida de classes CSS mobile
|
||||
|
||||
| Classe | Uso |
|
||||
|---|---|
|
||||
| `.mobile-view` | Aplicada ao `id` raiz do HTML gerado quando em modo mobile |
|
||||
| `.mobile-topbar` | Estilo do topBar no mobile (botões menores, compactos) |
|
||||
| `.preview-mobile-frame` | Borda azul que indica o frame de preview 375px |
|
||||
| `.preview-exit-btn` | Botão "✕ Sair Preview" flutuante fixed no canto superior direito |
|
||||
| `.mov-scroll` | Container da lista de movimentação (cresce com o conteúdo, scroll acima de 520px) |
|
||||
| `.tec-mob-scroll` | Container de lista de técnicos/ranking (cresce com o conteúdo, scroll acima de 420px) |
|
||||
| `.hbar-scroll` | Container de gráfico svgHBar (max 280px + scroll) |
|
||||
| `.mov-cards` | Container de cards empilhados (substitui a table no mobile) |
|
||||
| `.mov-card` | Card individual de cada linha da tabela |
|
||||
| `.mov-card-cliente` | Nome/título principal do card (font-size ≥ 15px mobile) |
|
||||
| `.mov-card-meta` | Informações secundárias (contrato, tipo, período) |
|
||||
| `.mov-card-values` | Flex wrap com os valores numéricos do card |
|
||||
| `.mov-card-val` | Célula individual de valor dentro do card |
|
||||
| `.mov-card-lbl` | Rótulo do valor (font-size ≥ 12px mobile) |
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<panel-form xmlns="http://www.davinti.com.br/vitruvio/form/mobile/panel"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form/mobile/panel
|
||||
https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-panel-form.xsd">
|
||||
<form formKey="dashboard-contratos-mobile">
|
||||
<name>Dashboard de Contratos</name>
|
||||
<description>Dashboard de Contratos</description>
|
||||
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// _mobileRender é setado automaticamente pelo DesktopPanel antes
|
||||
// do painel desktop inicializar — o desktop detecta e adapta o layout.
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
|
||||
<components>
|
||||
<VerticalLayout width="100%" height="100%">
|
||||
<DesktopPanel id="dashContratos" panelKey="dashboard-contratos"
|
||||
layoutId="rootLayout" external="true" height="100%"
|
||||
forceLayoutsRender="topBar, filterBar, mainArea, contentPanel, panelRoot"
|
||||
forceFieldsRender="cmbAno, cmbMes, cmbOrdem" />
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
</panel-form>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<panel-form xmlns="http://www.davinti.com.br/vitruvio/form/mobile/panel"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form/mobile/panel
|
||||
https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-panel-form.xsd">
|
||||
<form formKey="dashboard-tecnicos-mobile">
|
||||
<name>Dashboard Técnicos</name>
|
||||
<description>Dashboard de produtividade dos técnicos com ranking, SLA e detalhamento por OS.</description>
|
||||
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// _mobileRender é setado automaticamente pelo DesktopPanel antes
|
||||
// do painel desktop inicializar — o desktop detecta e adapta o layout.
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
|
||||
<components>
|
||||
<VerticalLayout width="100%" height="100%">
|
||||
<DesktopPanel id="dashTecnicosMobile" panelKey="dashboard_tecnicos"
|
||||
layoutId="rootLayout" external="true" height="100%"
|
||||
forceLayoutsRender="topBar, mainArea, filterBar, contentPanel, panelRoot"
|
||||
forceFieldsRender="dtIni, dtFim, nmfSla, cmbExecucao" />
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
</panel-form>
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Nome: Parâmetro de indicadores
|
||||
* Sigla: dashboard_ia
|
||||
* Descrição: Biblioteca compartilhada dos dashboards de indicadores da Diretoria: preferências de usuário (tema), CSS do topbar e toolbar, helpers de tema.
|
||||
*/
|
||||
(function() {
|
||||
var PREF_NS = 'preferences';
|
||||
|
||||
function loadPreferences(login) {
|
||||
try {
|
||||
var raw = vConfigService.getUserConfigAsString(String(login), PREF_NS);
|
||||
if (raw != null && String(raw) !== '') {
|
||||
return JSON.parse(String(raw));
|
||||
}
|
||||
} catch (e) {}
|
||||
return { dark_mode: true };
|
||||
}
|
||||
|
||||
function savePreferences(login, prefs) {
|
||||
try {
|
||||
vConfigService.saveUserConfig(String(login), PREF_NS, JSON.stringify(prefs));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function getTheme(engine) {
|
||||
return String(engine.getGlobalVariable('theme') || 'dark');
|
||||
}
|
||||
|
||||
function isLight(engine) {
|
||||
return getTheme(engine) === 'light';
|
||||
}
|
||||
|
||||
function applyTheme(themeName, page, btnTheme) {
|
||||
page.getJavaScript().execute(
|
||||
themeName === 'light'
|
||||
? "document.body.classList.add('theme-light-app');"
|
||||
: "document.body.classList.remove('theme-light-app');"
|
||||
);
|
||||
if (btnTheme != null) {
|
||||
btnTheme.setCaption(themeName === 'dark' ? '☽' : '☀');
|
||||
}
|
||||
}
|
||||
|
||||
function addSharedCss(page) {
|
||||
var style =
|
||||
".dash-topbar { background:#0f1923 !important; border-bottom:1px solid #243447; } " +
|
||||
".dash-topbar .v-button { background:transparent !important; border:1px solid transparent !important; color:#556677 !important; border-radius:4px !important; transition:none !important; } " +
|
||||
".dash-topbar .v-button .v-button-caption { color:#556677 !important; } " +
|
||||
".dash-topbar .v-button.dash-nav-active { background:#1a2733 !important; border-color:#4a9edd !important; } " +
|
||||
".dash-topbar .v-button.dash-nav-active .v-button-caption { color:#4a9edd !important; font-weight:bold !important; } " +
|
||||
".dash-topbar .v-caption { color:#8899aa !important; font-size:10px !important; } " +
|
||||
".dash-topbar input.v-textfield, .dash-topbar .v-datefield-textfield, .dash-topbar input.v-filterselect-input { background:#1a2733 !important; color:#ccc !important; border-color:#243447 !important; } " +
|
||||
".dash-topbar .v-datefield-button { background:#1a2733 !important; border-color:#243447 !important; } " +
|
||||
".dash-topbar .v-select-select { background:#1a2733 !important; color:#ccc !important; border-color:#243447 !important; } " +
|
||||
"body.theme-light-app .dash-topbar { background:#f8fafc !important; border-bottom:1px solid #e2e8f0; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button { color:#475569 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button .v-button-caption { color:#475569 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button.dash-nav-active { background:#dbeafe !important; border-color:#2563eb !important; color:#2563eb !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-button.dash-nav-active .v-button-caption { color:#2563eb !important; } " +
|
||||
"body.theme-light-app .dash-topbar input.v-textfield, body.theme-light-app .dash-topbar .v-datefield-textfield, body.theme-light-app .dash-topbar input.v-filterselect-input { background:#ffffff !important; color:#334155 !important; border-color:#e2e8f0 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-datefield-button { background:#ffffff !important; border-color:#e2e8f0 !important; } " +
|
||||
"body.theme-light-app .dash-topbar .v-select-select { background:#ffffff !important; color:#334155 !important; border-color:#e2e8f0 !important; } " +
|
||||
".theme-light.dash-wrapper { background:#f0f2f5; } " +
|
||||
".theme-light .dash-kpi { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " +
|
||||
".theme-light .dash-kpi-label { color:#64748b; } " +
|
||||
".theme-light .dash-kpi-value { color:#1e293b; } " +
|
||||
".theme-light .dash-chart-box { background:#ffffff; box-shadow:0 1px 3px rgba(0,0,0,.08); } " +
|
||||
".theme-light .dash-chart-title { color:#64748b; } " +
|
||||
".mov-toolbar { display:flex; align-items:center; gap:8px; margin-bottom:8px; } " +
|
||||
".mov-filter-input { background:#1a2733; border:1px solid #243447; color:#ccc; padding:5px 10px; border-radius:4px; font-size:11px; flex:1; outline:none; font-family:Arial; } " +
|
||||
".mov-filter-input:focus { border-color:#4a9edd; } " +
|
||||
".mov-export-btn { background:#1a2733; border:1px solid #243447; color:#8899aa; padding:5px 12px; border-radius:4px; font-size:11px; cursor:pointer; white-space:nowrap; font-family:Arial; } " +
|
||||
".mov-export-btn:hover { border-color:#4a9edd; color:#4a9edd; } " +
|
||||
".theme-light .mov-filter-input { background:#ffffff; border-color:#e2e8f0; color:#334155; } " +
|
||||
".theme-light .mov-filter-input:focus { border-color:#2563eb; } " +
|
||||
".theme-light .mov-export-btn { background:#ffffff; border-color:#e2e8f0; color:#64748b; } " +
|
||||
".theme-light .mov-export-btn:hover { border-color:#2563eb; color:#2563eb; } ";
|
||||
|
||||
page.getStyles().add(style);
|
||||
}
|
||||
|
||||
return {
|
||||
loadPreferences: loadPreferences,
|
||||
savePreferences: savePreferences,
|
||||
getTheme: getTheme,
|
||||
isLight: isLight,
|
||||
applyTheme: applyTheme,
|
||||
addSharedCss: addSharedCss
|
||||
};
|
||||
})()
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: vitruvio-criar-endpoint
|
||||
description: >
|
||||
Use when the user wants to create a new REST endpoint in a Vitruvio repository.
|
||||
Triggers: "create endpoint", "new endpoint", "criar endpoint", "novo endpoint", "add endpoint",
|
||||
"REST", "WebService", "integração", or any request to scaffold an endpoints/*.js file.
|
||||
---
|
||||
|
||||
# Create Vitruvio Endpoint
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating a new REST endpoint inside a Vitruvio repository. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect endpoint details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing from their original request):
|
||||
|
||||
- **Key** — kebab-case unique identifier. Forms the URL path. Changing it later breaks external integrations.
|
||||
- **Name** — human-readable label shown in the Vitruvio UI.
|
||||
- **Description** — one sentence about what this endpoint does.
|
||||
- **HTTP verbs** — which methods to implement: GET, POST, PUT, PATCH, DELETE. Only scaffold the ones actually needed.
|
||||
- **Auth mode** — one of:
|
||||
- `PUBLIC` — no authentication (default)
|
||||
- `STATIC_TOKEN` — token in query param `_x_token_auth` or header `X-WS-TOKEN-AUTH`
|
||||
- `VITRUVIO_WS_USER_AUTH` — Vitruvio bearer token; `loggedUser` is available in the script
|
||||
- `HTTP_BASIC_AUTH` — HTTP basic auth; `loggedUser` is available; roles validated by Vitruvio admin
|
||||
|
||||
## Step 3 — Scaffold and create file
|
||||
|
||||
```bash
|
||||
vitruvio new endpoint <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates `endpoints/<key>.js` and registers it in `vitruvio.json` with `authMode: "PUBLIC"` and `active: true`. Replace the generated file with the template below. If authMode is not PUBLIC, update that field in `vitruvio.json`.
|
||||
|
||||
Target path: `endpoints/<key>.js`
|
||||
|
||||
URL after deploy:
|
||||
|
||||
| authMode | URL |
|
||||
|---|---|
|
||||
| `PUBLIC` | `/api/integration/public/<key>` |
|
||||
| `STATIC_TOKEN` | `/api/integration/tokenauth/<key>` |
|
||||
| `VITRUVIO_WS_USER_AUTH` | `/api/integration/bearerauth/<key>` |
|
||||
| `HTTP_BASIC_AUTH` | `/api/integration/bauth/<key>` |
|
||||
|
||||
Template (include only the requested verbs):
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Nome: <name>
|
||||
* Sigla: <key>
|
||||
* Descrição: <description>
|
||||
* Auth: <authMode>
|
||||
*/
|
||||
function WebService() {
|
||||
|
||||
// this.onGet = function(params) { ... } ← GET / DELETE: params has .headers and .query
|
||||
// this.onPost = function(params) { ... } ← POST / PUT / PATCH: params also has .requestBody (string, always JSON.parse before use)
|
||||
|
||||
this.onPost = function(params) {
|
||||
try {
|
||||
var body = JSON.parse(params.requestBody);
|
||||
if (!body.id) throw 'Missing required field: id';
|
||||
|
||||
// implementation here
|
||||
|
||||
return JSON.stringify({ success: true });
|
||||
} catch (e) {
|
||||
return JSON.stringify({ error: e.toString() });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
module.exports = new WebService();
|
||||
```
|
||||
|
||||
Rules (Rhino ES5 — no exceptions):
|
||||
- No `let`, `const`, arrow functions, template literals, destructuring, spread, `class`, or `import/export`
|
||||
- Use `var` everywhere
|
||||
- Always `JSON.parse(params.requestBody)` before accessing the body — never trust it raw
|
||||
- Always return strings — `JSON.stringify(obj)`, not raw objects
|
||||
- Return `null` or nothing for `204 No Content`; return a string for `200 OK`
|
||||
- Remove unused verb stubs entirely — don't leave placeholder bodies
|
||||
- Never concatenate user input into SQL strings; use named bind params (`:paramName`)
|
||||
- Don't hardcode datasource names or tokens — read from `vConfigService` or a DB config table
|
||||
- Put heavy logic in a separate script loaded via `libService.loadScript`, not inline in the endpoint
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the endpoint entry. The full entry shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"language": "javascript",
|
||||
"authMode": "PUBLIC",
|
||||
"active": true,
|
||||
"source": "endpoints/<key>.js"
|
||||
}
|
||||
```
|
||||
|
||||
Update only what differs: set `"authMode"` to the correct value if not `"PUBLIC"`; add `"description"` if provided.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created: `endpoints/<key>.js`
|
||||
- Registered in `vitruvio.json` with key `<key>`
|
||||
- URL once deployed (based on authMode)
|
||||
- Which verbs were scaffolded
|
||||
@@ -0,0 +1,290 @@
|
||||
---
|
||||
name: vitruvio-criar-form-desktop
|
||||
description: >
|
||||
Use when the user wants to create or edit the DESKTOP (web) XML form of a Vitruvio panel
|
||||
or process — the Vaadin form rendered on desktop. Triggers: "create desktop form",
|
||||
"criar formulário desktop", "form xml", "desktop form xml", "tela desktop",
|
||||
"add a field to the desktop form", "process desktop form". This is the single home for
|
||||
desktop form knowledge; vitruvio-criar-painel and vitruvio-criar-processo call it for
|
||||
their form part. For the mobile form use vitruvio-criar-form-mobile.
|
||||
---
|
||||
|
||||
# Create Vitruvio Desktop Form
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
Desktop forms are Vaadin 8 forms defined in XML and rendered by the Vitruvio engine. This
|
||||
skill creates the **desktop** form. There are two variants that share almost all of their
|
||||
component vocabulary but differ in their root element and how variables flow — see
|
||||
**Differences: panel vs process** below.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Determine variant and target
|
||||
|
||||
Ask (only what is missing):
|
||||
|
||||
- **Panel or process form?** (decides root element / file location — see below)
|
||||
- **Key** — the panel or process key (the folder name).
|
||||
- For a **process**, which `formKey`(s) are needed — they must match the `activiti:formKey`
|
||||
values in `processes/<key>/<key>.bpmn`.
|
||||
- **What should the form show/do?** — fields and behaviour, so you can scaffold something useful.
|
||||
|
||||
## Differences: panel vs process
|
||||
|
||||
| | Panel | Process |
|
||||
|----------------|-------|---------|
|
||||
| File | `panels/<key>/<key>-desktop.xml` | `processes/<key>/<key>-desktop.xml` |
|
||||
| Root element | `<panel-form>` | `<forms>` (with `processKey` attribute optional) |
|
||||
| Namespace | `http://www.davinti.com.br/vitruvio/form/panel` | `http://www.davinti.com.br/vitruvio/form` |
|
||||
| XSD | `vitruvio-panel-form.xsd` | `vitruvio-form.xsd` |
|
||||
| Forms per file | exactly one `<form>` | **one `<form formKey>` per BPMN `activiti:formKey`** |
|
||||
| Variables | none built-in; use `engine.getGlobalVariable` | process variables: `engine.getVariable`/`setVariable`; submitted field `id="X"` in `formKey="A"` → variable `A_X` |
|
||||
| `<library>` | not used | optional: shared `<complex-component id>` reused via `<component-ref refId>` |
|
||||
|
||||
Everything below (components, DBTable, engine API, ES5 rules) is **identical** for both.
|
||||
|
||||
## Step 3 — Scaffold the file
|
||||
|
||||
### Panel variant — `panels/<key>/<key>-desktop.xml`
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<panel-form xmlns="http://www.davinti.com.br/vitruvio/form/panel"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form/panel https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-panel-form.xsd">
|
||||
|
||||
<form formKey="<key>" width="100%" height="100%">
|
||||
<name><name></name>
|
||||
<description><description></description>
|
||||
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// called once when the panel opens
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
|
||||
<components>
|
||||
<VerticalLayout spacing="true" margin="true" width="100%" height="100%">
|
||||
<!-- add widgets here -->
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
</panel-form>
|
||||
```
|
||||
|
||||
### Process variant — `processes/<key>/<key>-desktop.xml`
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<forms xmlns="http://www.davinti.com.br/vitruvio/form"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/form https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-form.xsd">
|
||||
|
||||
<!-- Optional: shared components used across multiple forms -->
|
||||
<library>
|
||||
<!-- <complex-component id="libShared">...</complex-component> -->
|
||||
</library>
|
||||
|
||||
<!-- One <form> per activiti:formKey in the BPMN -->
|
||||
<form formKey="formAbertura" width="100%">
|
||||
<name>Abertura</name>
|
||||
<description>Abertura do processo</description>
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// called when the form opens
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
<components>
|
||||
<VerticalLayout spacing="true" margin="true" width="100%">
|
||||
<TextField id="descricao" type="string" caption="Descrição" width="100%" required="true" />
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
|
||||
<form formKey="formExecutar" width="100%">
|
||||
<name>Executar</name>
|
||||
<description>Etapa de execução</description>
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// read a process variable set during a previous step
|
||||
// var valor = engine.getVariable('formAbertura_descricao');
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
<components>
|
||||
<VerticalLayout spacing="true" margin="true" width="100%">
|
||||
<TextField id="resultado" type="string" caption="Resultado" width="100%" required="true" />
|
||||
<ComboBox id="aprovado" type="string" caption="Aprovado?" required="true" allowNullSelection="false">
|
||||
<entry key="1" value="Sim"/>
|
||||
<entry key="0" value="Não"/>
|
||||
</ComboBox>
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</form>
|
||||
|
||||
</forms>
|
||||
```
|
||||
|
||||
## Components (78 desktop components)
|
||||
|
||||
Before using a component you are unsure about, read its reference:
|
||||
`~/.local/share/vitruvio-platform/docs/components/desktop/<ComponentName>.md`
|
||||
(full list in `docs/components/INDEX.md`).
|
||||
|
||||
### Layout
|
||||
|
||||
| Component | Common attributes |
|
||||
|---|---|
|
||||
| `VerticalLayout` | `spacing`, `margin`, `width`, `height`, `expandRatio` |
|
||||
| `HorizontalLayout` | same as above |
|
||||
| `Panel` | `id`, `caption`, `width`, `height`, `margin` |
|
||||
| `TabLayout` | `id`, `width`, `framed` — contains `<Tab caption="...">` children |
|
||||
|
||||
### Widgets
|
||||
|
||||
| Component | Key attributes |
|
||||
|---|---|
|
||||
| `TextField` | `id`, `type` (`string`/`number`), `caption`, `width`, `required` |
|
||||
| `NumericField` | `id`, `type`, `caption`, `width`, `visible` |
|
||||
| `DateField` | `id`, `type` (`date`/`datetime`), `caption`, `resolution` (`DAY`/`MINUTE`) |
|
||||
| `ComboBox` | `id`, `type`, `caption`, `allowNullSelection` — children: `<entry key="..." value="..."/>` |
|
||||
| `Label` | `id`, `width`, `contentMode` (`HTML`/`TEXT`) — child: `<value>...</value>` |
|
||||
| `ButtonWidget` | `id`, `caption`, `style` (`GREEN`/`RED`/`DEFAULT`), `defaultIcon` — child: `<onClickScript>` |
|
||||
| `RichTextArea` | `id`, `type`, `caption`, `width`, `height` |
|
||||
| `ImageWidget` | `id`, `width`, `height` — child: `<image><base64 extension="png">...</base64></image>` |
|
||||
|
||||
### DBTable (data grid)
|
||||
|
||||
```xml
|
||||
<DBTable id="tbDados" type="string" width="100%" rows="8"
|
||||
exportXLS="true" showRowCount="true" selectable="true">
|
||||
<datasource>
|
||||
<!-- Option A: static query -->
|
||||
<freeQuery connection-key="vitruvio_producao">
|
||||
<![CDATA[
|
||||
SELECT col1, col2
|
||||
FROM my_table
|
||||
WHERE param = ${myParam}
|
||||
]]>
|
||||
</freeQuery>
|
||||
|
||||
<!-- Option B: dynamic query built in JS -->
|
||||
<sqlBuilderDataSource connection-key="vitruvio_producao" language="JavaScript">
|
||||
<![CDATA[
|
||||
function buildSQL(params) {
|
||||
var sql = 'SELECT col1, col2 FROM my_table WHERE 1=1';
|
||||
var val = engine.getField('myFilter').getValue();
|
||||
if (val) {
|
||||
sql += ' AND col1 = ${val}';
|
||||
params.put('val', val);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
]]>
|
||||
</sqlBuilderDataSource>
|
||||
</datasource>
|
||||
<key-field>CHAVE</key-field>
|
||||
<columns>
|
||||
<column name="COL1" caption="Column 1" expand-ratio="1"/>
|
||||
<column name="COL2" caption="Column 2" expand-ratio="2"/>
|
||||
<generated name="Action" expand-ratio="0.5">
|
||||
<scriptColumnGenerator language="JavaScript">
|
||||
<![CDATA[
|
||||
function Generator() {
|
||||
var com = libService.loadScript('vaadinComponents');
|
||||
this.generate = function(itemId, columnId, item, container) {
|
||||
var btn = com.buttonIcon('action', function() {
|
||||
var id = item.getItemProperty('CHAVE').getValue();
|
||||
// do something
|
||||
}, 'edit');
|
||||
return com.horizontalLayout([btn]);
|
||||
}
|
||||
}
|
||||
var script = new Generator();
|
||||
]]>
|
||||
</scriptColumnGenerator>
|
||||
</generated>
|
||||
</columns>
|
||||
<bind>
|
||||
<parameter value-type="number" defaultValue="0" parameterName="myParam" field-ref="otherTable"/>
|
||||
</bind>
|
||||
</DBTable>
|
||||
```
|
||||
|
||||
**SQL in datasources:** use `${paramName}` for substitution — NOT `:paramName`. Named
|
||||
params (`:paramName`) are only for `queries/*.sql` files.
|
||||
|
||||
## engine API
|
||||
|
||||
```javascript
|
||||
// Fields
|
||||
engine.getField('id').getValue()
|
||||
engine.getField('id').getConvertedValue() // typed value (number, date, etc.)
|
||||
engine.getField('id').setValue(value)
|
||||
engine.getField('id').setEnabled(bool)
|
||||
engine.getField('id').setVisible(bool)
|
||||
engine.getField('id').setRequired(bool)
|
||||
engine.getField('id').setCaption('new caption')
|
||||
engine.getField('id').refresh() // DBTable — re-run its query
|
||||
|
||||
// Widgets / layouts
|
||||
engine.getWidgetController('id').getButton()
|
||||
engine.getLayout('id').getSelectedTab()
|
||||
|
||||
// User
|
||||
engine.getLoggedUser().getLogin()
|
||||
engine.getLoggedUser().getNome()
|
||||
|
||||
// Global variables (survive tab changes within a session)
|
||||
engine.setGlobalVariable('key', value)
|
||||
engine.getGlobalVariable('key')
|
||||
|
||||
// Process forms only — process variables:
|
||||
engine.getVariable('varName')
|
||||
engine.setVariable('varName', value)
|
||||
engine.getProcessDefinitionId()
|
||||
engine.formKey() // current form's formKey
|
||||
engine.getFormName()
|
||||
|
||||
// Open another panel / load a library
|
||||
var vUI = libService.loadScript('vUI');
|
||||
vUI.showPanel('panelKey', { param1: value1 });
|
||||
var lib = libService.loadScript('scriptKey');
|
||||
```
|
||||
|
||||
## Script rules (Rhino ES5)
|
||||
|
||||
Desktop form scripts run on **Rhino ES5** — no `let`/`const`, arrow functions, template
|
||||
literals, destructuring, `class`, `import/export`. Use `var`, string `+` concat,
|
||||
`JSON.parse`/`JSON.stringify`, `importClass(Packages.some.java.Class)` for Java interop.
|
||||
(See the repo `CLAUDE.md` "JavaScript — ES5 / Rhino Engine" section.)
|
||||
|
||||
> Note: this ES5 rule is for **desktop**. Mobile forms use modern JS on the client — see
|
||||
> **vitruvio-criar-form-mobile**.
|
||||
|
||||
## Step 4 — Manifest
|
||||
|
||||
If run **standalone**, set `"forms"."desktop"` to the file path on the existing panel or
|
||||
process entry in `vitruvio.json`. Full entry creation is handled by **vitruvio-criar-painel**
|
||||
/ **vitruvio-criar-processo**.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created/updated and which variant (panel/process).
|
||||
- For processes: each `<form formKey>` must match an `activiti:formKey` in the BPMN, and
|
||||
submitted field `id="X"` in `formKey="A"` becomes process variable `A_X`.
|
||||
- `run()` in `<initScript>` is called every time the form opens.
|
||||
- `${paramName}` for SQL substitution in datasources; `:paramName` only in named query files.
|
||||
@@ -0,0 +1,267 @@
|
||||
---
|
||||
name: vitruvio-criar-form-mobile
|
||||
description: >
|
||||
Use when the user wants to create or edit the MOBILE form of a Vitruvio panel or process
|
||||
(the React-Native form rendered in the mobile app). Triggers: "create mobile form",
|
||||
"criar formulário mobile", "mobile panel", "painel mobile", "criar painel mobile",
|
||||
"process mobile form", "formulário mobile do processo", "add mobile form", "mobile form xml",
|
||||
"bridge mobile". This is the single home for mobile form knowledge; vitruvio-criar-painel
|
||||
and vitruvio-criar-processo call it for their mobile part. For the desktop form use
|
||||
vitruvio-criar-form-desktop.
|
||||
---
|
||||
|
||||
# Create Vitruvio Mobile Form
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
Mobile forms are **not** desktop forms with a different skin. They use a different schema,
|
||||
a smaller component set, a different JavaScript runtime, and an explicit server↔client data
|
||||
contract. Read this whole skill before scaffolding — the mistakes here are not the desktop
|
||||
mistakes.
|
||||
|
||||
## The four things that make mobile different
|
||||
|
||||
1. **Different schema.** Root is `<mobile-forms>`, namespace
|
||||
`http://www.davinti.com.br/vitruvio/mobile-form`, XSD `vitruvio-mobile-form.xsd`.
|
||||
2. **Only 18 components** (vs 78 desktop). Do **not** assume a desktop widget exists on
|
||||
mobile. The full list:
|
||||
`CheckBox, ComboBox, GoogleMapsField, HorizontalLayout, ImageLibraryByFieldValue,
|
||||
ImageWidget, Label, MaskedField, MoneyField, NumericField, OptionGroup,
|
||||
ProgressBarWidget, RatingStars, SignaturePadField, TabLayout, TextField, Toggle,
|
||||
VerticalLayout` (plus structural `SubForm`/`ItemList`). Read
|
||||
`~/.local/share/vitruvio-platform/docs/components/mobile/<Component>.md` before using one.
|
||||
3. **Two JavaScript runtimes.**
|
||||
- **Client-side** (`initScript`, `discoveryScript`, validators, component event scripts):
|
||||
**modern React-Native JS** — arrow functions, Promises, `.then()/.catch()` are fine and
|
||||
expected. Most APIs are **async** and return Promises.
|
||||
- **Server-side** (`<ServerSide><Bridge>` `execute(...)` bodies): **Rhino ES5**, same rules
|
||||
as scripts/desktop. This is the only place with access to platform libs and services
|
||||
(`libService`, `runtimeService`, db, etc.).
|
||||
4. **Data is explicit.** Nothing is auto-injected the way desktop process variables are. Every
|
||||
piece of server data the form needs must be declared — via an `<Autoload>` variable, a
|
||||
`<QueryDataSource>` (synced to the device, works offline), or fetched on demand from a
|
||||
named `<Bridge>` with `vCommunicationService.executeOnServer(...)`.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Determine variant and gather the data contract
|
||||
|
||||
Ask (only what is missing):
|
||||
|
||||
- **Panel or process mobile form?** (see **Differences: panel vs process** below)
|
||||
- **Key** — the panel or process key (folder name).
|
||||
- For a **process**: which `formKey`(s) — they must match the `activiti:formKey` in
|
||||
`processes/<key>/<key>.bpmn`.
|
||||
- **Which variables does the form need?** Because nothing is auto-injected, ask explicitly:
|
||||
- process variables to read (each becomes an `<Autoload>` `<variable>` and/or a Bridge call)
|
||||
- reference/lookup data (each becomes a `<QueryDataSource>` backed by a `queries/*.sql`)
|
||||
- any server-only logic / libs needed (each becomes a `<Bridge>`)
|
||||
- **Offline?** Whether the data sources must be available without connectivity (affects
|
||||
`autoSyncOnInit` / `autoSyncOnDiscovery`).
|
||||
|
||||
## Differences: panel vs process
|
||||
|
||||
The schema, component set, ServerSide/Bridge mechanics and JS runtimes below are **identical**
|
||||
for both. Only these differ:
|
||||
|
||||
| | Panel mobile form | Process mobile form |
|
||||
|----------------|-------------------|---------------------|
|
||||
| File | `panels/<key>/<key>-mobile.xml` | `processes/<key>/<key>-mobile.xml` |
|
||||
| Root attribute | `<mobile-forms>` (no `processKey`) | `<mobile-forms processKey="<key>">` |
|
||||
| Forms per file | one `<form>` | **one `<form formKey>` per BPMN `activiti:formKey`** (must match) |
|
||||
| Manifest | set `forms.mobile` **and** `showInMobileList: true` on the panel entry | set `forms.mobile` (and optionally `forms.mobileAlternative`) on the process entry |
|
||||
| Variables | use `engine.getGlobalVariable` / Autoload | process variables are fetched server-side via a Bridge (`runtimeService.getVariable`) and/or declared in `<Autoload>` |
|
||||
|
||||
> Filename: name files by the **artifact key + suffix** — `<key>-mobile.xml` (and
|
||||
> `<key>-desktop.xml`, `<key>.bpmn`). This keeps every form searchable by its key instead of
|
||||
> dozens of identical `form-mobile.xml` tabs. Older content used `form-mobile.xml` /
|
||||
> `form_web_mobile.xml`; the real path is whatever `forms.mobile` points to, so legacy files
|
||||
> still work — but new scaffolds use `<key>-mobile.xml`.
|
||||
|
||||
## Step 3 — Scaffold the file
|
||||
|
||||
### Skeleton (process variant shown; for a panel drop `processKey` and use a single `<form>`)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mobile-forms processKey="<key>"
|
||||
xmlns="http://www.davinti.com.br/vitruvio/mobile-form"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.davinti.com.br/vitruvio/mobile-form https://bitbucket.org/davinTI/vitruvio-xds/raw/master/vitruvio-mobile-form.xsd">
|
||||
|
||||
<form formKey="formColeta">
|
||||
<name>Coleta</name>
|
||||
<description>Etapa de coleta no app</description>
|
||||
|
||||
<!-- Runs when the form opens. CLIENT-SIDE: modern JS, async APIs return Promises. -->
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
// Autoloaded variables land in the global scope:
|
||||
engine.getField('nomeLoja').setValue(engine.getGlobalVariable('nomeLoja'));
|
||||
}
|
||||
]]>
|
||||
</initScript>
|
||||
|
||||
<!-- Optional: pull server data when the task is discovered (before the user opens it). -->
|
||||
<discoveryScript language="JavaScript">
|
||||
<![CDATA[
|
||||
function run() {
|
||||
var params = { id: execution.getProcessInstanceId() };
|
||||
vCommunicationService.executeOnServer('bridgeNumeroCarga', params).then(result => {
|
||||
var n = parseInt(result, 10);
|
||||
execution.setVariable('numeroCarga', n).then(ok => {}).catch(err => {
|
||||
console.log('Erro ao setar numeroCarga localmente', err);
|
||||
});
|
||||
}).catch(err => {
|
||||
console.log('Erro ao coletar numeroCarga no server', err);
|
||||
});
|
||||
}
|
||||
]]>
|
||||
</discoveryScript>
|
||||
|
||||
<!-- Block completing the task when a rule is not met. -->
|
||||
<validators>
|
||||
<ScriptValidator execution="COMPLETE" language="JavaScript" id="validadorComplete">
|
||||
<![CDATA[
|
||||
function Validator() {
|
||||
var msg;
|
||||
this.getMessage = function() { return msg; };
|
||||
this.isValid = function() {
|
||||
if (engine.getField('confirma').getValue() == 'Sim') { return true; }
|
||||
msg = 'Confirme a execução antes de finalizar.';
|
||||
return false;
|
||||
};
|
||||
}
|
||||
var validator = new Validator();
|
||||
]]>
|
||||
</ScriptValidator>
|
||||
</validators>
|
||||
|
||||
<components>
|
||||
<VerticalLayout spacing="true" margin="true" width="100%">
|
||||
<TextField id="nomeLoja" type="string" caption="Loja" readOnly="true" />
|
||||
<OptionGroup id="confirma" type="string" caption="Executou?" required="true">
|
||||
<entry key="Sim" value="Sim" />
|
||||
<entry key="Nao" value="Não" />
|
||||
</OptionGroup>
|
||||
<SignaturePadField id="assinatura" caption="Assinatura" />
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
|
||||
<!-- Explicitly declared variables auto-injected into the form's scope on load. -->
|
||||
<Autoload>
|
||||
<variables autoInjectionScope="ENGINE_GLOBAL_SCOPE" autoPersist="true">
|
||||
<variable>nomeLoja</variable>
|
||||
<variable>numeroCarga</variable>
|
||||
</variables>
|
||||
</Autoload>
|
||||
|
||||
<!-- Everything the server provides to this form. -->
|
||||
<ServerSide>
|
||||
<!-- Named queries (queries/*.sql) synced to the device for offline lookups. -->
|
||||
<DataSources>
|
||||
<QueryDataSource key="qry_produtos_carga" autoSyncOnInit="true" autoSyncOnDiscovery="true" refreshInSeconds="60" />
|
||||
</DataSources>
|
||||
|
||||
<!-- Server-side functions. Rhino ES5. Full access to libService / runtimeService / db.
|
||||
Called from the client via vCommunicationService.executeOnServer('id', params). -->
|
||||
<Bridges>
|
||||
<Bridge language="JavaScript" id="bridgeNumeroCarga">
|
||||
<![CDATA[
|
||||
function execute(params) {
|
||||
var numeroCarga = runtimeService.getVariable(params.id, 'numeroCarga');
|
||||
return numeroCarga ? numeroCarga : -1;
|
||||
}
|
||||
]]>
|
||||
</Bridge>
|
||||
</Bridges>
|
||||
</ServerSide>
|
||||
</form>
|
||||
|
||||
</mobile-forms>
|
||||
```
|
||||
|
||||
## How libs and process variables reach the mobile form
|
||||
|
||||
The mobile app cannot call `libService.loadScript(...)` or read process variables directly —
|
||||
those live on the server. The pattern is always **declare a Bridge, call it from the client**:
|
||||
|
||||
```xml
|
||||
<!-- SERVER-SIDE (Rhino ES5): a lib used to build a barcode image -->
|
||||
<Bridge language="JavaScript" id="imagemCodBarras">
|
||||
<![CDATA[
|
||||
function execute(codbarras) {
|
||||
var generator = libService.loadScript('barcode-gen');
|
||||
return ',' + generator.generateEAN13BarcodeImageAsBase64({ value: codbarras });
|
||||
}
|
||||
]]>
|
||||
</Bridge>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// CLIENT-SIDE (modern JS): call the bridge, use the Promise result
|
||||
vCommunicationService.executeOnServer('imagemCodBarras', codigoBarras).then(base64 => {
|
||||
engine.getField('codigoImagem').setValue(base64);
|
||||
}).catch(err => console.log('Erro no bridge imagemCodBarras', err));
|
||||
```
|
||||
|
||||
Rules of thumb:
|
||||
- Anything needing a **platform lib, the database, or a platform service** → put it in a
|
||||
**Bridge** (server, ES5) and call it with `vCommunicationService.executeOnServer`.
|
||||
- **Reference/lookup tables** the form reads repeatedly → a **`QueryDataSource`** backed by a
|
||||
`queries/*.sql` (works offline once synced).
|
||||
- **Process variables** the form needs → either declare them in `<Autoload>` or fetch them
|
||||
in a Bridge via `runtimeService.getVariable(processInstanceId, 'varName')` and store locally
|
||||
with `execution.setVariable(...)`.
|
||||
- Client-side `execution.getVariable(...)` / `execution.setVariable(...)` are **async** and
|
||||
return Promises — use `.then()`.
|
||||
|
||||
## List-based entry: SubForm + ItemList
|
||||
|
||||
For "add many items" screens (collect a list of rows), use a `SubForm` with an `<ItemList>`:
|
||||
|
||||
```xml
|
||||
<SubForm formKey="executarAcao">
|
||||
<name>Executar AÇÃO</name>
|
||||
<initScript language="JavaScript">
|
||||
<![CDATA[ function run(apply) { if (apply) { apply(); } } ]]>
|
||||
</initScript>
|
||||
<ItemList addItemButtonCaption="Gravar na Lista" caption="Itens">
|
||||
<property id="OBSERVACAO" caption="Observação" />
|
||||
</ItemList>
|
||||
<components>
|
||||
<VerticalLayout width="100%" spacing="true" margin="true">
|
||||
<!-- fields captured per item -->
|
||||
</VerticalLayout>
|
||||
</components>
|
||||
</SubForm>
|
||||
```
|
||||
|
||||
Validate list completeness with `engine.getSubFormListSizeByStatus(function(size){ ... }, "all")`
|
||||
inside a `COMPLETE` validator.
|
||||
|
||||
## Step 4 — Manifest
|
||||
|
||||
If run **standalone**, update the matching entry in `vitruvio.json`:
|
||||
- **Panel**: set `forms.mobile = "panels/<key>/<key>-mobile.xml"` and `showInMobileList: true`.
|
||||
- **Process**: set `forms.mobile = "processes/<key>/<key>-mobile.xml"`.
|
||||
|
||||
Full entry creation is handled by **vitruvio-criar-painel** / **vitruvio-criar-processo**.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created/updated and variant (panel/process).
|
||||
- Which variables/queries/bridges were declared, and that **only declared data is available**
|
||||
on the device — anything else must be added as an Autoload variable, QueryDataSource, or Bridge.
|
||||
- Reminder: client scripts are modern JS (Promises); Bridge bodies are server-side Rhino ES5.
|
||||
- For processes: each `<form formKey>` must match an `activiti:formKey` in the BPMN.
|
||||
- Suggest reading `docs/components/mobile/` and an example
|
||||
(`~/.local/share/vitruvio-platform/examples/processes/*/form_web_mobile.xml`) for richer screens.
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
name: vitruvio-criar-painel
|
||||
description: >
|
||||
Use when the user wants to create a new panel (tela / form) in a Vitruvio repository.
|
||||
Triggers: "create panel", "new panel", "criar painel", "novo painel", "add panel",
|
||||
or any request to scaffold a <key>-desktop.xml inside a panels/ folder.
|
||||
---
|
||||
|
||||
# Create Vitruvio Panel (orchestrator)
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
A panel is one or two form files plus a manifest entry:
|
||||
|
||||
| Part | Skill that owns it |
|
||||
|------|--------------------|
|
||||
| `panels/<key>/<key>-desktop.xml` (web form) | **vitruvio-criar-form-desktop** (panel variant) |
|
||||
| `panels/<key>/<key>-mobile.xml` (app form, optional) | **vitruvio-criar-form-mobile** (panel variant) |
|
||||
|
||||
This skill collects the intent once, drives those skills, and registers the panel in
|
||||
`vitruvio.json`. Do the file work by following the referenced skills — do not re-derive
|
||||
their templates here.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect panel details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **Key** — PascalCase or snake_case unique identifier. Used in `engine.showPanel(key)` and
|
||||
as the folder name.
|
||||
- **Name** — human-readable label shown in the Vitruvio menu.
|
||||
- **Category** — display category, hierarchical with `/` (e.g. `"Comercial"`,
|
||||
`"Auditoria/Gondola"`).
|
||||
- **Description** — one sentence (optional).
|
||||
- **Open in new window?** — `true`/`false`. Default `true`.
|
||||
- **Show in mobile list?** — `true`/`false`. Default `false`.
|
||||
- **Needs a mobile form?** — if yes, a `<key>-mobile.xml` is created too. Mobile is a different
|
||||
schema with explicit data wiring (the mobile skill will ask for variables/lookups/libs).
|
||||
- **What should the panel do?** — fields/behaviour, so the form scaffold is useful.
|
||||
|
||||
## Step 3 — Scaffold
|
||||
|
||||
```bash
|
||||
vitruvio new panel <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates `panels/<key>/<key>-desktop.xml` and the `vitruvio.json` entry. Then replace the
|
||||
generated form using the focused skills:
|
||||
|
||||
1. **Desktop form** — follow **vitruvio-criar-form-desktop** (panel variant) to write
|
||||
`panels/<key>/<key>-desktop.xml` from the user's description.
|
||||
2. **Mobile form (if requested)** — follow **vitruvio-criar-form-mobile** (panel variant) to
|
||||
write `panels/<key>/<key>-mobile.xml`. `vitruvio new` does not create it.
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the entry. Full shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"category": "<category>",
|
||||
"displayOrder": 0,
|
||||
"showInPresentation": false,
|
||||
"openInNewWindow": true,
|
||||
"showInMobileList": false,
|
||||
"displayTimeInSeconds": 0,
|
||||
"allowedGroups": [],
|
||||
"allowedUsers": [],
|
||||
"forms": {
|
||||
"desktop": "panels/<key>/<key>-desktop.xml"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fields `vitruvio new` typically leaves at defaults that need updating:
|
||||
- `"category"` — set to the user's category (default `""`).
|
||||
- `"description"` — add if provided.
|
||||
- `"openInNewWindow"` — set to `false` for same-window.
|
||||
- `"showInMobileList"` — set to `true` for mobile visibility.
|
||||
- If a mobile form was created, add `"mobile": "panels/<key>/<key>-mobile.xml"` inside `"forms"`
|
||||
(and set `showInMobileList: true`).
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- Files created: `<key>-desktop.xml` (and `<key>-mobile.xml` if applicable).
|
||||
- Registered in `vitruvio.json` with key `<key>`.
|
||||
- `run()` in `<initScript>` is called every time the panel opens.
|
||||
- `${paramName}` for SQL substitution in datasource blocks; `:paramName` only in named query files.
|
||||
- For mobile: only explicitly declared data is available on the device — see vitruvio-criar-form-mobile.
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
name: vitruvio-criar-patch
|
||||
description: >
|
||||
Use when the user wants to create a new Liquibase database migration patch in a Vitruvio repository.
|
||||
Triggers: "create patch", "new patch", "criar patch", "novo patch", "database migration",
|
||||
"migração de banco", "changeset", "liquibase", or any request to scaffold oracle/postgresql patch XML files.
|
||||
---
|
||||
|
||||
# Create Vitruvio Patch
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating a new Liquibase database migration patch inside a Vitruvio repository. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Inspect existing patches
|
||||
|
||||
`vitruvio new patch` auto-detects the next changeset ID by scanning all existing XML files — you don't need to find it manually. For situational awareness you can check:
|
||||
|
||||
```bash
|
||||
find patches/ -name "*.xml" | xargs grep -h 'id="[0-9]' 2>/dev/null | grep -oP 'id="\K[0-9]+' | sort -n | tail -1
|
||||
```
|
||||
|
||||
Next ID = last found + 1. If no patches exist yet, starts at 1. The CLI also creates `patches/oracle/` and `patches/postgresql/` subdirectories if they don't exist yet.
|
||||
|
||||
## Step 3 — Collect migration details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **What this migration does** — describe the change (e.g. "add column STATUS to table PEDIDO", "create table AUDIT_LOG", "insert config rows").
|
||||
- **Author** — Vitruvio username (e.g. `joao.felix`). Use `git config user.name` if unsure.
|
||||
- **Module key** — used in the filename (e.g. `GO`, `checklist`, `faturamento`). Default: the repo's `metadata.key` from `vitruvio.json`.
|
||||
|
||||
## Step 4 — Create the patch files
|
||||
|
||||
**Both oracle/ and postgresql/ files must always be created and kept in sync.**
|
||||
|
||||
Filename convention: `{YYYYMMDDHHmm}_{MODULE_KEY}.xml` (e.g. `202506011430_checklist.xml`).
|
||||
|
||||
Use `vitruvio new patch` to scaffold — it handles the filename, ID assignment, and directory creation automatically:
|
||||
|
||||
```bash
|
||||
vitruvio new patch <module-key>
|
||||
```
|
||||
|
||||
Find the created files, then replace the SQL placeholder in both with the actual migration for each DB dialect, and set `author` to the correct Vitruvio username:
|
||||
|
||||
```bash
|
||||
ls -t patches/oracle/ | head -1
|
||||
```
|
||||
|
||||
### Absolute rules
|
||||
|
||||
- **Append-only.** Never edit or delete existing `<changeSet>` entries — modifying a checksum that Liquibase already recorded breaks deployment.
|
||||
- **Unique numeric IDs.** Each `<changeSet id="...">` must have a unique ID within the repo. Increment from the last found.
|
||||
- **Always use `<preConditions onFail="MARK_RAN">`** — every changeset must be idempotent and safe to re-run on any DB state.
|
||||
- **Oracle ≠ PostgreSQL.** Write each file for its target DB — data types, sequences, and quoting differ. Never copy-paste blindly.
|
||||
- **One logical change per changeset** — don't batch unrelated changes into a single `<changeSet>`.
|
||||
|
||||
### File skeleton
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">
|
||||
|
||||
<changeSet author="<author>" id="<next-id>" objectQuotingStrategy="LEGACY">
|
||||
<preConditions onError="WARN" onFail="MARK_RAN" onSqlOutput="IGNORE">
|
||||
<!-- guard appropriate for the operation — see examples below -->
|
||||
</preConditions>
|
||||
<sql endDelimiter=";" splitStatements="true" stripComments="false">
|
||||
-- SQL for this DB dialect
|
||||
</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
```
|
||||
|
||||
### preConditions reference
|
||||
|
||||
| Operation | Guard to use |
|
||||
|-----------|-------------|
|
||||
| CREATE TABLE | `<not><tableExists tableName="MY_TABLE"/></not>` |
|
||||
| ADD COLUMN | `<not><columnExists tableName="MY_TABLE" columnName="MY_COL"/></not>` |
|
||||
| CREATE INDEX | `<not><indexExists indexName="IDX_NAME"/></not>` |
|
||||
| ADD CONSTRAINT / FK | `<not><foreignKeyConstraintExists foreignKeyName="FK_NAME"/></not>` |
|
||||
| INSERT row (by PK) | `<sqlCheck expectedResult="0">SELECT COUNT(*) FROM MY_TABLE WHERE ID = 1</sqlCheck>` |
|
||||
| DROP TABLE | `<tableExists tableName="MY_TABLE"/>` |
|
||||
| DROP COLUMN | `<columnExists tableName="MY_TABLE" columnName="MY_COL"/>` |
|
||||
|
||||
### Oracle vs PostgreSQL differences to watch
|
||||
|
||||
| | Oracle | PostgreSQL |
|
||||
|---|---|---|
|
||||
| Auto-increment | Separate `CREATE SEQUENCE` + trigger or `DEFAULT seq.NEXTVAL` | `SERIAL` or `GENERATED ALWAYS AS IDENTITY` |
|
||||
| String type | `VARCHAR2(n)` | `VARCHAR(n)` |
|
||||
| Boolean | `NUMBER(1)` | `BOOLEAN` |
|
||||
| Date/time | `DATE`, `TIMESTAMP` | `DATE`, `TIMESTAMP` |
|
||||
| Current timestamp | `SYSDATE` | `CURRENT_TIMESTAMP` |
|
||||
| Quoting | `LEGACY` strategy (unquoted) | Same |
|
||||
|
||||
## Step 5 — Check vitruvio.json patches registration
|
||||
|
||||
The patches directory only needs to be registered once. Check if it is already there:
|
||||
|
||||
```bash
|
||||
grep -A2 '"patches"' vitruvio.json
|
||||
```
|
||||
|
||||
If not registered, add to `vitruvio.json`:
|
||||
|
||||
```json
|
||||
"patches": "patches/"
|
||||
```
|
||||
|
||||
## Step 6 — Report
|
||||
|
||||
Tell the user:
|
||||
- Files created: `patches/oracle/<filename>.xml` and `patches/postgresql/<filename>.xml`
|
||||
- Changeset IDs used
|
||||
- Summary of what each changeset does
|
||||
- Reminder: never edit existing changesets once committed — add new ones instead
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
name: vitruvio-criar-processo-bpmn
|
||||
description: >
|
||||
Use when the user wants to create or edit only the BPMN workflow file of a Vitruvio
|
||||
process (the Activiti flow: lanes, tasks, gateways, sequence flows), without touching
|
||||
the forms. Triggers: "create bpmn", "criar bpmn", "novo bpmn", "fluxo do processo",
|
||||
"workflow do processo", "só o bpmn", "process flow", "edit the bpmn".
|
||||
For the full process (BPMN + forms + manifest) use vitruvio-criar-processo, which calls
|
||||
this skill for the BPMN part.
|
||||
---
|
||||
|
||||
# Create Vitruvio Process BPMN
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating the **BPMN workflow file** of a Vitruvio process. This skill is focused
|
||||
on the `.bpmn` file only — the desktop form is handled by **vitruvio-criar-form-desktop**
|
||||
(process variant) and the mobile form by **vitruvio-criar-form-mobile** (process variant).
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect flow details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **Key** — snake_case or camelCase unique identifier. Becomes the BPMN process ID and the
|
||||
folder name. Must match the `key` used for the process in `vitruvio.json`.
|
||||
- **Name** — human-readable label.
|
||||
- **Who can start it?** — group key(s) allowed to open the process (e.g. `gestao`,
|
||||
`admin`). Used in `activiti:candidateStarterGroups`.
|
||||
- **Steps** — the human tasks (user tasks), and which group handles each. A simple linear
|
||||
flow is enough to start.
|
||||
- **Decisions / branches?** — any exclusive gateways with conditions.
|
||||
- **Automatic steps?** — any script tasks running between user tasks.
|
||||
|
||||
## Step 3 — Write the BPMN file: `processes/<key>/<key>.bpmn`
|
||||
|
||||
### Critical rules
|
||||
|
||||
- The `<bpmn2:process id="...">` value is the canonical process identity. The importer
|
||||
reads it from the BPMN, not from vitruvio.json. **It must match the `key`.**
|
||||
- Every node must appear in a `<bpmn2:laneSet>` / `<bpmn2:lane>` **and** in the
|
||||
`<bpmndi:BPMNDi>` section — Vitruvio renders the diagram.
|
||||
- Each `activiti:formKey` on the start event and user tasks must match a
|
||||
`<form formKey="...">` in the desktop form XML (and mobile form, if present).
|
||||
- Process variables from submitted forms are auto-named `{formKey}_{fieldId}`
|
||||
(e.g. `formAbertura_status`). Gateway conditions reference them.
|
||||
- Gateway conditions use `#{variable == 'value'}` (JUEL expression language).
|
||||
- Script tasks call `vScriptService.loadScript('scriptKey', 'javascript')`, **not**
|
||||
`libService`.
|
||||
|
||||
### Minimal skeleton (start → user task → end, single lane)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn2:definitions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
||||
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
||||
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
||||
xmlns:activiti="http://activiti.org/bpmn"
|
||||
id="sample-diagram"
|
||||
targetNamespace="http://bpmn.io/schema/bpmn"
|
||||
exporter="bpmn-js (https://demo.bpmn.io)"
|
||||
exporterVersion="8.2.0"
|
||||
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd">
|
||||
|
||||
<bpmn2:collaboration id="Collaboration_<key>">
|
||||
<bpmn2:participant id="processo_<key>" name="<Name>" processRef="<key>" />
|
||||
</bpmn2:collaboration>
|
||||
|
||||
<bpmn2:process id="<key>" name="<Name>" isExecutable="true"
|
||||
activiti:candidateStarterGroups="<starterGroup>">
|
||||
<bpmn2:laneSet>
|
||||
<bpmn2:lane id="lane_execucao" name="Execução">
|
||||
<bpmn2:flowNodeRef>inicio</bpmn2:flowNodeRef>
|
||||
<bpmn2:flowNodeRef>task_executar</bpmn2:flowNodeRef>
|
||||
<bpmn2:flowNodeRef>fim</bpmn2:flowNodeRef>
|
||||
</bpmn2:lane>
|
||||
</bpmn2:laneSet>
|
||||
|
||||
<!-- Start event: activiti:initiator stores the login of who opened the process -->
|
||||
<bpmn2:startEvent id="inicio" name="Início"
|
||||
activiti:formKey="formAbertura"
|
||||
activiti:initiator="iniciador">
|
||||
<bpmn2:outgoing>flow_inicio_task</bpmn2:outgoing>
|
||||
</bpmn2:startEvent>
|
||||
|
||||
<!-- User task: candidateGroups controls who sees it in their inbox -->
|
||||
<bpmn2:userTask id="task_executar" name="Executar"
|
||||
activiti:formKey="formExecutar"
|
||||
activiti:candidateGroups="${vStringUtils.validateRoles(gr_executores)}">
|
||||
<bpmn2:incoming>flow_inicio_task</bpmn2:incoming>
|
||||
<bpmn2:outgoing>flow_task_fim</bpmn2:outgoing>
|
||||
</bpmn2:userTask>
|
||||
|
||||
<!-- Script task example (omit if not needed):
|
||||
<bpmn2:scriptTask id="script_processar" name="Processar" scriptFormat="javascript">
|
||||
<bpmn2:incoming>flow_task_script</bpmn2:incoming>
|
||||
<bpmn2:outgoing>flow_script_fim</bpmn2:outgoing>
|
||||
<bpmn2:script>var f = vScriptService.loadScript('meu_script', 'javascript');
|
||||
f(execution);</bpmn2:script>
|
||||
</bpmn2:scriptTask>
|
||||
-->
|
||||
|
||||
<!-- Exclusive gateway example (omit if not needed):
|
||||
<bpmn2:exclusiveGateway id="gw_decisao" name="Aprovado?">
|
||||
<bpmn2:incoming>flow_task_gw</bpmn2:incoming>
|
||||
<bpmn2:outgoing>flow_sim</bpmn2:outgoing>
|
||||
<bpmn2:outgoing>flow_nao</bpmn2:outgoing>
|
||||
</bpmn2:exclusiveGateway>
|
||||
<bpmn2:sequenceFlow id="flow_sim" name="Sim" sourceRef="gw_decisao" targetRef="fim">
|
||||
<bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">#{formExecutar_aprovado == '1'}</bpmn2:conditionExpression>
|
||||
</bpmn2:sequenceFlow>
|
||||
<bpmn2:sequenceFlow id="flow_nao" name="Não" sourceRef="gw_decisao" targetRef="task_executar">
|
||||
<bpmn2:conditionExpression xsi:type="bpmn2:tFormalExpression">#{formExecutar_aprovado == '0'}</bpmn2:conditionExpression>
|
||||
</bpmn2:sequenceFlow>
|
||||
-->
|
||||
|
||||
<bpmn2:endEvent id="fim" name="Fim">
|
||||
<bpmn2:incoming>flow_task_fim</bpmn2:incoming>
|
||||
</bpmn2:endEvent>
|
||||
|
||||
<bpmn2:sequenceFlow id="flow_inicio_task" sourceRef="inicio" targetRef="task_executar" />
|
||||
<bpmn2:sequenceFlow id="flow_task_fim" sourceRef="task_executar" targetRef="fim" />
|
||||
</bpmn2:process>
|
||||
|
||||
<!-- BPMNDi: visual layout — required for the diagram to render -->
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_<key>">
|
||||
|
||||
<bpmndi:BPMNShape id="Participant_di" bpmnElement="processo_<key>" isHorizontal="true">
|
||||
<dc:Bounds x="100" y="80" width="750" height="180" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
||||
<bpmndi:BPMNShape id="lane_execucao_di" bpmnElement="lane_execucao" isHorizontal="true">
|
||||
<dc:Bounds x="130" y="80" width="720" height="180" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
||||
<bpmndi:BPMNShape id="inicio_di" bpmnElement="inicio">
|
||||
<dc:Bounds x="192" y="152" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="195" y="195" width="30" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
|
||||
<bpmndi:BPMNShape id="task_executar_di" bpmnElement="task_executar">
|
||||
<dc:Bounds x="310" y="130" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
|
||||
<bpmndi:BPMNShape id="fim_di" bpmnElement="fim">
|
||||
<dc:Bounds x="492" y="152" width="36" height="36" />
|
||||
<bpmndi:BPMNLabel>
|
||||
<dc:Bounds x="497" y="195" width="19" height="14" />
|
||||
</bpmndi:BPMNLabel>
|
||||
</bpmndi:BPMNShape>
|
||||
|
||||
<bpmndi:BPMNEdge id="flow_inicio_task_di" bpmnElement="flow_inicio_task">
|
||||
<di:waypoint x="228" y="170" />
|
||||
<di:waypoint x="310" y="170" />
|
||||
</bpmndi:BPMNEdge>
|
||||
|
||||
<bpmndi:BPMNEdge id="flow_task_fim_di" bpmnElement="flow_task_fim">
|
||||
<di:waypoint x="410" y="170" />
|
||||
<di:waypoint x="492" y="170" />
|
||||
</bpmndi:BPMNEdge>
|
||||
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn2:definitions>
|
||||
```
|
||||
|
||||
### Multi-lane pattern (when tasks belong to different roles)
|
||||
|
||||
Add each lane inside `<bpmn2:laneSet>`, list the node IDs inside each lane, and adjust
|
||||
the BPMNDi bounds:
|
||||
|
||||
```xml
|
||||
<bpmn2:laneSet>
|
||||
<bpmn2:lane id="lane_gestao" name="Gestão">
|
||||
<bpmn2:flowNodeRef>inicio</bpmn2:flowNodeRef>
|
||||
<bpmn2:flowNodeRef>fim</bpmn2:flowNodeRef>
|
||||
</bpmn2:lane>
|
||||
<bpmn2:lane id="lane_execucao" name="Execução">
|
||||
<bpmn2:flowNodeRef>task_executar</bpmn2:flowNodeRef>
|
||||
</bpmn2:lane>
|
||||
</bpmn2:laneSet>
|
||||
```
|
||||
|
||||
### Script task — script side
|
||||
|
||||
```javascript
|
||||
// In <bpmn2:script> inside a scriptTask:
|
||||
var f = vScriptService.loadScript('meu_script', 'javascript');
|
||||
f(execution);
|
||||
|
||||
// In the script file itself (pattern: process/task script — see vitruvio-criar-script):
|
||||
(function(execution) {
|
||||
var db = libService.loadScript('db');
|
||||
var banco = new db(db.VITRUVIO_DATASOURCE);
|
||||
var status = execution.getVariable('formAbertura_status');
|
||||
// ...
|
||||
})(execution)
|
||||
```
|
||||
|
||||
## Step 4 — Manifest
|
||||
|
||||
If this skill is run **standalone** (the process already exists in `vitruvio.json`),
|
||||
ensure its entry has `"bpmn": "processes/<key>/<key>.bpmn"`. Do not create or modify
|
||||
the rest of the entry here — full registration is handled by **vitruvio-criar-processo**.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created/updated: `processes/<key>/<key>.bpmn`
|
||||
- The process identity is the `<bpmn2:process id>` — it must match the key.
|
||||
- Each `activiti:formKey` must have a matching `<form formKey="...">` in the form XML
|
||||
(create/update it with **vitruvio-criar-form-desktop** / **vitruvio-criar-form-mobile**).
|
||||
- Submitted field `id="X"` in `formKey="formAbertura"` becomes variable `formAbertura_X`.
|
||||
- For complex flows, recommend editing the BPMN in bpmn.io or Camunda Modeler before deploying.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
name: vitruvio-criar-processo
|
||||
description: >
|
||||
Use when the user wants to create a new Activiti workflow process in a Vitruvio repository.
|
||||
Triggers: "create process", "new process", "criar processo", "novo processo", "add process",
|
||||
"workflow", "BPMN", or any request to scaffold a BPMN file or process form XML.
|
||||
---
|
||||
|
||||
# Create Vitruvio Process (orchestrator)
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
A process is made of up to three artifacts plus a manifest entry:
|
||||
|
||||
| Part | Skill that owns it |
|
||||
|------|--------------------|
|
||||
| `processes/<key>/<key>.bpmn` (workflow) | **vitruvio-criar-processo-bpmn** |
|
||||
| `processes/<key>/<key>-desktop.xml` (web form) | **vitruvio-criar-form-desktop** (process variant) |
|
||||
| `processes/<key>/<key>-mobile.xml` (app form, optional) | **vitruvio-criar-form-mobile** (process variant) |
|
||||
|
||||
This skill collects the intent once, drives those skills, and registers the process in
|
||||
`vitruvio.json`. Do the file work by following the referenced skills — do not re-derive
|
||||
their templates here.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect process details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
- **Key** — snake_case or camelCase unique identifier. Becomes the BPMN process ID and the
|
||||
folder name.
|
||||
- **Name** — human-readable label.
|
||||
- **Description** — one sentence (optional).
|
||||
- **Who can start it?** — group key(s) for `activiti:candidateStarterGroups`.
|
||||
- **Steps** — the human tasks and which group handles each. A simple linear flow is enough.
|
||||
- **Needs a script task?** — automatic steps running a script between user tasks.
|
||||
- **Needs a mobile form?** — if yes, a `<key>-mobile.xml` is created too. Remember mobile is a
|
||||
different schema with explicit data wiring — variables, lookups and libs must be named
|
||||
up front (the mobile skill will ask).
|
||||
|
||||
## Step 3 — Scaffold
|
||||
|
||||
```bash
|
||||
vitruvio new process <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates `processes/<key>/<key>.bpmn`, `processes/<key>/<key>-desktop.xml`, and the
|
||||
`vitruvio.json` entry. Then replace the generated files using the focused skills:
|
||||
|
||||
1. **BPMN** — follow **vitruvio-criar-processo-bpmn** to write `processes/<key>/<key>.bpmn`
|
||||
from the user's steps/branches.
|
||||
2. **Desktop form** — follow **vitruvio-criar-form-desktop** (process variant) to write
|
||||
`processes/<key>/<key>-desktop.xml`, one `<form formKey>` per `activiti:formKey` in the BPMN.
|
||||
3. **Mobile form (if requested)** — follow **vitruvio-criar-form-mobile** (process variant) to
|
||||
write `processes/<key>/<key>-mobile.xml`. `vitruvio new` does not create it.
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the entry. Full shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"bpmn": "processes/<key>/<key>.bpmn",
|
||||
"forms": {
|
||||
"desktop": "processes/<key>/<key>-desktop.xml"
|
||||
},
|
||||
"schedules": []
|
||||
}
|
||||
```
|
||||
|
||||
- Add `"description"` if the user provided one — `vitruvio new` does not set it.
|
||||
- If a mobile form was created, add `"mobile": "processes/<key>/<key>-mobile.xml"` inside `"forms"`.
|
||||
- Add `schedules` only if the process runs on a timer (cron/simple interval).
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- Files created: `<key>.bpmn`, `<key>-desktop.xml` (and `<key>-mobile.xml` if applicable).
|
||||
- Registered in `vitruvio.json` with key `<key>`.
|
||||
- The process identity is the `<bpmn2:process id>` — it must match the key.
|
||||
- Each `activiti:formKey` in the BPMN must have a matching `<form formKey="...">` in **every**
|
||||
form file (desktop and mobile).
|
||||
- Submitted field `id="X"` in `formKey="formAbertura"` becomes process variable `formAbertura_X`
|
||||
(desktop auto-injects these; mobile must declare/fetch them — see vitruvio-criar-form-mobile).
|
||||
- For complex flows, recommend editing the BPMN in bpmn.io or Camunda Modeler before deploying.
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: vitruvio-criar-query
|
||||
description: >
|
||||
Use when the user wants to create a new named SQL query in a Vitruvio repository.
|
||||
Triggers: "create query", "new query", "criar query", "nova query", "add query", "named query",
|
||||
or any request to scaffold a queries/*.sql file and register it in vitruvio.json.
|
||||
---
|
||||
|
||||
# Create Vitruvio Query
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating a new named SQL query inside a Vitruvio repository. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect query details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing from their original request):
|
||||
|
||||
- **Key** — kebab-case unique identifier. Used to reference this query in reports, DBTable components, and scripts.
|
||||
- **Name** — human-readable label shown in the Vitruvio UI.
|
||||
- **SQL** — the query itself, or enough context to write it.
|
||||
- **Connection** — datasource name (default: `vitruvio_producao`). Ask only if the user mentions a specific datasource.
|
||||
|
||||
## Step 3 — Scaffold and create file
|
||||
|
||||
```bash
|
||||
vitruvio new query <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates `queries/<key>.sql` and registers it in `vitruvio.json` with `connection: "vitruvio_producao"`. Replace the generated SQL with the actual query. If a different datasource is needed, update `"connection"` in `vitruvio.json`.
|
||||
|
||||
Target path: `queries/<key>.sql`
|
||||
|
||||
Rules:
|
||||
- One `SELECT` per file. No multiple statements, no DDL, no INSERT/UPDATE/DELETE.
|
||||
- Named bind parameters use `:paramName` syntax — never concatenate user input into SQL.
|
||||
- Write ANSI SQL where possible. If DB-specific syntax is unavoidable, note it in a comment.
|
||||
- Keep Oracle and PostgreSQL compatibility in mind — avoid syntax that only works in one.
|
||||
|
||||
```sql
|
||||
SELECT col1,
|
||||
col2
|
||||
FROM my_table
|
||||
WHERE active = 1
|
||||
AND id = :id
|
||||
ORDER BY col1
|
||||
```
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the query entry. The full entry shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"source": "queries/<key>.sql",
|
||||
"connection": "vitruvio_producao"
|
||||
}
|
||||
```
|
||||
|
||||
Update `"connection"` only if the user specified a datasource other than `"vitruvio_producao"`.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created: `queries/<key>.sql`
|
||||
- Registered in `vitruvio.json` with key `<key>`
|
||||
- How it can be used: as a datasource in a report, in a DBTable component, or loaded in a script via `db.executeNamedQuery('<key>', params)`
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: vitruvio-criar-relatorio
|
||||
description: >
|
||||
Use when the user wants to register a new report in a Vitruvio repository.
|
||||
Triggers: "create report", "new report", "criar relatório", "novo relatório", "add report",
|
||||
MODELO_ESTATICO, DINAMICO_QUERY_SQL, "jrxml", or any request to scaffold a report entry.
|
||||
---
|
||||
|
||||
# Create Vitruvio Report
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are registering a new report inside a Vitruvio repository. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Identify the report type
|
||||
|
||||
Ask the user which type applies (if not already clear from the request):
|
||||
|
||||
| Type | When to use |
|
||||
|------|-------------|
|
||||
| **MODELO_ESTATICO** | A Jasper report whose layout is fully designed in a `.jrxml` file (Jaspersoft Studio). The dev provides or will provide the `.jrxml`. |
|
||||
| **DINAMICO_QUERY_SQL** | A Vitruvio-managed report where data comes from a named query and columns/layout are configured in `vitruvio.json`. The `.jrxml` is still needed but is simpler — Vitruvio drives the structure. |
|
||||
|
||||
## Step 3 — Collect details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing):
|
||||
|
||||
**Both types:**
|
||||
- **Key** — kebab-case or snake_case unique identifier.
|
||||
- **Name** — human-readable label shown in the UI.
|
||||
- **Category** — display category (e.g. `"Compras"`, `"Auditoria/Gondola"`).
|
||||
- **Owner** — Vitruvio username of the responsible person.
|
||||
- **Orientation** — `RETRATO` (portrait) or `PAISAGEM` (landscape). Default: `RETRATO`.
|
||||
- **Allowed groups / users** — who can access this report (can be empty arrays).
|
||||
- **Has parameters?** — does the user fill in parameters before running it? If yes, a params form is needed.
|
||||
|
||||
**DINAMICO_QUERY_SQL only:**
|
||||
- **Query key** — the named query that feeds the report (must be registered in `vitruvio.json`).
|
||||
- **Columns** — list of columns: name (DB column), label, alignment (`LEFT`/`CENTER`/`RIGHT`), width (px), aggregation (`null`, `SUM`, `COUNT`, etc.).
|
||||
|
||||
## Step 4 — Create the files
|
||||
|
||||
### MODELO_ESTATICO
|
||||
|
||||
Files live flat in `reports/`:
|
||||
- `reports/<key>.jrxml` — **do not generate this file**; tell the user to place the Jaspersoft-designed template here. Must target **JasperReports 6.21.2** — do not save with a newer version.
|
||||
- `reports/<key>-params.xml` — only if the report has parameters (follows the same Vaadin XML form schema as panels).
|
||||
|
||||
### DINAMICO_QUERY_SQL
|
||||
|
||||
Files live in a subdirectory:
|
||||
- `reports/<key>/template.jrxml` — **do not generate this file**; tell the user to place the template here.
|
||||
- `reports/<key>/params.xml` — only if the report has parameters.
|
||||
|
||||
## Step 5 — Register in vitruvio.json
|
||||
|
||||
Read `vitruvio.json`, find or create the `"reports"` array, and add the entry.
|
||||
|
||||
### MODELO_ESTATICO entry
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"type": "MODELO_ESTATICO",
|
||||
"category": "<category>",
|
||||
"owner": "<owner>",
|
||||
"template": "reports/<key>.jrxml",
|
||||
"parameterForm": "reports/<key>-params.xml",
|
||||
"orientation": "RETRATO",
|
||||
"allowedGroups": [],
|
||||
"allowedUsers": [],
|
||||
"columns": [],
|
||||
"schedules": []
|
||||
}
|
||||
```
|
||||
|
||||
Omit `"parameterForm"` if no params form.
|
||||
|
||||
### DINAMICO_QUERY_SQL entry
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"type": "DINAMICO_QUERY_SQL",
|
||||
"category": "<category>",
|
||||
"owner": "<owner>",
|
||||
"template": "reports/<key>/template.jrxml",
|
||||
"parameterForm": "reports/<key>/params.xml",
|
||||
"query": "<query-key>",
|
||||
"orientation": "RETRATO",
|
||||
"allowedGroups": [],
|
||||
"allowedUsers": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "COLUMN_NAME",
|
||||
"label": "Column Label",
|
||||
"align": "LEFT",
|
||||
"width": 100,
|
||||
"aggregation": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Omit `"parameterForm"` if no params form.
|
||||
|
||||
Preserve the existing file structure and all other entries. Write the updated `vitruvio.json` back.
|
||||
|
||||
## Step 6 — Report
|
||||
|
||||
Tell the user:
|
||||
- Entry registered in `vitruvio.json` with key `<key>` and type `<type>`
|
||||
- For MODELO_ESTATICO: remind them to place the `.jrxml` at `reports/<key>.jrxml`, designed in Jaspersoft Studio 6.21.2
|
||||
- For DINAMICO_QUERY_SQL: remind them to place the template at `reports/<key>/template.jrxml`
|
||||
- If a params form is needed: what file to create and that it follows the same Vaadin XML schema as panels
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: vitruvio-criar-script
|
||||
description: >
|
||||
Use when the user wants to create a new JavaScript script in a Vitruvio repository.
|
||||
Triggers: "create script", "new script", "criar script", "novo script", "add script",
|
||||
or any request to scaffold a scripts/*.js file and register it in vitruvio.json.
|
||||
---
|
||||
|
||||
# Create Vitruvio Script
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are creating a new script inside a Vitruvio repository. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect script details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing from their original request):
|
||||
|
||||
- **Key (sigla)** — unique identifier used in `libService.loadScript('key')`. Snake_case. Must be unique across all scripts in the repo. If the user already provided a name, suggest a key derived from it.
|
||||
- **Name** — human-readable label shown in the Vitruvio UI.
|
||||
- **Pattern** — which of the two patterns applies:
|
||||
- **Library** — reusable module, loaded by other scripts/endpoints/panels via `libService.loadScript`. Wrap in `({...})`.
|
||||
- **Process/task script** — runs directly from a process task or scheduler. Top-level execution, no export.
|
||||
- **Description** — one sentence about what this script does (optional, but ask if not provided).
|
||||
- **Domain** — `USUARIO` (user-level, default) or `SISTEMA` (system-level).
|
||||
|
||||
## Step 3 — Scaffold and create file
|
||||
|
||||
```bash
|
||||
vitruvio new script <key> --name "<name>"
|
||||
```
|
||||
|
||||
This creates `scripts/<key>.js` and registers it in `vitruvio.json` with `domain: "USUARIO"`. Replace the generated file with the appropriate template below. If the domain should be `SISTEMA`, update that field in `vitruvio.json`.
|
||||
|
||||
Target path: `scripts/<key>.js`
|
||||
|
||||
### Library template
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Nome: <name>
|
||||
* Sigla: <key>
|
||||
* Descrição: <description>
|
||||
*/
|
||||
({
|
||||
// example function — replace with actual implementation
|
||||
run: function(params) {
|
||||
var db = libService.loadScript('db');
|
||||
var banco = new db(db.VITRUVIO_DATASOURCE);
|
||||
|
||||
// implementation here
|
||||
|
||||
return {};
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Process / task script template
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Nome: <name>
|
||||
* Sigla: <key>
|
||||
* Descrição: <description>
|
||||
*/
|
||||
(function(execution) {
|
||||
var db = libService.loadScript('db');
|
||||
var banco = new db(db.VITRUVIO_DATASOURCE);
|
||||
|
||||
// implementation here
|
||||
})(execution)
|
||||
```
|
||||
|
||||
Rules (Rhino ES5 — no exceptions):
|
||||
- No `let`, `const`, arrow functions, template literals, destructuring, spread, `class`, or `import/export`
|
||||
- Use `var` everywhere
|
||||
- No `require`, `process`, `window`, or Node/browser globals
|
||||
- String concatenation with `+`, not template literals
|
||||
- `JSON.parse` / `JSON.stringify` for serialization
|
||||
|
||||
## Step 4 — Update vitruvio.json entry
|
||||
|
||||
`vitruvio new` already added the script entry. The full entry shape is:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description>",
|
||||
"language": "javascript",
|
||||
"domain": "USUARIO",
|
||||
"source": "scripts/<key>.js"
|
||||
}
|
||||
```
|
||||
|
||||
Update only what differs: change `"domain"` to `"SISTEMA"` if needed; add `"description"` if provided.
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- File created: `scripts/<key>.js`
|
||||
- Registered in `vitruvio.json` with key `<key>`
|
||||
- How to load it from another script or endpoint: `var lib = libService.loadScript('<key>');`
|
||||
@@ -0,0 +1,178 @@
|
||||
---
|
||||
name: vitruvio-registrar-artefato
|
||||
description: >
|
||||
Use when the user has an existing file they want to add to a Vitruvio repository.
|
||||
Triggers: "I have an existing file", "move this to panels/", "register this artifact",
|
||||
"add this already existing", "tenho um arquivo pronto", "mover para o repo",
|
||||
or any request where the source file already exists and needs to be placed in the
|
||||
correct repo directory and registered in vitruvio.json.
|
||||
Do NOT use vitruvio-criar-* skills for this — those scaffold new files and would overwrite the existing one.
|
||||
---
|
||||
|
||||
# Register Existing Artifact
|
||||
|
||||
> All messages shown to the user must be written in Portuguese.
|
||||
|
||||
You are wiring an already-existing file into a Vitruvio repository. No scaffolding — the file exists and must be placed correctly then registered in `vitruvio.json`. Follow these steps in order.
|
||||
|
||||
## Step 1 — Confirm you are inside a Vitruvio repo
|
||||
|
||||
```bash
|
||||
ls vitruvio.json 2>/dev/null && echo "OK" || echo "NOT_A_VITRUVIO_REPO"
|
||||
```
|
||||
|
||||
If `NOT_A_VITRUVIO_REPO`, stop and tell the user to `cd` into the correct repo.
|
||||
|
||||
## Step 2 — Collect details
|
||||
|
||||
Ask the user (in a single message, only ask what is missing from their original request):
|
||||
|
||||
- **Source file path** — where the file currently lives (absolute or relative).
|
||||
- **Artifact type** — one of: `panel`, `script`, `endpoint`, `query`, `report`, `library`, `process`.
|
||||
- **Key** — kebab-case unique identifier for this artifact. Must be unique within its section in `vitruvio.json`. Changing it later is a breaking change.
|
||||
- **Name** — human-readable label shown in the Vitruvio UI.
|
||||
- Type-specific fields (only ask what is relevant):
|
||||
- Panel: `category` (e.g. `"Base de Conhecimento"`), `openInNewWindow` (default `true`), `showInMobileList` (default `false`), mobile form path if one also exists.
|
||||
- Script: `domain` — `USUARIO` (default) or `SISTEMA`.
|
||||
- Endpoint: `authMode` — `PUBLIC` (default), `STATIC_TOKEN`, `VITRUVIO_WS_USER_AUTH`, or `HTTP_BASIC_AUTH`.
|
||||
- Query: `connection` — datasource name (default `vitruvio_producao`).
|
||||
- Report: `type` — `MODELO_ESTATICO` or `DINAMICO_QUERY_SQL`; `query` key; `template` path; `parameterForm` path (optional).
|
||||
- Library: `authMode` — `PUBLIC` (default); `mobileEnabled` (default `false`).
|
||||
- Process: path to the BPMN file and desktop form XML if they are separate files.
|
||||
|
||||
## Step 3 — Place the file(s)
|
||||
|
||||
Create the destination directory if it does not exist, then move the source file to the correct location. **Never call `vitruvio new`** — it would scaffold and overwrite the existing file.
|
||||
|
||||
| Artifact | Destination | Notes |
|
||||
|----------|-------------|-------|
|
||||
| `panel` | `panels/<key>/<key>-desktop.xml` | Rename the source desktop form to `<key>-desktop.xml`. If a mobile form also exists, place it at `panels/<key>/<key>-mobile.xml`. |
|
||||
| `script` | `scripts/<key>.js` | |
|
||||
| `endpoint` | `endpoints/<key>.js` | |
|
||||
| `query` | `queries/<key>.sql` | |
|
||||
| `report` | `reports/<key>/` | Move jrxml and optional params.xml into this directory. |
|
||||
| `library` | `libraries/<key>/` | Move all files into this directory. |
|
||||
| `process` | `processes/<key>/` | Move the .bpmn and form XML(s) into this directory. |
|
||||
|
||||
```bash
|
||||
# Example: panel
|
||||
mkdir -p panels/<key>
|
||||
mv /path/to/source.xml panels/<key>/<key>-desktop.xml
|
||||
```
|
||||
|
||||
## Step 4 — Add entry to vitruvio.json
|
||||
|
||||
Read `vitruvio.json`. If the relevant section array does not exist, create it. Append the entry for the artifact type. Do **not** remove or modify any existing entries.
|
||||
|
||||
### Panel
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description or omit>",
|
||||
"category": "<category>",
|
||||
"displayOrder": 10,
|
||||
"showInPresentation": false,
|
||||
"openInNewWindow": true,
|
||||
"showInMobileList": false,
|
||||
"displayTimeInSeconds": 0,
|
||||
"allowedGroups": [],
|
||||
"allowedUsers": [],
|
||||
"forms": {
|
||||
"desktop": "panels/<key>/<key>-desktop.xml",
|
||||
"mobile": null,
|
||||
"mobileAlternative": null
|
||||
},
|
||||
"defaultState": null,
|
||||
"thumbnail": null
|
||||
}
|
||||
```
|
||||
|
||||
### Script
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description or omit>",
|
||||
"language": "javascript",
|
||||
"domain": "USUARIO",
|
||||
"source": "scripts/<key>.js"
|
||||
}
|
||||
```
|
||||
|
||||
### Endpoint
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description or omit>",
|
||||
"language": "javascript",
|
||||
"authMode": "PUBLIC",
|
||||
"active": true,
|
||||
"source": "endpoints/<key>.js"
|
||||
}
|
||||
```
|
||||
|
||||
### Query
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"connection": "vitruvio_producao",
|
||||
"source": "queries/<key>.sql"
|
||||
}
|
||||
```
|
||||
|
||||
### Report
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description or omit>",
|
||||
"type": "DINAMICO_QUERY_SQL",
|
||||
"category": "<category or omit>",
|
||||
"owner": null,
|
||||
"template": "reports/<key>/template.jrxml",
|
||||
"parameterForm": "reports/<key>/params.xml",
|
||||
"query": "<query-key>",
|
||||
"orientation": "RETRATO",
|
||||
"allowedGroups": [],
|
||||
"allowedUsers": []
|
||||
}
|
||||
```
|
||||
|
||||
### Library
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"type": "LOCAL",
|
||||
"authMode": "PUBLIC",
|
||||
"authToken": null,
|
||||
"mobileEnabled": false,
|
||||
"files": "libraries/<key>/"
|
||||
}
|
||||
```
|
||||
|
||||
### Process
|
||||
```json
|
||||
{
|
||||
"key": "<key>",
|
||||
"name": "<name>",
|
||||
"description": "<description or omit>",
|
||||
"bpmn": "processes/<key>/<key>.bpmn",
|
||||
"forms": {
|
||||
"desktop": "processes/<key>/<key>-desktop.xml",
|
||||
"mobile": null,
|
||||
"mobileAlternative": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5 — Report
|
||||
|
||||
Tell the user:
|
||||
- Where the file was placed
|
||||
- Registered in `vitruvio.json` under section `<type>` with key `<key>`
|
||||
- Which fields were left at defaults and may need updating (e.g. `category`, `allowedGroups`, `description`)
|
||||
- Reminder: keys are stable identifiers — changing them later is a breaking change
|
||||
Reference in New Issue
Block a user