@meistrari/minuta-react 1.1.0 → 1.3.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.
- package/README.md +68 -17
- package/dist/index.d.mts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.mjs +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ um `Blob` de `.docx`. Buscar o documento, persistir a edição e baixar o arquiv
|
|
|
35
35
|
| `chrome` | — | `{ header, footer, headerScope, footerScope, showPageNumber }` |
|
|
36
36
|
| `variables` | — | catálogo `{ key, label, description?, group? }[]` → liga o modo template |
|
|
37
37
|
| `uploadImage` | — | `(file) => Promise<{ src, vaultSrc } \| null>` |
|
|
38
|
+
| `imageFallback` | `'dataUri'` | `'none'` = upload falhou → não insere nada, só `onImageUploadError` |
|
|
39
|
+
| `onImageUploadError` | — | `(error, file) => void` |
|
|
38
40
|
| `toolbar` | `true` | `false` = você monta a sua sobre `ref.current.tiptap` |
|
|
39
41
|
| `blockClickWhileEditing` | `false` | dispara `onBlockClick` também em edição (padrão: só em leitura) |
|
|
40
42
|
| `onChange` | — | `(html) => void`, a cada edição |
|
|
@@ -80,26 +82,40 @@ Tudo pelo seu backend, com `x-api-key` + `x-org-id` — seus usuários não prec
|
|
|
80
82
|
de conta no defesa.ai:
|
|
81
83
|
|
|
82
84
|
```
|
|
83
|
-
GET /data/case/:id/html
|
|
84
|
-
POST /data/case/:id/editions { name, html }
|
|
85
|
-
PATCH /data/case/:id/editions/:
|
|
86
|
-
|
|
85
|
+
GET /data/case/:id/html → { html, chrome, margins, source, editionId, variablesMarked, markedVariables }
|
|
86
|
+
POST /data/case/:id/editions { name, html } → cria a edição → { id, updatedAt, contentHash, … }
|
|
87
|
+
PATCH /data/case/:id/editions/:id → salva → { updatedAt, updatedBy, contentHash, … }
|
|
88
|
+
{ html, baseUpdatedAt } 400 sem o token; 409 se outro salvou antes
|
|
89
|
+
GET /data/case/:id/editions → [{ id, updatedAt, updatedBy, contentHash, … }] sem html
|
|
90
|
+
POST /data/case/:id/editions/:id/versions → marco no histórico (o PATCH NÃO cria versão)
|
|
91
|
+
{ comment?, baseUpdatedAt? } com baseUpdatedAt: 409 se a revisão mudou; já versionada → 200 reused
|
|
92
|
+
POST /data/case/:id/docx → .docx do estado atual (a edição, se existir)
|
|
87
93
|
```
|
|
88
94
|
|
|
95
|
+
`contentHash` é o sha256 do `html` exatamente como foi gravado (o servidor não
|
|
96
|
+
normaliza): num save que deu timeout, compare com o hash do que você enviou
|
|
97
|
+
pela listagem, sem baixar o documento. `baseUpdatedAt` no `POST …/versions`
|
|
98
|
+
amarra o checkpoint à revisão que você viu: a versão nunca sai com o seu nome
|
|
99
|
+
e o texto que outra pessoa salvou depois, e repetir a chamada não duplica.
|
|
100
|
+
|
|
89
101
|
```tsx
|
|
90
102
|
// abrir
|
|
91
103
|
const doc = await api.get(`/case/${caseId}/html`)
|
|
92
104
|
<MinutaEditor key={caseId} html={doc.html} chrome={doc.chrome ?? undefined} margins={doc.margins ?? undefined} onChange={setHtml} />
|
|
93
105
|
|
|
94
|
-
// salvar (debounce do seu lado)
|
|
95
|
-
await api.patch(`/case/${caseId}/editions/${editionId}`, { html })
|
|
106
|
+
// salvar (debounce do seu lado): baseUpdatedAt = o updatedAt da última resposta
|
|
107
|
+
const saved = await api.patch(`/case/${caseId}/editions/${editionId}`, { html, baseUpdatedAt })
|
|
108
|
+
baseUpdatedAt = saved.updatedAt
|
|
96
109
|
|
|
97
110
|
// baixar — fiel ao preview, gerado no browser
|
|
98
111
|
const blob = await editor.current!.exportDocx()
|
|
99
112
|
```
|
|
100
113
|
|
|
101
|
-
Mande `x-user-email` / `x-user-name`
|
|
102
|
-
|
|
114
|
+
Mande `x-user-email` / `x-user-name` em toda chamada feita em nome de alguém:
|
|
115
|
+
nas rotas de edição viram `createdBy` (criação), `updatedBy` (último save de
|
|
116
|
+
conteúdo), `createdBy` de cada versão e `deletedBy` (descarte); no
|
|
117
|
+
`PATCH /data/case/:id` viram `updatedByEmail`/`updatedByName` do case. Sem os
|
|
118
|
+
headers a autoria fica `null` — nunca "a integração".
|
|
103
119
|
|
|
104
120
|
## Formulário ↔ documento
|
|
105
121
|
|
|
@@ -109,8 +125,9 @@ e o editor trata a marca como nó atômico. Isso dá as duas direções sem casa
|
|
|
109
125
|
texto (que erra quando o valor coincide com algo que já existia no template):
|
|
110
126
|
|
|
111
127
|
```tsx
|
|
112
|
-
// documento → formulário: clicou no trecho, foca o campo
|
|
113
|
-
|
|
128
|
+
// documento → formulário: clicou no trecho, foca o campo. `kind` diz o que foi
|
|
129
|
+
// clicado: 'text' (chip), 'image' (<img> marcada) ou 'missingImage' (aviso)
|
|
130
|
+
<MinutaEditor ref={editor} html={html} onVariableClick={v => focusField(v.variableId, v.kind)} />
|
|
114
131
|
|
|
115
132
|
// formulário → documento: focou o campo, rola até o trecho
|
|
116
133
|
editor.current!.scrollToVariable('nome_cliente')
|
|
@@ -135,17 +152,23 @@ edição criada a partir de HTML já resolvido (sem marcas) a ponte não existe,
|
|
|
135
152
|
| `<for item="c" in="contratos"> <variable name="c.numero">` | um chip por linha, com `data-variable-index` | `contratos[0].numero`, `contratos[1].numero`… |
|
|
136
153
|
| `<img variable="img_doc">` com valor | `<img data-variable-id data-parent-block-id>` | `img_doc` |
|
|
137
154
|
| `<img variable="img_doc">` sem valor | aviso `missing-image-warning` (nó `missingImage` no editor) | `img_doc` |
|
|
155
|
+
| `<variable name="valor">` sem valor | aviso vermelho `<span data-variable-id data-parent-block-id data-invalid-variable>` | `valor` |
|
|
138
156
|
| cada bloco do template | `<div data-minuta-block data-block-id data-block-title>` | `data-block-id` = id do bloco no `templateSpec` |
|
|
139
157
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
158
|
+
O aviso de texto sem valor é uma marca como as outras: entra em
|
|
159
|
+
`markedVariables`, dispara `onVariableClick` (`kind: 'text'`) e
|
|
160
|
+
`setVariableValue` o converte em chip comum ao receber valor. Booleans não
|
|
161
|
+
viram marca: eles decidem se um trecho ou bloco é emitido; a regra fica em
|
|
162
|
+
`displayCondition` e nos `<if condition>` do template (podem envolver várias
|
|
163
|
+
variáveis).
|
|
144
164
|
|
|
145
165
|
```tsx
|
|
146
166
|
// array: uma linha de cada vez
|
|
147
167
|
editor.current!.setVariableValue('contratos[1].numero', '375586874')
|
|
148
168
|
|
|
169
|
+
// texto sem valor: o aviso vermelho vira chip
|
|
170
|
+
editor.current!.setVariableValue('valor', 'R$ 1.000,00')
|
|
171
|
+
|
|
149
172
|
// imagem: troca a existente, ou transforma o aviso de "faltando" em imagem
|
|
150
173
|
editor.current!.setVariableImage('img_doc', urlAssinada) // → nº de ocorrências
|
|
151
174
|
editor.current!.scrollToVariable('img_faltando') // acha o aviso também
|
|
@@ -155,11 +178,39 @@ editor.current!.scrollToVariable('img_faltando') // acha o aviso
|
|
|
155
178
|
```
|
|
156
179
|
|
|
157
180
|
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
|
|
159
|
-
apagaria o texto do advogado).
|
|
160
|
-
(
|
|
181
|
+
**é** a edição — o `PATCH /data/case/:id` de variáveis não re-renderiza a peça
|
|
182
|
+
(senão apagaria o texto do advogado). A API aceita a mudança e devolve
|
|
183
|
+
`activeEdition` (`{ id, name, updatedAt }` da edição que ficou como estava, ou
|
|
184
|
+
`null`) para o app decidir: ou o formulário trava quando a lapidação começa (o
|
|
185
|
+
modelo do defesa.ai), ou você mantém os dois vivos com `setVariableValue` +
|
|
161
186
|
salva a edição. Decisão de UX do seu app; as duas funcionam.
|
|
162
187
|
|
|
188
|
+
### Imagens inseridas pelo advogado
|
|
189
|
+
|
|
190
|
+
`uploadImage` devolve `{ src, vaultSrc }`: o editor grava a tag com o `src`
|
|
191
|
+
(presignado, expira) e `data-vault-src` (a `vault://`, durável). Persista o
|
|
192
|
+
`getHTML()` como está: o `POST /data/case/:id/docx` usa o `data-vault-src` e
|
|
193
|
+
resolve `vault://` sozinho. Ao reabrir uma edição salva, renove os links antes
|
|
194
|
+
de mostrar:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
// resolver: POST /data/vault/download-url { vaultUrl } → { downloadUrl } (pelo seu backend)
|
|
198
|
+
await editor.current!.refreshVaultImages(vaultUrl => api.post('/vault/download-url', { vaultUrl }).then(r => r.downloadUrl))
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Renova tanto `data-vault-src` quanto imagens salvas só com `src="vault://…"`
|
|
202
|
+
(que ganham o `data-vault-src`), mantendo a referência durável. Não entra no
|
|
203
|
+
undo nem dispara `onChange`: chame `getHTML()` depois se guarda uma cópia. O
|
|
204
|
+
`exportDocx()` do browser baixa pelo `src`, então renove antes de exportar.
|
|
205
|
+
|
|
206
|
+
Se o upload falhar (ou `uploadImage` devolver `null`), o padrão é embutir a
|
|
207
|
+
imagem em base64. Um app cujo documento canônico depende de `vault://` não
|
|
208
|
+
quer isso: `imageFallback="none"` não insere nada e `onImageUploadError` recebe
|
|
209
|
+
o erro e o arquivo para você mostrar e deixar tentar de novo.
|
|
210
|
+
|
|
211
|
+
`setVariableImage(id, src, vaultSrc)` grava o `data-vault-src` junto — use a
|
|
212
|
+
forma de três argumentos para a imagem sobreviver à expiração do `src`.
|
|
213
|
+
|
|
163
214
|
## Next.js (App Router)
|
|
164
215
|
|
|
165
216
|
O entry publicado carrega `'use client'`: dá para importar o componente direto
|
package/dist/index.d.mts
CHANGED
|
@@ -32,6 +32,9 @@ interface MinutaEditorProps {
|
|
|
32
32
|
chrome?: Partial<MinutaChromeText>;
|
|
33
33
|
variables?: MinutaVariableCatalog;
|
|
34
34
|
uploadImage?: MinutaEditorOptions['uploadImage'];
|
|
35
|
+
/** `'dataUri'` (default) embute em base64 quando o upload falha; `'none'` não insere e avisa por `onImageUploadError` */
|
|
36
|
+
imageFallback?: MinutaEditorOptions['imageFallback'];
|
|
37
|
+
onImageUploadError?: MinutaEditorOptions['onImageUploadError'];
|
|
35
38
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
39
|
toolbar?: boolean;
|
|
37
40
|
showBlockOrigin?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ interface MinutaEditorProps {
|
|
|
32
32
|
chrome?: Partial<MinutaChromeText>;
|
|
33
33
|
variables?: MinutaVariableCatalog;
|
|
34
34
|
uploadImage?: MinutaEditorOptions['uploadImage'];
|
|
35
|
+
/** `'dataUri'` (default) embute em base64 quando o upload falha; `'none'` não insere e avisa por `onImageUploadError` */
|
|
36
|
+
imageFallback?: MinutaEditorOptions['imageFallback'];
|
|
37
|
+
onImageUploadError?: MinutaEditorOptions['onImageUploadError'];
|
|
35
38
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
39
|
toolbar?: boolean;
|
|
37
40
|
showBlockOrigin?: boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -21,6 +21,8 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
21
21
|
variables: props.variables,
|
|
22
22
|
// lido na hora do upload, não no mount: o host pode trocar a função
|
|
23
23
|
uploadImage: (file) => callbacks.current.uploadImage ? callbacks.current.uploadImage(file) : Promise.resolve(null),
|
|
24
|
+
imageFallback: props.imageFallback,
|
|
25
|
+
onImageUploadError: (error, file) => callbacks.current.onImageUploadError?.(error, file),
|
|
24
26
|
toolbar: props.toolbar,
|
|
25
27
|
showBlockOrigin: props.showBlockOrigin,
|
|
26
28
|
blockClickWhileEditing: props.blockClickWhileEditing,
|
|
@@ -64,6 +66,9 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
64
66
|
useEffect(() => {
|
|
65
67
|
instance.current?.setBlockClickWhileEditing(props.blockClickWhileEditing === true);
|
|
66
68
|
}, [props.blockClickWhileEditing]);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
instance.current?.setImageFallback(props.imageFallback ?? "dataUri");
|
|
71
|
+
}, [props.imageFallback]);
|
|
67
72
|
useImperativeHandle(ref, () => instance.current);
|
|
68
73
|
return createElement("div", {
|
|
69
74
|
ref: host,
|