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`
|
**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
|
### Databases
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
Reference in New Issue
Block a user