@meistrari/minuta-react 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +47 -17
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -80,10 +80,12 @@ Tudo pelo seu backend, com `x-api-key` + `x-org-id` — seus usuários não prec
80
80
  de conta no defesa.ai:
81
81
 
82
82
  ```
83
- GET /data/case/:id/html → { html, chrome, margins, source, editionId }
84
- POST /data/case/:id/editions { name, html } → cria a edição (o documento lapidado)
85
- PATCH /data/case/:id/editions/:editionId { html } → salva (versão automática)
86
- POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
83
+ GET /data/case/:id/html → { html, chrome, margins, source, editionId, variablesMarked, markedVariables }
84
+ POST /data/case/:id/editions { name, html } → cria a edição → { id, updatedAt, … }
85
+ PATCH /data/case/:id/editions/:id → salva → { updatedAt, updatedBy, … }
86
+ { html, baseUpdatedAt } 400 sem o token; 409 se outro salvou antes
87
+ POST /data/case/:id/editions/:id/versions → marco no histórico (o PATCH NÃO cria versão)
88
+ POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
87
89
  ```
88
90
 
89
91
  ```tsx
@@ -91,15 +93,19 @@ POST /data/case/:id/docx → .docx do estado atual (a e
91
93
  const doc = await api.get(`/case/${caseId}/html`)
92
94
  <MinutaEditor key={caseId} html={doc.html} chrome={doc.chrome ?? undefined} margins={doc.margins ?? undefined} onChange={setHtml} />
93
95
 
94
- // salvar (debounce do seu lado)
95
- await api.patch(`/case/${caseId}/editions/${editionId}`, { html })
96
+ // salvar (debounce do seu lado): baseUpdatedAt = o updatedAt da última resposta
97
+ const saved = await api.patch(`/case/${caseId}/editions/${editionId}`, { html, baseUpdatedAt })
98
+ baseUpdatedAt = saved.updatedAt
96
99
 
97
100
  // baixar — fiel ao preview, gerado no browser
98
101
  const blob = await editor.current!.exportDocx()
99
102
  ```
100
103
 
101
- Mande `x-user-email` / `x-user-name` nas chamadas de edição para a autoria
102
- ficar registrada.
104
+ Mande `x-user-email` / `x-user-name` em toda chamada feita em nome de alguém:
105
+ nas rotas de edição viram `createdBy` (criação), `updatedBy` (último save de
106
+ conteúdo), `createdBy` de cada versão e `deletedBy` (descarte); no
107
+ `PATCH /data/case/:id` viram `updatedByEmail`/`updatedByName` do case. Sem os
108
+ headers a autoria fica `null` — nunca "a integração".
103
109
 
104
110
  ## Formulário ↔ documento
105
111
 
@@ -109,8 +115,9 @@ e o editor trata a marca como nó atômico. Isso dá as duas direções sem casa
109
115
  texto (que erra quando o valor coincide com algo que já existia no template):
110
116
 
