@meistrari/minuta-react 1.0.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.
- package/README.md +75 -14
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,13 +36,16 @@ um `Blob` de `.docx`. Buscar o documento, persistir a edição e baixar o arquiv
|
|
|
36
36
|
| `variables` | — | catálogo `{ key, label, description?, group? }[]` → liga o modo template |
|
|
37
37
|
| `uploadImage` | — | `(file) => Promise<{ src, vaultSrc } \| null>` |
|
|
38
38
|
| `toolbar` | `true` | `false` = você monta a sua sobre `ref.current.tiptap` |
|
|
39
|
+
| `blockClickWhileEditing` | `false` | dispara `onBlockClick` também em edição (padrão: só em leitura) |
|
|
39
40
|
| `onChange` | — | `(html) => void`, a cada edição |
|
|
40
41
|
| `onTransaction` | — | qualquer transação, inclusive só de seleção — para toolbar própria |
|
|
41
42
|
| `onReady`, `onEditChrome`, `onBlockClick`, `onError` | — | |
|
|
42
43
|
|
|
43
44
|
O `ref` entrega a instância do editor: `getHTML()`, `exportDocx()` (→ `Blob`),
|
|
44
45
|
`usedVariables()`, `setEditable()`, `setChrome()`, `tiptap` (a instância do
|
|
45
|
-
Tiptap — escotilha de fuga), `root`/`container`/`sheet` (os elementos)
|
|
46
|
+
Tiptap — escotilha de fuga), `root`/`container`/`sheet` (os elementos) — e a
|
|
47
|
+
ponte formulário ↔ documento: `scrollToVariable()`, `setVariableValue()`,
|
|
48
|
+
`setVariableImage()`, `variablesInDocument()` (abaixo).
|
|
46
49
|
|
|
47
50
|
### Por que o `html` é lido uma vez
|
|
48
51
|
|
|
@@ -77,10 +80,12 @@ Tudo pelo seu backend, com `x-api-key` + `x-org-id` — seus usuários não prec
|
|
|
77
80
|
de conta no defesa.ai:
|
|
78
81
|
|
|
79
82
|
```
|
|
80
|
-
GET /data/case/:id/html
|
|
81
|
-
POST /data/case/:id/editions { name, html }
|
|
82
|
-
PATCH /data/case/:id/editions/:
|
|
83
|
-
|
|
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)
|
|
84
89
|
```
|
|
85
90
|
|
|
86
91
|
```tsx
|
|
@@ -88,15 +93,19 @@ POST /data/case/:id/docx → .docx do estado atual (a e
|
|
|
88
93
|
const doc = await api.get(`/case/${caseId}/html`)
|
|
89
94
|
<MinutaEditor key={caseId} html={doc.html} chrome={doc.chrome ?? undefined} margins={doc.margins ?? undefined} onChange={setHtml} />
|
|
90
95
|
|
|
91
|
-
// salvar (debounce do seu lado)
|
|
92
|
-
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
|
|
93
99
|
|
|
94
100
|
// baixar — fiel ao preview, gerado no browser
|
|
95
101
|
const blob = await editor.current!.exportDocx()
|
|
96
102
|
```
|
|
97
103
|
|
|
98
|
-
Mande `x-user-email` / `x-user-name`
|
|
99
|
-
|
|
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".
|
|
100
109
|
|
|
101
110
|
## Formulário ↔ documento
|
|
102
111
|
|
|
@@ -106,8 +115,9 @@ e o editor trata a marca como nó atômico. Isso dá as duas direções sem casa
|
|
|
106
115
|
texto (que erra quando o valor coincide com algo que já existia no template):
|
|
107
116
|
|
|
108
117
|
```tsx
|
|
109
|
-
// documento → formulário: clicou no trecho, foca o campo
|
|
110
|
-
|
|
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)} />
|
|
111
121
|
|
|
112
122
|
// formulário → documento: focou o campo, rola até o trecho
|
|
113
123
|
editor.current!.scrollToVariable('nome_cliente')
|
|
@@ -123,12 +133,63 @@ edição criada a partir de HTML já resolvido (sem marcas) a ponte não existe,
|
|
|
123
133
|
é melhor saber antes de montar a tela do que descobrir com
|
|
124
134
|
`variablesInDocument()` vazio.
|
|
125
135
|
|
|
136
|
+
### O que é marcado, e com que id
|
|
137
|
+
|
|
138
|
+
| no template | no HTML | id da marca |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `<variable name="comarca">` | `<span data-variable-id data-parent-block-id>` (chip) | `comarca` |
|
|
141
|
+
| variável `compute` | idem — o valor é calculado antes do render | o id da compute |
|
|
142
|
+
| `<for item="c" in="contratos"> <variable name="c.numero">` | um chip por linha, com `data-variable-index` | `contratos[0].numero`, `contratos[1].numero`… |
|
|
143
|
+
| `<img variable="img_doc">` com valor | `<img data-variable-id data-parent-block-id>` | `img_doc` |
|
|
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` |
|
|
146
|
+
| cada bloco do template | `<div data-minuta-block data-block-id data-block-title>` | `data-block-id` = id do bloco no `templateSpec` |
|
|
147
|
+
|
|
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).
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// array: uma linha de cada vez
|
|
157
|
+
editor.current!.setVariableValue('contratos[1].numero', '375586874')
|
|
158
|
+
|
|
159
|
+
// texto sem valor: o aviso vermelho vira chip
|
|
160
|
+
editor.current!.setVariableValue('valor', 'R$ 1.000,00')
|
|
161
|
+
|
|
162
|
+
// imagem: troca a existente, ou transforma o aviso de "faltando" em imagem
|
|
163
|
+
editor.current!.setVariableImage('img_doc', urlAssinada) // → nº de ocorrências
|
|
164
|
+
editor.current!.scrollToVariable('img_faltando') // acha o aviso também
|
|
165
|
+
|
|
166
|
+
// bloco: com blockClickWhileEditing o clique em edição também avisa
|
|
167
|
+
<MinutaEditor blockClickWhileEditing onBlockClick={id => openChecklistBlock(id)} … />
|
|
168
|
+
```
|
|
169
|
+
|
|
126
170
|
Sobre o fluxo de "enviar variáveis de volta": com uma edição viva o documento
|
|
127
|
-
**é** a edição — o `PATCH` de variáveis
|
|
128
|
-
apagaria o texto do advogado).
|
|
129
|
-
(
|
|
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` +
|
|
130
176
|
salva a edição. Decisão de UX do seu app; as duas funcionam.
|
|
131
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
|
+
|
|
132
193
|
## Next.js (App Router)
|
|
133
194
|
|
|
134
195
|
O entry publicado carrega `'use client'`: dá para importar o componente direto
|
package/dist/index.d.mts
CHANGED
|
@@ -35,6 +35,8 @@ interface MinutaEditorProps {
|
|
|
35
35
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
36
|
toolbar?: boolean;
|
|
37
37
|
showBlockOrigin?: boolean;
|
|
38
|
+
/** dispara `onBlockClick` também em edição (padrão: só em leitura) */
|
|
39
|
+
blockClickWhileEditing?: boolean;
|
|
38
40
|
onReady?: MinutaEditorOptions['onReady'];
|
|
39
41
|
/** o documento mudou por edição — recebe o HTML atual */
|
|
40
42
|
onChange?: (html: string) => void;
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ interface MinutaEditorProps {
|
|
|
35
35
|
/** barra de formatação do pacote (default: true). `false` = monte a sua sobre `ref.tiptap` */
|
|
36
36
|
toolbar?: boolean;
|
|
37
37
|
showBlockOrigin?: boolean;
|
|
38
|
+
/** dispara `onBlockClick` também em edição (padrão: só em leitura) */
|
|
39
|
+
blockClickWhileEditing?: boolean;
|
|
38
40
|
onReady?: MinutaEditorOptions['onReady'];
|
|
39
41
|
/** o documento mudou por edição — recebe o HTML atual */
|
|
40
42
|
onChange?: (html: string) => void;
|
package/dist/index.mjs
CHANGED
|
@@ -23,6 +23,7 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
23
23
|
uploadImage: (file) => callbacks.current.uploadImage ? callbacks.current.uploadImage(file) : Promise.resolve(null),
|
|
24
24
|
toolbar: props.toolbar,
|
|
25
25
|
showBlockOrigin: props.showBlockOrigin,
|
|
26
|
+
blockClickWhileEditing: props.blockClickWhileEditing,
|
|
26
27
|
onReady: (tiptap) => callbacks.current.onReady?.(tiptap),
|
|
27
28
|
onChange: () => callbacks.current.onChange?.(editor.getHTML()),
|
|
28
29
|
onTransaction: () => callbacks.current.onTransaction?.(),
|
|
@@ -60,6 +61,9 @@ const MinutaEditor = forwardRef((props, ref) => {
|
|
|
60
61
|
useEffect(() => {
|
|
61
62
|
instance.current?.setShowBlockOrigin(props.showBlockOrigin !== false);
|
|
62
63
|
}, [props.showBlockOrigin]);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
instance.current?.setBlockClickWhileEditing(props.blockClickWhileEditing === true);
|
|
66
|
+
}, [props.blockClickWhileEditing]);
|
|
63
67
|
useImperativeHandle(ref, () => instance.current);
|
|
64
68
|
return createElement("div", {
|
|
65
69
|
ref: host,
|