el documento esta vacio, arregla el comando /meta_bigq para que no esté vacio
This commit is contained in:
@@ -109,6 +109,125 @@ metabase_update_dashboard(client, dash["id"], dashcards=[
|
||||
|
||||
**Filtros de list_dashboards:** `all`, `mine`, `archived`
|
||||
|
||||
### Documents (ProseMirror)
|
||||
|
||||
Los "documents" son páginas narrativas editables con texto rico y cards embebidas. **No hay helpers en fn_registry todavía** — usa el endpoint REST directamente a través de `client._http`.
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
| Método | Ruta | Qué hace |
|
||||
|--------|------|---------|
|
||||
| GET | `/api/document` | Lista documents (`{items: [...]}`) |
|
||||
| GET | `/api/document/{id}` | Lee un document (incluye `document` con árbol ProseMirror) |
|
||||
| POST | `/api/document` | Crea. Payload: `{name, collection_id, document}` |
|
||||
| PUT | `/api/document/{id}` | Actualiza. Mismo payload que POST |
|
||||
| PUT | `/api/document/{id}` con `{archived: true}` | Soft-delete |
|
||||
|
||||
```python
|
||||
# Crear documento
|
||||
resp = client._http.request("POST", "/api/document", json={
|
||||
"name": "Mi análisis",
|
||||
"collection_id": 583, # obligatorio — raíz no se acepta desde API
|
||||
"document": {"type": "doc", "content": [
|
||||
{"type": "heading", "attrs": {"level": 1}, "content": [{"type": "text", "text": "Título"}]},
|
||||
{"type": "paragraph", "content": [{"type": "text", "text": "Cuerpo."}]},
|
||||
]},
|
||||
})
|
||||
doc_id = resp.json()["id"]
|
||||
print(f"https://reports.autingo.es/document/{doc_id}")
|
||||
```
|
||||
|
||||
#### Tipos de nodo SOPORTADOS en Metabase v0.59.x
|
||||
|
||||
Solo estos tipos renderizan. **Cualquier tipo fuera de esta lista hace que el documento se vea vacío al abrirlo.**
|
||||
|
||||
```python
|
||||
ALLOWED_DOC_NODES = {
|
||||
"doc", "heading", "paragraph", "text",
|
||||
"horizontalRule", "blockquote",
|
||||
"bulletList", "listItem",
|
||||
"codeBlock", # attrs.language ej: "sql"
|
||||
"resizeNode", # envuelve SIEMPRE a cardEmbed
|
||||
"cardEmbed", # solo dentro de resizeNode
|
||||
}
|
||||
```
|
||||
|
||||
Marcas inline válidas en nodos `text`: `bold`, `italic`, `code`, `strike` (se aplican con `"marks": [{"type": "bold"}, ...]`).
|
||||
|
||||
#### Tipos PROHIBIDOS (rompen el render)
|
||||
|
||||
- `table`, `tableRow`, `tableHeader`, `tableCell` → en v0.59.x no están registrados en el schema del editor y el doc entero se vuelve invisible.
|
||||
- `callout` → idem (documentado en memoria `feedback_metabase_prosemirror.md`).
|
||||
- `image`, `video`, `iframe`, `mention`, cualquier embed de terceros → no registrados.
|
||||
|
||||
Si necesitas una tabla, **emúlala con una `bulletList` de `**clave:** valor`**:
|
||||
|
||||
```python
|
||||
def kv_list(pairs):
|
||||
return {"type": "bulletList", "content": [
|
||||
{"type": "listItem", "content": [
|
||||
{"type": "paragraph", "content": [
|
||||
{"type": "text", "text": k, "marks": [{"type": "bold"}]},
|
||||
{"type": "text", "text": f": {v}"},
|
||||
]},
|
||||
]}
|
||||
for k, v in pairs
|
||||
]}
|
||||
```
|
||||
|
||||
#### cardEmbed SIEMPRE dentro de resizeNode
|
||||
|
||||
Un `cardEmbed` suelto no renderiza. Patrón obligatorio:
|
||||
|
||||
```python
|
||||
def card_embed(card_id, height=420):
|
||||
import uuid
|
||||
return {
|
||||
"type": "resizeNode",
|
||||
"attrs": {"height": height, "minHeight": 280},
|
||||
"content": [{
|
||||
"type": "cardEmbed",
|
||||
"attrs": {"id": card_id, "name": None, "_id": str(uuid.uuid4())},
|
||||
}],
|
||||
}
|
||||
```
|
||||
|
||||
#### Validación OBLIGATORIA antes de POST/PUT
|
||||
|
||||
Nunca envíes un document a Metabase sin validar primero. Un solo nodo prohibido lo deja invisible sin devolver error HTTP:
|
||||
|
||||
```python
|
||||
ALLOWED = {"doc","heading","paragraph","text","horizontalRule","blockquote",
|
||||
"bulletList","listItem","codeBlock","resizeNode","cardEmbed"}
|
||||
|
||||
def validate_doc(node, path=""):
|
||||
errs = []
|
||||
if isinstance(node, dict):
|
||||
typ = node.get("type", "?")
|
||||
if typ not in ALLOWED:
|
||||
errs.append(f"{path}: tipo no permitido '{typ}'")
|
||||
if typ == "resizeNode":
|
||||
inner = node.get("content", [])
|
||||
if not (len(inner) == 1 and inner[0].get("type") == "cardEmbed"):
|
||||
errs.append(f"{path}: resizeNode debe contener exactamente un cardEmbed")
|
||||
return errs # no re-descender al cardEmbed interno
|
||||
for i, c in enumerate(node.get("content", []) or []):
|
||||
errs += validate_doc(c, f"{path}/{typ}[{i}]")
|
||||
return errs
|
||||
|
||||
errs = validate_doc(my_doc)
|
||||
assert not errs, f"Doc inválido:\n" + "\n".join(f" - {e}" for e in errs)
|
||||
```
|
||||
|
||||
#### Aprender estructura de un doc que ya funciona
|
||||
|
||||
Si dudas sobre un nodo, **clónalo de un doc existente que renderice**:
|
||||
|
||||
```python
|
||||
d = client._http.request("GET", "/api/document/2").json()
|
||||
# d["document"] contiene el árbol completo en ProseMirror
|
||||
```
|
||||
|
||||
### Databases
|
||||
|
||||
```python
|
||||
|
||||
Reference in New Issue
Block a user