111
117
  ```tsx
112
- // documento → formulário: clicou no trecho, foca o campo
113
- <MinutaEditor ref={editor} html={html} onVariableClick={v => focusField(v.variableId)} />
118
+ // documento → formulário: clicou no trecho, foca o campo. `kind` diz o que foi
119
+ // clicado: 'text' (chip), 'image' (<img> marcada) ou 'missingImage' (aviso)
120
+ <MinutaEditor ref={editor} html={html} onVariableClick={v => focusField(v.variableId, v.kind)} />
114
121
 
115
122
  // formulário → documento: focou o campo, rola até o trecho
116
123
  editor.current!.scrollToVariable('nome_cliente')
@@ -135,17 +142,23 @@ edição criada a partir de HTML já resolvido (sem marcas) a ponte não existe,
135
142
  | `<for item="c" in="contratos"> <variable name="c.numero">` | um chip por linha, com `data-variable-index` | `contratos[0].numero`, `contratos[1].numero`… |
136
143
  | `<img variable="img_doc">` com valor | `<img data-variable-id data-parent-block-id>` | `img_doc` |
137
144
  | `<img variable="img_doc">` sem valor | aviso `missing-image-warning` (nó `missingImage` no editor) | `img_doc` |
145
+ | `<variable name="valor">` sem valor | aviso vermelho `<span data-variable-id data-parent-block-id data-invalid-variable>` | `valor` |
138
146
  | cada bloco do template | `<div data-minuta-block data-block-id data-block-title>` | `data-block-id` = id do bloco no `templateSpec` |
139
147
 
140
- Variável de texto **sem valor** sai como aviso (`data-invalid-variable`), não
141
- como chip, e não entra em `markedVariables`. Booleans não viram marca: eles
142
- decidem se um trecho ou bloco é emitido; a regra fica em `displayCondition` e
143
- nos `<if condition>` do template (podem envolver várias variáveis).
148
+ O aviso de texto sem valor é uma marca como as outras: entra em
149
+ `markedVariables`, dispara `onVariableClick` (`kind: 'text'`) e
150
+ `setVariableValue` o converte em chip comum ao receber valor. Booleans não
151
+ viram marca: eles decidem se um trecho ou bloco é emitido; a regra fica em
152
+ `displayCondition` e nos `<if condition>` do template (podem envolver várias
153
+ variáveis).
144
154
 
145
155
  ```tsx
146
156
  // array: uma linha de cada vez
147
157
  editor.current!.setVariableValue('contratos[1].numero', '375586874')
148
158
 
159
+ // texto sem valor: o aviso vermelho vira chip
160
+ editor.current!.setVariableValue('valor', 'R$ 1.000,00')
161
+
149
162
  // imagem: troca a existente, ou transforma o aviso de "faltando" em imagem
150
163
  editor.current!.setVariableImage('img_doc', urlAssinada) // → nº de ocorrências
151
164
  editor.current!.scrollToVariable('img_faltando') // acha o aviso também
@@ -155,11 +168,28 @@ editor.current!.scrollToVariable('img_faltando') // acha o aviso
155
168
  ```
156
169
 
157
170
  Sobre o fluxo de "enviar variáveis de volta": com uma edição viva o documento
158
- **é** a edição — o `PATCH` de variáveis no case não re-renderiza a peça (senão
159
- apagaria o texto do advogado). Ou o formulário trava quando a lapidação começa
160
- (o modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
171
+ **é** a edição — o `PATCH /data/case/:id` de variáveis não re-renderiza a peça
172
+ (senão apagaria o texto do advogado). A API aceita a mudança e devolve
173
+ `activeEdition` (`{ id, name, updatedAt }` da edição que ficou como estava, ou
174
+ `null`) para o app decidir: ou o formulário trava quando a lapidação começa (o
175
+ modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
161
176
  salva a edição. Decisão de UX do seu app; as duas funcionam.
162
177
 
178
+ ### Imagens inseridas pelo advogado
179
+
180
+ `uploadImage` devolve `{ src, vaultSrc }`: o editor grava a tag com o `src`
181
+ (presignado, expira) e `data-vault-src` (a `vault://`, durável). Ao reabrir uma
182
+ edição salva, renove os links antes de mostrar:
183
+
184
+ ```tsx
185
+ // resolver: POST /data/vault/download-url { vaultUrl } → { downloadUrl } (pelo seu backend)
186
+ await editor.current!.refreshVaultImages(vaultUrl => api.post('/vault/download-url', { vaultUrl }).then(r => r.downloadUrl))
187
+ ```
188
+
189
+ Não entra no undo nem dispara `onChange`. O `exportDocx()` do browser baixa
190
+ pelo `src`, então renove antes de exportar também; o `POST /data/case/:id/docx`
191
+ resolve `vault://` sozinho.
192
+
163
193
  ## Next.js (App Router)
164
194
 
165
195
  O entry publicado carrega `'use client'`: dá para importar o componente direto
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/minuta-react",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Componente React do editor de minuta do defesa.ai — casca fina sobre @meistrari/minuta-editor",
5
5
  "repository": {
6
6
  "type": "git",