@bacano/sdk 1.1.18 → 1.1.20
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/CONTENT_TYPE_MANAGEMENT.md +80 -2
- package/README.md +3 -3
- package/STOREFRONT_FORMS.md +57 -0
- package/STOREFRONT_LEADS.md +1 -1
- package/STOREFRONT_PREVIEW.md +21 -8
- package/STOREFRONT_VOLUME_PRICING.md +1 -1
- package/content-management-types.d.ts +4 -3
- package/content-management.cjs +37 -0
- package/content-management.d.ts +1 -0
- package/content-management.js +35 -0
- package/form-management-types.d.ts +62 -0
- package/form-management.d.ts +2 -0
- package/package.json +1 -1
- package/react/forms.cjs +11 -2
- package/react/forms.d.ts +4 -0
- package/react/forms.js +11 -2
- package/skills/bacano-figma-web/SKILL.md +1 -1
- package/skills/bacano-figma-web/references/content-workflow.md +7 -0
- package/skills/bacano-web/references/forms.md +13 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`@bacano/sdk/content-management` permite que una herramienta administrativa o
|
|
4
4
|
un agente autorizado consulte y mantenga **tipos, componentes, grupos, listas
|
|
5
|
-
de opciones, entradas e imágenes** de Bacano Content
|
|
5
|
+
de opciones, entradas e imágenes** de Bacano Content, y **formularios web**.
|
|
6
|
+
Cada capacidad requiere
|
|
6
7
|
un permiso explícito. El acceso no autoriza productos, ventas, clientes,
|
|
7
8
|
configuración general, GraphQL, SQL ni acceso directo a Storage.
|
|
8
9
|
|
|
@@ -12,7 +13,7 @@ dentro del bundle del sitio web.
|
|
|
12
13
|
|
|
13
14
|
## Activar un acceso temporal
|
|
14
15
|
|
|
15
|
-
1. En Bacano, abre **Sitios web → Contenido → Tipos** o **
|
|
16
|
+
1. En Bacano, abre **Sitios web → Contenido → Tipos**, **Componentes** o **Formularios**.
|
|
16
17
|
2. Selecciona **Conectar agente**.
|
|
17
18
|
3. Elige el alcance, 15, 30 o 60 minutos y genera el acceso.
|
|
18
19
|
4. Copia el token al entorno local del agente.
|
|
@@ -32,6 +33,15 @@ Scopes disponibles:
|
|
|
32
33
|
también `content_entries:manage`.
|
|
33
34
|
- `content_media:upload`: consultar imágenes de la empresa y cargar JPEG, PNG o
|
|
34
35
|
WebP. Requiere también `content_entries:manage`; no permite borrar archivos.
|
|
36
|
+
- `forms:manage`: consultar sitios y listas disponibles para formularios,
|
|
37
|
+
inventariar formularios y crear o reemplazar sus borradores.
|
|
38
|
+
- `forms:publish`: publicar, pausar, reactivar o archivar formularios. Requiere
|
|
39
|
+
`forms:manage`. También se exige para cambiar notificaciones de formularios
|
|
40
|
+
que ya fueron publicados, porque afectan envíos en curso.
|
|
41
|
+
|
|
42
|
+
Los permisos de Content no conceden acceso a formularios ni viceversa. Los
|
|
43
|
+
tokens existentes no se amplían; solicita uno nuevo con el alcance necesario.
|
|
44
|
+
Estos permisos no permiten leer respuestas, datos personales, leads ni métricas.
|
|
35
45
|
|
|
36
46
|
No guardes el token en Git, Figma, Content, logs, variables públicas ni archivos
|
|
37
47
|
que se empaqueten con el storefront.
|
|
@@ -432,6 +442,74 @@ omitas opciones existentes por accidente en una actualización.
|
|
|
432
442
|
- Los tokens v2 no son compatibles con tokens anteriores y tienen como máximo
|
|
433
443
|
60 minutos de vigencia.
|
|
434
444
|
|
|
445
|
+
## Formularios: misma definición en el agente, el ERP y el storefront
|
|
446
|
+
|
|
447
|
+
No modeles un formulario como un tipo de Content ni mantengas una copia de sus
|
|
448
|
+
campos en el frontend. Usa el mismo cliente administrativo con `forms:manage`:
|
|
449
|
+
|
|
450
|
+
1. `getContext()` confirma empresa, scopes y vencimiento.
|
|
451
|
+
2. `getFormReferences()` devuelve los sitios de esa empresa y las Listas de
|
|
452
|
+
Content activas, con sus pares `value` / `label`.
|
|
453
|
+
3. `listForms({ websiteId, key, limit, offset })` inventaría antes de crear.
|
|
454
|
+
Las keys son únicas por sitio, no por empresa. Pagina hasta alcanzar `total`.
|
|
455
|
+
4. Presenta al usuario sitio, key, campos, acción y mensajes antes de crear.
|
|
456
|
+
5. `createForm(input)` crea exclusivamente un borrador.
|
|
457
|
+
6. `getForm(formId)` consulta la definición completa y su `updatedAt`.
|
|
458
|
+
7. `updateForm(formId, { ...input, expectedUpdatedAt })` reemplaza el borrador
|
|
459
|
+
completo. Conserva los campos que deben permanecer; no cambia key ni sitio.
|
|
460
|
+
8. Solo con aprobación humana y `forms:publish`, usa `publishForm(formId,
|
|
461
|
+
{ expectedUpdatedAt })`. Crea una versión inmutable que consume el SDK público.
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
const { websites, optionLists } = await content.getFormReferences();
|
|
465
|
+
// Elegir y confirmar con el usuario un websiteId devuelto por websites.
|
|
466
|
+
const form = await content.createForm({
|
|
467
|
+
websiteId,
|
|
468
|
+
key: "solicitar-informacion",
|
|
469
|
+
name: "Solicitar información",
|
|
470
|
+
action: "CAPTURE_LEAD",
|
|
471
|
+
definition: {
|
|
472
|
+
schemaVersion: 1,
|
|
473
|
+
submitLabel: "Enviar solicitud",
|
|
474
|
+
successMessage: "Recibimos tu solicitud.",
|
|
475
|
+
unavailableMessage: "Este formulario no está disponible.",
|
|
476
|
+
fields: [
|
|
477
|
+
{ key: "correo", label: "Correo electrónico", type: "email", required: true },
|
|
478
|
+
{
|
|
479
|
+
key: "intereses", label: "¿Qué te interesa?", type: "multi_select", required: true,
|
|
480
|
+
optionSource: { kind: "INLINE", options: [
|
|
481
|
+
{ value: "sitio_web", label: "Sitio web" },
|
|
482
|
+
{ value: "tienda_virtual", label: "Tienda virtual" },
|
|
483
|
+
] },
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
sections: [],
|
|
487
|
+
steps: [],
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
// Tras revisar y aprobar; requiere forms:publish:
|
|
491
|
+
await content.publishForm(form.id, { expectedUpdatedAt: form.updatedAt });
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Las opciones `select`, `radio` y `multi_select` usan pares `value` / `label`
|
|
495
|
+
o `optionSource: { kind: "CONTENT_LIST", listKey }`. Reutiliza únicamente una
|
|
496
|
+
lista activa de `getFormReferences()`; administrarla exige el permiso separado
|
|
497
|
+
de Content. Los tipos `email` y `phone` conservan la semántica de contacto.
|
|
498
|
+
`CAPTURE_LEAD` utiliza la resolución de identidad ya existente: no llames además
|
|
499
|
+
a la captura de campañas ni crees otro lead desde el agente.
|
|
500
|
+
|
|
501
|
+
`changeFormStatus(formId, { status, expectedUpdatedAt })` acepta `ACTIVE`,
|
|
502
|
+
`PAUSED` o `ARCHIVED`, con aprobación humana. Activar o pausar requiere una
|
|
503
|
+
versión publicada; archivar es terminal en el flujo actual. No hay borrado ni
|
|
504
|
+
restauración por este acceso.
|
|
505
|
+
|
|
506
|
+
Ante `WEBSITE_FORM_REVISION_CONFLICT` (HTTP 409), vuelve a consultar y presenta
|
|
507
|
+
las diferencias; nunca reintentes automáticamente reemplazando la revisión.
|
|
508
|
+
Editar un borrador no publica su estructura: la versión pública anterior sigue
|
|
509
|
+
vigente hasta publicar. Las respuestas y sus snapshots no se reescriben.
|
|
510
|
+
Usa `BacanoForm` o `client.forms` con la key publicada y el mismo website,
|
|
511
|
+
siguiendo `STOREFRONT_FORMS.md`. El token temporal jamás entra en ese bundle.
|
|
512
|
+
|
|
435
513
|
## Límites de esta versión
|
|
436
514
|
|
|
437
515
|
- La carga administrativa solo admite imágenes raster JPEG, PNG y WebP. SVG,
|
package/README.md
CHANGED
|
@@ -21,10 +21,10 @@ React hooks are available from `@bacano/sdk/react` and require `react >= 18`
|
|
|
21
21
|
as a peer dependency. The core client has no runtime dependencies and works
|
|
22
22
|
without React.
|
|
23
23
|
|
|
24
|
-
### Gestión de
|
|
24
|
+
### Gestión de Content y formularios para agentes
|
|
25
25
|
|
|
26
26
|
Un agente autorizado puede consultar y mantener tipos, componentes, grupos,
|
|
27
|
-
listas de opciones y
|
|
27
|
+
listas de opciones, entradas y formularios con el entrypoint administrativo
|
|
28
28
|
`@bacano/sdk/content-management`. Cada capacidad exige un scope explícito. El
|
|
29
29
|
acceso se genera temporalmente desde Bacano, está limitado a una empresa y
|
|
30
30
|
nunca debe incluirse en el storefront:
|
|
@@ -221,7 +221,7 @@ The public storefront remains static. Preview V1 never exposes drafts and
|
|
|
221
221
|
automatically disables the SDK marketing runtime inside its tree. See
|
|
222
222
|
[`STOREFRONT_PREVIEW.md`](./STOREFRONT_PREVIEW.md), the canonical contract for
|
|
223
223
|
provider hierarchy, responsibilities, security, agent checklist, and the
|
|
224
|
-
validated
|
|
224
|
+
validated reference implementation.
|
|
225
225
|
|
|
226
226
|
---
|
|
227
227
|
|
package/STOREFRONT_FORMS.md
CHANGED
|
@@ -4,6 +4,12 @@ Los formularios públicos de Bacano son definiciones tipadas y versionadas. El
|
|
|
4
4
|
storefront decide dónde se muestran y cómo lucen; Bacano controla su estructura,
|
|
5
5
|
estado y validaciones. La definición nunca contiene HTML ni JavaScript arbitrario.
|
|
6
6
|
|
|
7
|
+
Un agente puede crear y editar estas mismas definiciones desde
|
|
8
|
+
`@bacano/sdk/content-management`, con el acceso temporal de **Conectar agente**.
|
|
9
|
+
Lee la sección de formularios en `CONTENT_TYPE_MANAGEMENT.md`: `forms:manage`
|
|
10
|
+
prepara borradores y `forms:publish` permite publicar con aprobación. Nunca
|
|
11
|
+
incluyas ese cliente administrativo ni su token en el storefront público.
|
|
12
|
+
|
|
7
13
|
## Renderer oficial para React
|
|
8
14
|
|
|
9
15
|
Monta un único `BacanoProvider` global y usa la key pública configurada en el ERP:
|
|
@@ -29,6 +35,57 @@ idempotente estable y llama `client.forms.submitResponse(...)`. Un formulario
|
|
|
29
35
|
pausado muestra su mensaje de indisponibilidad y deja de operar sin reconstruir
|
|
30
36
|
el storefront.
|
|
31
37
|
|
|
38
|
+
## Preseleccionar desde un botón externo
|
|
39
|
+
|
|
40
|
+
Usa `values` y `onValuesChange` juntos para controlar el estado. El botón mezcla
|
|
41
|
+
solo el campo que cambia: no reemplaces todo el objeto ni desmontes el formulario
|
|
42
|
+
para preseleccionar. El visitante puede seguir editando cualquier campo.
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useRef, useState } from "react";
|
|
46
|
+
import { BacanoForm, type BacanoFormValues } from "@bacano/sdk/react/forms";
|
|
47
|
+
|
|
48
|
+
export function ProjectInquiry() {
|
|
49
|
+
const [values, setValues] = useState<BacanoFormValues>({});
|
|
50
|
+
const sectionRef = useRef<HTMLElement>(null);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => {
|
|
57
|
+
setValues((current) => ({ ...current, producto: "gaia" }));
|
|
58
|
+
sectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
Me interesa Gaia
|
|
62
|
+
</button>
|
|
63
|
+
<section ref={sectionRef}>
|
|
64
|
+
<BacanoForm
|
|
65
|
+
formKey="contacto"
|
|
66
|
+
values={values}
|
|
67
|
+
onValuesChange={setValues}
|
|
68
|
+
/>
|
|
69
|
+
</section>
|
|
70
|
+
</>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `producto` debe ser la key real del campo publicado y `gaia` el `value`
|
|
76
|
+
real de la opción, no su label. Usa `string[]` para `multi_select` y booleanos
|
|
77
|
+
para `checkbox`. Las validaciones publicadas siguen aplicándose.
|
|
78
|
+
- Puedes inicializar `useState` con la preselección, incluso antes de cargar
|
|
79
|
+
la definición. Un cambio externo no reinicia los pasos ni los otros campos.
|
|
80
|
+
- `onValuesChange` entrega el objeto completo tras cada edición del visitante;
|
|
81
|
+
los cambios externos se hacen sobre el estado del componente padre.
|
|
82
|
+
- Las keys ajenas a la definición publicada no se envían. Si compartes un
|
|
83
|
+
componente entre distintos sitios o formularios, reinicia su estado al cambiar
|
|
84
|
+
de identidad; no reutilices los datos de un visitante entre formularios.
|
|
85
|
+
- Sin `onSubmit` se mantiene el envío oficial, incluyendo la selección. No
|
|
86
|
+
necesitas otro endpoint, eventos DOM ni un formulario paralelo.
|
|
87
|
+
- Omitir ambas props conserva el funcionamiento anterior, con estado interno.
|
|
88
|
+
|
|
32
89
|
## Estructura del formulario
|
|
33
90
|
|
|
34
91
|
La definición tiene tres niveles explícitos:
|
package/STOREFRONT_LEADS.md
CHANGED
|
@@ -50,7 +50,7 @@ if (!consent) return null;
|
|
|
50
50
|
// Conserva esta clave en el estado del formulario mientras reintentas el envío.
|
|
51
51
|
const idempotencyKey = crypto.randomUUID();
|
|
52
52
|
const result = await client.marketing.captureLead({
|
|
53
|
-
campaignKey: "newsletter-
|
|
53
|
+
campaignKey: "newsletter-primavera",
|
|
54
54
|
idempotencyKey,
|
|
55
55
|
commercialConsent: true,
|
|
56
56
|
contact: {
|
package/STOREFRONT_PREVIEW.md
CHANGED
|
@@ -227,6 +227,7 @@ export default function PreviewPage() {
|
|
|
227
227
|
"use client";
|
|
228
228
|
|
|
229
229
|
import { useSearchParams } from "next/navigation";
|
|
230
|
+
import { useState } from "react";
|
|
230
231
|
import {
|
|
231
232
|
BacanoPreviewShell,
|
|
232
233
|
interceptPreviewLink,
|
|
@@ -265,7 +266,11 @@ const loadRoute: PreviewRouteLoader<PreviewDocument> = async ({
|
|
|
265
266
|
|
|
266
267
|
export function PreviewClient() {
|
|
267
268
|
const searchParams = useSearchParams();
|
|
268
|
-
|
|
269
|
+
// Initial value only. Held in state afterwards so `navigate` can change it
|
|
270
|
+
// without a reload; listen for `popstate` to keep the back button working.
|
|
271
|
+
const [route, setRoute] = useState(() =>
|
|
272
|
+
normalizePreviewRoute(searchParams.get("route")),
|
|
273
|
+
);
|
|
269
274
|
|
|
270
275
|
return (
|
|
271
276
|
<BacanoPreviewShell
|
|
@@ -274,9 +279,17 @@ export function PreviewClient() {
|
|
|
274
279
|
loadRoute={loadRoute}
|
|
275
280
|
isPreviewableRoute={(nextRoute) => previewable.has(nextRoute)}
|
|
276
281
|
navigate={(nextRoute) => {
|
|
277
|
-
|
|
282
|
+
// pushState, not location.assign. A full navigation unmounts the
|
|
283
|
+
// provider, and its route cache lives in a ref on it — so assigning
|
|
284
|
+
// location throws away every already-loaded route and refetches the
|
|
285
|
+
// one you are navigating to. Keep the route in state and let the
|
|
286
|
+
// provider reload it in place.
|
|
287
|
+
window.history.pushState(
|
|
288
|
+
null,
|
|
289
|
+
"",
|
|
278
290
|
`/vista-previa/?route=${encodeURIComponent(nextRoute)}`,
|
|
279
291
|
);
|
|
292
|
+
setRoute(nextRoute);
|
|
280
293
|
}}
|
|
281
294
|
toolbar={{
|
|
282
295
|
websiteName: "My store",
|
|
@@ -414,10 +427,10 @@ When the storefront follows this contract:
|
|
|
414
427
|
The build still depends on whatever Bacano data the public static pages request
|
|
415
428
|
at build time. Preview does not change or replace that build contract.
|
|
416
429
|
|
|
417
|
-
## Validated reference implementation
|
|
430
|
+
## Validated reference implementation
|
|
418
431
|
|
|
419
|
-
|
|
420
|
-
2026-08-06. It uses one global `BacanoProvider`; `BacanoPreviewShell` consumes
|
|
432
|
+
A customer storefront validated the official integration with `@bacano/sdk`
|
|
433
|
+
`1.0.5` on 2026-08-06. It uses one global `BacanoProvider`; `BacanoPreviewShell` consumes
|
|
421
434
|
that initialized client and the loader receives `{ route, force, client }`.
|
|
422
435
|
|
|
423
436
|
The validated static production build generated all `417/417` pages. TypeScript,
|
|
@@ -435,7 +448,7 @@ The implementation removed these generic storefront responsibilities:
|
|
|
435
448
|
- Pathname-based marketing suppression inside preview renderers once the SDK
|
|
436
449
|
boundary covers them.
|
|
437
450
|
|
|
438
|
-
|
|
451
|
+
That storefront retained these storefront-specific responsibilities:
|
|
439
452
|
|
|
440
453
|
- `/vista-previa/page.tsx` and its `noindex` metadata.
|
|
441
454
|
- Sitemap exclusion.
|
|
@@ -445,9 +458,9 @@ Baby Rex retained these storefront-specific responsibilities:
|
|
|
445
458
|
content-driven catalog sections.
|
|
446
459
|
- `PreviewDocumentView` and all production renderers.
|
|
447
460
|
- Storefront-specific skeleton, missing-content, and unsupported-route views.
|
|
448
|
-
- The Next.js adapter that writes `?
|
|
461
|
+
- The Next.js adapter that writes `?route={encodedRoute}`.
|
|
449
462
|
|
|
450
|
-
|
|
463
|
+
It is the implementation reference for integration shape, not a source to
|
|
451
464
|
copy blindly. Other storefronts keep their own route maps, Content keys,
|
|
452
465
|
mappers, and visual components.
|
|
453
466
|
|
|
@@ -124,7 +124,7 @@ confirmado.
|
|
|
124
124
|
|
|
125
125
|
## Prohibiciones
|
|
126
126
|
|
|
127
|
-
- No calcular umbrales en
|
|
127
|
+
- No calcular umbrales en el storefront.
|
|
128
128
|
- No seleccionar `WHOLESALE` directamente desde el navegador.
|
|
129
129
|
- No enviar precios en operaciones de carrito o checkout.
|
|
130
130
|
- No consultar Hasura ni tablas de precios mediante GraphQL directo.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import type { FormManagementClient } from "./form-management-types";
|
|
1
2
|
export type ContentManagementTypeKind = "entry" | "component";
|
|
2
3
|
export type ContentManagementTypeStatus = "draft" | "active" | "archived";
|
|
3
4
|
export type ContentManagementTypeUsage = "website_content" | "product_additional" | "product_specification";
|
|
4
|
-
export type ContentManagementScope = "content_types:manage" | "content_entries:manage" | "content_media:upload" | "content_entries:publish";
|
|
5
|
+
export type ContentManagementScope = "content_types:manage" | "content_entries:manage" | "content_media:upload" | "content_entries:publish" | "forms:manage" | "forms:publish";
|
|
5
6
|
export type ContentManagementFieldType = "boolean" | "component" | "date" | "link" | "measure" | "media" | "number" | "reference" | "repeater" | "rich_text" | "rich_text_full" | "select" | "slug" | "text" | "textarea";
|
|
6
7
|
export interface ContentManagementChildFieldInput {
|
|
7
8
|
id?: string | null;
|
|
@@ -65,7 +66,7 @@ export interface ContentManagementType extends Omit<ContentManagementTypeInput,
|
|
|
65
66
|
export interface ContentManagementContext {
|
|
66
67
|
companyId: string;
|
|
67
68
|
/** @deprecated Use scopes to inspect the complete grant. */
|
|
68
|
-
scope:
|
|
69
|
+
scope: ContentManagementScope;
|
|
69
70
|
scopes: ContentManagementScope[];
|
|
70
71
|
expiresAt: string;
|
|
71
72
|
}
|
|
@@ -259,7 +260,7 @@ export interface ContentManagementClientConfig {
|
|
|
259
260
|
accessToken: string | ContentManagementAccessTokenProvider;
|
|
260
261
|
fetch?: typeof fetch;
|
|
261
262
|
}
|
|
262
|
-
export interface ContentManagementClient {
|
|
263
|
+
export interface ContentManagementClient extends FormManagementClient {
|
|
263
264
|
getContext(): Promise<ContentManagementContext>;
|
|
264
265
|
getReferences(): Promise<ContentManagementReferences>;
|
|
265
266
|
listTypes(): Promise<ContentManagementType[]>;
|
package/content-management.cjs
CHANGED
|
@@ -24,6 +24,42 @@ __export(content_management_exports, {
|
|
|
24
24
|
createContentManagementClient: () => createContentManagementClient
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(content_management_exports);
|
|
27
|
+
|
|
28
|
+
// libs/sdk/src/form-management.ts
|
|
29
|
+
function createFormManagementMethods(request) {
|
|
30
|
+
const path = (id) => `/forms/${encodeURIComponent(id.trim())}`;
|
|
31
|
+
return {
|
|
32
|
+
getFormReferences: () => request("/forms/references"),
|
|
33
|
+
listForms: (input = {}) => {
|
|
34
|
+
const search = new URLSearchParams();
|
|
35
|
+
for (const key of ["websiteId", "key", "limit", "offset"]) {
|
|
36
|
+
if (input[key] !== void 0) search.set(key, String(input[key]));
|
|
37
|
+
}
|
|
38
|
+
return request(
|
|
39
|
+
`/forms${search.size ? `?${search}` : ""}`
|
|
40
|
+
);
|
|
41
|
+
},
|
|
42
|
+
getForm: (id) => request(path(id)),
|
|
43
|
+
createForm: (input) => request("/forms", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: JSON.stringify(input)
|
|
46
|
+
}),
|
|
47
|
+
updateForm: (id, input) => request(path(id), {
|
|
48
|
+
method: "PUT",
|
|
49
|
+
body: JSON.stringify(input)
|
|
50
|
+
}),
|
|
51
|
+
publishForm: (id, input) => request(`${path(id)}/publish`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: JSON.stringify(input)
|
|
54
|
+
}),
|
|
55
|
+
changeFormStatus: (id, input) => request(`${path(id)}/status`, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
body: JSON.stringify(input)
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// libs/sdk/src/content-management.ts
|
|
27
63
|
var ContentManagementError = class extends Error {
|
|
28
64
|
code;
|
|
29
65
|
status;
|
|
@@ -145,6 +181,7 @@ function createContentManagementClient(config) {
|
|
|
145
181
|
}
|
|
146
182
|
);
|
|
147
183
|
return {
|
|
184
|
+
...createFormManagementMethods(request),
|
|
148
185
|
getContext: () => request("/context"),
|
|
149
186
|
getReferences: () => request("/references"),
|
|
150
187
|
listTypes: () => request("/types"),
|
package/content-management.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContentManagementClient, ContentManagementClientConfig } from "./content-management-types";
|
|
2
2
|
export * from "./content-management-types";
|
|
3
|
+
export * from "./form-management-types";
|
|
3
4
|
export declare class ContentManagementError extends Error {
|
|
4
5
|
readonly code: string;
|
|
5
6
|
readonly status: number;
|
package/content-management.js
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
// libs/sdk/src/form-management.ts
|
|
2
|
+
function createFormManagementMethods(request) {
|
|
3
|
+
const path = (id) => `/forms/${encodeURIComponent(id.trim())}`;
|
|
4
|
+
return {
|
|
5
|
+
getFormReferences: () => request("/forms/references"),
|
|
6
|
+
listForms: (input = {}) => {
|
|
7
|
+
const search = new URLSearchParams();
|
|
8
|
+
for (const key of ["websiteId", "key", "limit", "offset"]) {
|
|
9
|
+
if (input[key] !== void 0) search.set(key, String(input[key]));
|
|
10
|
+
}
|
|
11
|
+
return request(
|
|
12
|
+
`/forms${search.size ? `?${search}` : ""}`
|
|
13
|
+
);
|
|
14
|
+
},
|
|
15
|
+
getForm: (id) => request(path(id)),
|
|
16
|
+
createForm: (input) => request("/forms", {
|
|
17
|
+
method: "POST",
|
|
18
|
+
body: JSON.stringify(input)
|
|
19
|
+
}),
|
|
20
|
+
updateForm: (id, input) => request(path(id), {
|
|
21
|
+
method: "PUT",
|
|
22
|
+
body: JSON.stringify(input)
|
|
23
|
+
}),
|
|
24
|
+
publishForm: (id, input) => request(`${path(id)}/publish`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
body: JSON.stringify(input)
|
|
27
|
+
}),
|
|
28
|
+
changeFormStatus: (id, input) => request(`${path(id)}/status`, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
body: JSON.stringify(input)
|
|
31
|
+
})
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
1
35
|
// libs/sdk/src/content-management.ts
|
|
2
36
|
var ContentManagementError = class extends Error {
|
|
3
37
|
code;
|
|
@@ -120,6 +154,7 @@ function createContentManagementClient(config) {
|
|
|
120
154
|
}
|
|
121
155
|
);
|
|
122
156
|
return {
|
|
157
|
+
...createFormManagementMethods(request),
|
|
123
158
|
getContext: () => request("/context"),
|
|
124
159
|
getReferences: () => request("/references"),
|
|
125
160
|
listTypes: () => request("/types"),
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WebsiteFormAction, WebsiteFormDefinition } from "./types";
|
|
2
|
+
export interface ManagedFormInput {
|
|
3
|
+
websiteId: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
action?: WebsiteFormAction;
|
|
8
|
+
notifyByEmail?: boolean;
|
|
9
|
+
notificationRecipients?: string[];
|
|
10
|
+
definition: WebsiteFormDefinition;
|
|
11
|
+
}
|
|
12
|
+
export interface ManagedForm extends Required<ManagedFormInput> {
|
|
13
|
+
id: string;
|
|
14
|
+
companyId: string;
|
|
15
|
+
status: "DRAFT" | "ACTIVE" | "PAUSED" | "ARCHIVED";
|
|
16
|
+
publishedVersion: number | null;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
updatedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ManagedFormListInput {
|
|
21
|
+
websiteId?: string;
|
|
22
|
+
key?: string;
|
|
23
|
+
limit?: number;
|
|
24
|
+
offset?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ManagedFormList {
|
|
27
|
+
forms: ManagedForm[];
|
|
28
|
+
total: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ManagedFormReferences {
|
|
31
|
+
websites: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
slug: string;
|
|
35
|
+
isActive: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
optionLists: Array<{
|
|
38
|
+
key: string;
|
|
39
|
+
name: string;
|
|
40
|
+
options: Array<{
|
|
41
|
+
value: string;
|
|
42
|
+
label: string;
|
|
43
|
+
}>;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export interface FormManagementClient {
|
|
47
|
+
getFormReferences(): Promise<ManagedFormReferences>;
|
|
48
|
+
listForms(input?: ManagedFormListInput): Promise<ManagedFormList>;
|
|
49
|
+
getForm(formId: string): Promise<ManagedForm>;
|
|
50
|
+
createForm(input: ManagedFormInput): Promise<ManagedForm>;
|
|
51
|
+
/** Replaces the full draft. Preserve existing fields and use the last read revision. */
|
|
52
|
+
updateForm(formId: string, input: ManagedFormInput & {
|
|
53
|
+
expectedUpdatedAt: string;
|
|
54
|
+
}): Promise<ManagedForm>;
|
|
55
|
+
publishForm(formId: string, input: {
|
|
56
|
+
expectedUpdatedAt: string;
|
|
57
|
+
}): Promise<ManagedForm>;
|
|
58
|
+
changeFormStatus(formId: string, input: {
|
|
59
|
+
status: "ACTIVE" | "PAUSED" | "ARCHIVED";
|
|
60
|
+
expectedUpdatedAt: string;
|
|
61
|
+
}): Promise<ManagedForm>;
|
|
62
|
+
}
|
package/package.json
CHANGED
package/react/forms.cjs
CHANGED
|
@@ -260,7 +260,7 @@ function BacanoForm(props) {
|
|
|
260
260
|
const { form, loading, error } = useBacanoForm(props.formKey, {
|
|
261
261
|
initialForm: props.initialForm
|
|
262
262
|
});
|
|
263
|
-
const [
|
|
263
|
+
const [internalValues, setValues] = (0, import_react3.useState)({});
|
|
264
264
|
const [step, setStep] = (0, import_react3.useState)(0);
|
|
265
265
|
const [submitting, setSubmitting] = (0, import_react3.useState)(false);
|
|
266
266
|
const [submitted, setSubmitted] = (0, import_react3.useState)(false);
|
|
@@ -283,6 +283,11 @@ function BacanoForm(props) {
|
|
|
283
283
|
),
|
|
284
284
|
[fields, sections, steps]
|
|
285
285
|
);
|
|
286
|
+
const values = Object.fromEntries(
|
|
287
|
+
Object.entries(props.values ?? internalValues).filter(
|
|
288
|
+
([key]) => fieldKeys.has(key)
|
|
289
|
+
)
|
|
290
|
+
);
|
|
286
291
|
(0, import_react3.useEffect)(() => {
|
|
287
292
|
setStep(0);
|
|
288
293
|
setValues(
|
|
@@ -308,7 +313,11 @@ function BacanoForm(props) {
|
|
|
308
313
|
const isMultiStep = steps.length > 0;
|
|
309
314
|
const activeStep = isMultiStep ? steps[step] : null;
|
|
310
315
|
const isLastStep = !isMultiStep || step === steps.length - 1;
|
|
311
|
-
const setFieldValue = (key, value) =>
|
|
316
|
+
const setFieldValue = (key, value) => {
|
|
317
|
+
const nextValues = { ...values, [key]: value };
|
|
318
|
+
setValues(nextValues);
|
|
319
|
+
props.onValuesChange?.(nextValues);
|
|
320
|
+
};
|
|
312
321
|
const submit = async (event) => {
|
|
313
322
|
event.preventDefault();
|
|
314
323
|
if (!isLastStep) {
|
package/react/forms.d.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface BacanoFormClassNames {
|
|
|
33
33
|
}
|
|
34
34
|
export interface BacanoFormProps {
|
|
35
35
|
formKey: string;
|
|
36
|
+
/** Controlled values. Use together with onValuesChange; option values, not labels. */
|
|
37
|
+
values?: BacanoFormValues;
|
|
38
|
+
/** Receives the complete next values after a visitor edits a field. */
|
|
39
|
+
onValuesChange?(values: BacanoFormValues): void;
|
|
36
40
|
/** Definition embedded by a server/build; it is revalidated in runtime. */
|
|
37
41
|
initialForm?: WebsitePublishedForm | null;
|
|
38
42
|
/**
|
package/react/forms.js
CHANGED
|
@@ -242,7 +242,7 @@ function BacanoForm(props) {
|
|
|
242
242
|
const { form, loading, error } = useBacanoForm(props.formKey, {
|
|
243
243
|
initialForm: props.initialForm
|
|
244
244
|
});
|
|
245
|
-
const [
|
|
245
|
+
const [internalValues, setValues] = useState2({});
|
|
246
246
|
const [step, setStep] = useState2(0);
|
|
247
247
|
const [submitting, setSubmitting] = useState2(false);
|
|
248
248
|
const [submitted, setSubmitted] = useState2(false);
|
|
@@ -265,6 +265,11 @@ function BacanoForm(props) {
|
|
|
265
265
|
),
|
|
266
266
|
[fields, sections, steps]
|
|
267
267
|
);
|
|
268
|
+
const values = Object.fromEntries(
|
|
269
|
+
Object.entries(props.values ?? internalValues).filter(
|
|
270
|
+
([key]) => fieldKeys.has(key)
|
|
271
|
+
)
|
|
272
|
+
);
|
|
268
273
|
useEffect2(() => {
|
|
269
274
|
setStep(0);
|
|
270
275
|
setValues(
|
|
@@ -290,7 +295,11 @@ function BacanoForm(props) {
|
|
|
290
295
|
const isMultiStep = steps.length > 0;
|
|
291
296
|
const activeStep = isMultiStep ? steps[step] : null;
|
|
292
297
|
const isLastStep = !isMultiStep || step === steps.length - 1;
|
|
293
|
-
const setFieldValue = (key, value) =>
|
|
298
|
+
const setFieldValue = (key, value) => {
|
|
299
|
+
const nextValues = { ...values, [key]: value };
|
|
300
|
+
setValues(nextValues);
|
|
301
|
+
props.onValuesChange?.(nextValues);
|
|
302
|
+
};
|
|
294
303
|
const submit = async (event) => {
|
|
295
304
|
event.preventDefault();
|
|
296
305
|
if (!isLastStep) {
|
|
@@ -96,7 +96,7 @@ Figma, desde Bacano Content o desde ambas fuentes.
|
|
|
96
96
|
entrada completa, nunca campos individuales. Conservar esta identidad al
|
|
97
97
|
transformar Content en view models y seguir
|
|
98
98
|
`STOREFRONT_CONTENT_LOCATOR.md`.
|
|
99
|
-
- No alterar
|
|
99
|
+
- No alterar el storefront de referencia salvo que ese sea el proyecto indicado.
|
|
100
100
|
|
|
101
101
|
Si Content todavía no existe, generar un blueprint de entrega y fixtures
|
|
102
102
|
temporales fieles al contenido visible de Figma. Los fixtures deben usar el
|
|
@@ -11,6 +11,7 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
|
|
|
11
11
|
| Información | Fuente habitual |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| Títulos, textos, banners y bloques editoriales | Bacano Content |
|
|
14
|
+
| Campos, opciones, pasos y validaciones de formularios | Formularios de Bacano; nunca tipos de Content |
|
|
14
15
|
| Productos, variantes, precios y disponibilidad | Catálogo y live state del SDK |
|
|
15
16
|
| Legal, branding y configuración del sitio | `publicConfig` |
|
|
16
17
|
| Carrito, checkout, cliente y pedidos | Módulo correspondiente del SDK |
|
|
@@ -19,6 +20,12 @@ Antes de crear campos o fixtures, asignar cada valor a una fuente:
|
|
|
19
20
|
No duplicar en Content información que ya tiene una fuente funcional. Un valor
|
|
20
21
|
visible en Figma no implica que sea editorial.
|
|
21
22
|
|
|
23
|
+
Para materializar un formulario del diseño, seguir `STOREFRONT_FORMS.md` y la
|
|
24
|
+
sección de formularios de `CONTENT_TYPE_MANAGEMENT.md`. El cliente temporal
|
|
25
|
+
admite `forms:manage` para crear/editar borradores y `forms:publish` para publicar
|
|
26
|
+
con confirmación. Reutilizar la definición del ERP y sus pares value/label;
|
|
27
|
+
no inventar un esquema paralelo. Los conflictos de revisión requieren releer.
|
|
28
|
+
|
|
22
29
|
## Modelar sin extremos
|
|
23
30
|
|
|
24
31
|
No convertir una página extensa en un único tipo gigante ni crear un componente
|
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
Aplicar este contrato cuando el sitio muestre al menos un formulario administrado
|
|
4
4
|
desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
|
|
5
5
|
|
|
6
|
+
## Creación asistida y sincronización
|
|
7
|
+
|
|
8
|
+
Si falta el formulario, proponer su definición y pedir el acceso temporal de
|
|
9
|
+
**Conectar agente** en Formularios o Content. Leer `CONTENT_TYPE_MANAGEMENT.md`.
|
|
10
|
+
Usar `createContentManagementClient` con `forms:manage`; inventariar sitios,
|
|
11
|
+
listas y formularios antes de crear. Editar mediante `updateForm()` enviando
|
|
12
|
+
el documento completo y `expectedUpdatedAt`. Los conflictos exigen releer,
|
|
13
|
+
no sobreescribir. Publicar o retirar requiere `forms:publish` y confirmación.
|
|
14
|
+
No duplicar las definiciones en Content, GraphQL, SQL ni en fixtures permanentes.
|
|
15
|
+
El storefront consume la key y versión publicada usando el SDK público.
|
|
16
|
+
El acceso administrativo no permite leer respuestas ni PII. `CAPTURE_LEAD`
|
|
17
|
+
ya resuelve la identidad; no ejecutar una segunda captura por campaña.
|
|
18
|
+
|
|
6
19
|
## Implementación oficial
|
|
7
20
|
|
|
8
21
|
- Clasificar formularios como `ACTIVA`, `INACTIVA` o `BLOQUEADA` en la matriz de
|