@prenta/admin 1.17.3 → 1.17.5

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.
Files changed (28) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/__tests__/views/admin-section-renderers.test.js +1 -0
  3. package/dist/__tests__/views/admin-section-renderers.test.js.map +1 -1
  4. package/dist/__tests__/views/article-body-section.render.test.js +7 -0
  5. package/dist/__tests__/views/article-body-section.render.test.js.map +1 -1
  6. package/dist/__tests__/views/seo-autopilot-card.render.test.js +28 -45
  7. package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -1
  8. package/dist/__tests__/views/updates-tab.render.test.js +45 -3
  9. package/dist/__tests__/views/updates-tab.render.test.js.map +1 -1
  10. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +3 -4
  11. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -1
  12. package/dist/views/page-editor/sections/ArticleBodySection.js +6 -5
  13. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -1
  14. package/dist/views/page-editor/sections/admin-renderers.d.ts +3 -36
  15. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  16. package/dist/views/page-editor/sections/admin-renderers.js +5 -5
  17. package/dist/views/page-editor/sections/admin-renderers.js.map +1 -1
  18. package/dist/views/settings/useUpdates.d.ts.map +1 -1
  19. package/dist/views/settings/useUpdates.js +14 -4
  20. package/dist/views/settings/useUpdates.js.map +1 -1
  21. package/package.json +5 -5
  22. package/src/__tests__/views/admin-section-renderers.test.tsx +1 -0
  23. package/src/__tests__/views/article-body-section.render.test.tsx +14 -0
  24. package/src/__tests__/views/seo-autopilot-card.render.test.tsx +28 -49
  25. package/src/__tests__/views/updates-tab.render.test.tsx +51 -3
  26. package/src/views/page-editor/sections/ArticleBodySection.tsx +7 -5
  27. package/src/views/page-editor/sections/admin-renderers.tsx +5 -5
  28. package/src/views/settings/useUpdates.ts +15 -4
@@ -1,35 +1,3 @@
1
- /**
2
- * Pluggable canvas renderers for the editor canvas and the eye preview.
3
- *
4
- * RESOLUTION ORDER — supplied renderer → built-in → placeholder.
5
- *
6
- * A renderer passed to `<AdminRoot sectionRenderers>` wins for EVERY section
7
- * type, including the built-ins. Before v0.93 the map was consulted only for
8
- * types the canvas did not already know, so a consumer passing the public
9
- * renderer map had their hero, text, feature, stats and cta silently ignored
10
- * while the canvas rendered the admin's own approximations — different font
11
- * size, weight and breakpoints — and presented itself as a preview.
12
- *
13
- * With nothing supplied, built-in types fall back to those approximations and
14
- * custom (config-declared) or unmanaged types render as a structural
15
- * placeholder.
16
- *
17
- * The renderer signature is a structural mirror of `SectionRenderer` from
18
- * `@prenta/sections-react` (cms-admin depends on cms-core only, so the type is
19
- * duplicated rather than imported). `admin-renderer-compat.test.ts` performs
20
- * the assignment that keeps the two shapes compatible.
21
- *
22
- * ```tsx
23
- * import { DEFAULT_SECTION_RENDERERS } from '@prenta/sections-react'
24
- * import { OPAL_RENDERERS } from './sections'
25
- * <AdminRoot sectionRenderers={{ ...DEFAULT_SECTION_RENDERERS, ...OPAL_RENDERERS }} />
26
- * ```
27
- *
28
- * Renderers run inside the admin bundle, so they must be client-safe (no
29
- * server-only imports). Each is wrapped in an error boundary — a throwing
30
- * renderer falls back to the built-in for that type, or to the placeholder if
31
- * the type has no built-in, instead of taking down the editor.
32
- */
33
1
  import { type ReactNode } from 'react';
34
2
  /** Structural mirror of `RenderImageProps` from `@prenta/sections-react`. */
35
3
  export interface AdminRenderImageProps {
@@ -107,10 +75,9 @@ export type AdminSectionRenderer = (props: AdminSectionRendererProps) => ReactNo
107
75
  /** Section-type id → canvas renderer. */
108
76
  export type AdminSectionRendererMap = Record<string, AdminSectionRenderer>;
109
77
  /**
110
- * Render options passed to every admin renderer. Rich text is sanitized on
111
- * write by cms-core, so the canvas (same trust domain as the public default)
112
- * uses the identity sanitizer; images render as plain lazy `<img>` since the
113
- * editor canvas has no next/image pipeline.
78
+ * Render options passed to every admin renderer. Draft HTML is sanitized
79
+ * with the cms-core allow-list before any renderer injects it. Images render
80
+ * as plain lazy `<img>` since the editor canvas has no next/image pipeline.
114
81
  */
115
82
  export declare const ADMIN_RENDER_OPTIONS: AdminSectionRendererProps['options'];
116
83
  export declare const AdminSectionRenderersProvider: import("react").Provider<AdminSectionRendererMap>;
@@ -1 +1 @@
1
- {"version":3,"file":"admin-renderers.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/sections/admin-renderers.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjE,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACxE;AAED;;;;GAIG;AACH,UAAU,kBAAkB;IAC1B,KAAK,IAAI,OAAO,CAAA;IAChB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACnC,CAAA;IACD,GAAG,CAAC,EAAE,uBAAuB,CAAA;IAC7B,OAAO,EAAE;QACP,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;QACtC,OAAO,EAAE,MAAM,CAAA;QACf,WAAW,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,SAAS,CAAA;QACxD,2CAA2C;QAC3C,GAAG,EAAE,kBAAkB,CAAA;QACvB;;;;;;;;;;;;WAYG;QAEH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KACrC,CAAA;CACF;AAED,qDAAqD;AACrD,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,yBAAyB,KAAK,SAAS,CAAA;AAElF,yCAAyC;AACzC,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,yBAAyB,CAAC,SAAS,CAkBrE,CAAA;AAID,eAAO,MAAM,6BAA6B,mDAAwC,CAAA;AAElF,oFAAoF;AACpF,wBAAgB,wBAAwB,IAAI,uBAAuB,CAElE"}
1
+ {"version":3,"file":"admin-renderers.d.ts","sourceRoot":"","sources":["../../../../src/views/page-editor/sections/admin-renderers.tsx"],"names":[],"mappings":"AAmCA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAEjE,6EAA6E;AAC7E,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAC5D,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACxE;AAED;;;;GAIG;AACH,UAAU,kBAAkB;IAC1B,KAAK,IAAI,OAAO,CAAA;IAChB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED,kFAAkF;AAClF,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,WAAW,EAAE,MAAM,CAAA;QACnB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KACnC,CAAA;IACD,GAAG,CAAC,EAAE,uBAAuB,CAAA;IAC7B,OAAO,EAAE;QACP,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;QACtC,OAAO,EAAE,MAAM,CAAA;QACf,WAAW,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,SAAS,CAAA;QACxD,2CAA2C;QAC3C,GAAG,EAAE,kBAAkB,CAAA;QACvB;;;;;;;;;;;;WAYG;QAEH,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KACrC,CAAA;CACF;AAED,qDAAqD;AACrD,MAAM,MAAM,oBAAoB,GAAG,CAAC,KAAK,EAAE,yBAAyB,KAAK,SAAS,CAAA;AAElF,yCAAyC;AACzC,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;AAE1E;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,EAAE,yBAAyB,CAAC,SAAS,CAkBrE,CAAA;AAID,eAAO,MAAM,6BAA6B,mDAAwC,CAAA;AAElF,oFAAoF;AACpF,wBAAgB,wBAAwB,IAAI,uBAAuB,CAElE"}
@@ -32,15 +32,15 @@ import { jsx as _jsx } from "react/jsx-runtime";
32
32
  * renderer falls back to the built-in for that type, or to the placeholder if
33
33
  * the type has no built-in, instead of taking down the editor.
34
34
  */
35
+ import { sanitizeHtml } from '@prenta/core/sanitize';
35
36
  import { createContext, useContext } from 'react';
36
37
  /**
37
- * Render options passed to every admin renderer. Rich text is sanitized on
38
- * write by cms-core, so the canvas (same trust domain as the public default)
39
- * uses the identity sanitizer; images render as plain lazy `<img>` since the
40
- * editor canvas has no next/image pipeline.
38
+ * Render options passed to every admin renderer. Draft HTML is sanitized
39
+ * with the cms-core allow-list before any renderer injects it. Images render
40
+ * as plain lazy `<img>` since the editor canvas has no next/image pipeline.
41
41
  */
42
42
  export const ADMIN_RENDER_OPTIONS = {
43
- sanitizeHtml: (html) => html,
43
+ sanitizeHtml,
44
44
  baseUrl: '',
45
45
  // Never grants the priority slot. On a public page exactly one image may
46
46
  // load eagerly; in the canvas nothing competes for bandwidth and every
@@ -1 +1 @@
1
- {"version":3,"file":"admin-renderers.js","sourceRoot":"","sources":["../../../../src/views/page-editor/sections/admin-renderers.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAA;AA4EjE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAyC;IACxE,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI;IAC5B,OAAO,EAAE,EAAE;IACX,yEAAyE;IACzE,uEAAuE;IACvE,wDAAwD;IACxD,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;IAC1C,eAAe,EAAE,EAAE;IACnB,WAAW,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CACjE,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,IAAI,SAAS,EACzB,MAAM,EAAE,MAAM,IAAI,SAAS,EAC3B,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GACtC,CACH;CACF,CAAA;AAED,MAAM,4BAA4B,GAAG,aAAa,CAA0B,EAAE,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,4BAA4B,CAAC,QAAQ,CAAA;AAElF,oFAAoF;AACpF,MAAM,UAAU,wBAAwB;IACtC,OAAO,UAAU,CAAC,4BAA4B,CAAC,CAAA;AACjD,CAAC"}
1
+ {"version":3,"file":"admin-renderers.js","sourceRoot":"","sources":["../../../../src/views/page-editor/sections/admin-renderers.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAkB,MAAM,OAAO,CAAA;AA4EjE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAyC;IACxE,YAAY;IACZ,OAAO,EAAE,EAAE;IACX,yEAAyE;IACzE,uEAAuE;IACvE,wDAAwD;IACxD,GAAG,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;IAC1C,eAAe,EAAE,EAAE;IACnB,WAAW,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CACjE,cACE,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,KAAK,IAAI,SAAS,EACzB,MAAM,EAAE,MAAM,IAAI,SAAS,EAC3B,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,GACtC,CACH;CACF,CAAA;AAED,MAAM,4BAA4B,GAAG,aAAa,CAA0B,EAAE,CAAC,CAAA;AAE/E,MAAM,CAAC,MAAM,6BAA6B,GAAG,4BAA4B,CAAC,QAAQ,CAAA;AAElF,oFAAoF;AACpF,MAAM,UAAU,wBAAwB;IACtC,OAAO,UAAU,CAAC,4BAA4B,CAAC,CAAA;AACjD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useUpdates.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useUpdates.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,KAAK,GACL,aAAa,GACb,UAAU,GACV,UAAU,GACV,aAAa,CAAA;AAEjB,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB;AAED,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAA;IACnD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAA;IACjC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,mBAAmB,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACjD,MAAM,CAAC,EAAE,eAAe,GAAG,KAAK,CAAA;IAChC,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAUD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;eAMF,MAAM;kBAAY,MAAM;;;;oBAqCvC,MAAM,UAAU,SAAS,MAAM;;;;;;;;;EA0EnE"}
1
+ {"version":3,"file":"useUpdates.d.ts","sourceRoot":"","sources":["../../../src/views/settings/useUpdates.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,gBAAgB,GACxB,SAAS,GACT,KAAK,GACL,aAAa,GACb,UAAU,GACV,UAAU,GACV,aAAa,CAAA;AAEjB,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,gBAAgB,CAAA;CACxB;AAED,UAAU,yBAAyB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,eAAe,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,CAAA;IACnD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAA;IACjC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,mBAAmB,CAAC,EAAE,yBAAyB,EAAE,CAAA;IACjD,MAAM,CAAC,EAAE,eAAe,GAAG,KAAK,CAAA;IAChC,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,UAAU,UAAU;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;CACpB;AAUD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;eAMF,MAAM;kBAAY,MAAM;;;;oBAgDvC,MAAM,UAAU,SAAS,MAAM;;;;;;;;;EA0EnE"}
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { toast } from 'sonner';
4
4
  import { cmsApi } from '../../lib/api.js';
5
5
  const REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
@@ -24,21 +24,31 @@ export function useUpdates({ role }) {
24
24
  const [form, setForm] = useState({ githubRepo: '', githubToken: '' });
25
25
  const [baseline, setBaseline] = useState({ githubRepo: '', githubToken: '' });
26
26
  const [saveState, setSaveState] = useState('idle');
27
+ // The check request is in flight across renders. Read the baseline from the
28
+ // render that started it, not from a later save or keystroke.
29
+ const baselineRef = useRef(baseline);
30
+ baselineRef.current = baseline;
27
31
  // Optimistic until the check response confirms the server-side permission.
28
32
  const canManage = info?.canManage ?? canManageUpdates(role);
29
33
  const check = useCallback(async () => {
30
34
  setChecking(true);
31
35
  setError('');
32
36
  setPrResult(null);
37
+ const baselineAtStart = baselineRef.current;
33
38
  const res = await cmsApi('/updates/check');
34
39
  if (res.error) {
35
40
  setError(res.error);
36
41
  }
37
42
  else if (res.data) {
38
43
  setInfo(res.data);
39
- const repo = res.data.githubRepo ?? '';
40
- setForm({ githubRepo: repo, githubToken: '' });
41
- setBaseline({ githubRepo: repo, githubToken: '' });
44
+ const next = { githubRepo: res.data.githubRepo ?? '', githubToken: '' };
45
+ // A mount-time check can take seconds. Only replace the fields when the
46
+ // user has not typed since this request started (#929).
47
+ setForm((current) => {
48
+ const untouched = current.githubRepo === baselineAtStart.githubRepo && current.githubToken.trim() === '';
49
+ return untouched ? next : current;
50
+ });
51
+ setBaseline(next);
42
52
  }
43
53
  setChecking(false);
44
54
  setHasChecked(true);
@@ -1 +1 @@
1
- {"version":3,"file":"useUpdates.js","sourceRoot":"","sources":["../../../src/views/settings/useUpdates.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useUpdates.js","sourceRoot":"","sources":["../../../src/views/settings/useUpdates.ts"],"names":[],"mappings":"AAAA,YAAY,CAAA;AAEZ,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChE,OAAO,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAE9B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AA8CzC,MAAM,YAAY,GAAG,oCAAoC,CAAA;AAEzD,iFAAiF;AACjF,SAAS,gBAAgB,CAAC,IAA+B;IACvD,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IACpC,OAAO,CAAC,KAAK,OAAO,IAAI,CAAC,KAAK,OAAO,CAAA;AACvC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,EAAE,IAAI,EAAqB;IACpD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAoB,IAAI,CAAC,CAAA;IACzD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/C,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC/C,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAA6C,IAAI,CAAC,CAAA;IAE1F,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAa,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAA;IACjF,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAa,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAA;IACzF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAA0C,MAAM,CAAC,CAAA;IAC3F,4EAA4E;IAC5E,8DAA8D;IAC9D,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IACpC,WAAW,CAAC,OAAO,GAAG,QAAQ,CAAA;IAE9B,2EAA2E;IAC3E,MAAM,SAAS,GAAG,IAAI,EAAE,SAAS,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAA;IAE3D,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,WAAW,CAAC,IAAI,CAAC,CAAA;QACjB,QAAQ,CAAC,EAAE,CAAC,CAAA;QACZ,WAAW,CAAC,IAAI,CAAC,CAAA;QACjB,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,CAAA;QAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAa,gBAAgB,CAAC,CAAA;QACtD,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YACpB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACjB,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,GAAG,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,CAAA;YACvE,wEAAwE;YACxE,wDAAwD;YACxD,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBAClB,MAAM,SAAS,GACb,OAAO,CAAC,UAAU,KAAK,eAAe,CAAC,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;gBACxF,OAAO,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;YACnC,CAAC,CAAC,CAAA;YACF,WAAW,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;QACD,WAAW,CAAC,KAAK,CAAC,CAAA;QAClB,aAAa,CAAC,IAAI,CAAC,CAAA;IACrB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,SAAS,CAAC,GAAG,EAAE;QACb,KAAK,EAAE,CAAA;IACT,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,MAAM,SAAS,GACb,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC;QAClE,CAAC,CAAC,yDAAyD;QAC3D,CAAC,CAAC,EAAE,CAAA;IACR,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,UAAU,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAA;IAEvF,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,GAAqB,EAAE,KAAa,EAAE,EAAE;QACpE,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;QACxC,YAAY,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAClC,IAAI,SAAS,IAAI,CAAC,KAAK;YAAE,OAAM;QAC/B,YAAY,CAAC,QAAQ,CAAC,CAAA;QACtB,MAAM,IAAI,GAA2B,EAAE,CAAA;QACvC,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;QACvE,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;QAEpE,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,iBAAiB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC1F,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,YAAY,CAAC,OAAO,CAAC,CAAA;YACrB,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;YACtB,OAAM;QACR,CAAC;QACD,YAAY,CAAC,SAAS,CAAC,CAAA;QACvB,KAAK,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAA;QAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAA;QACxC,WAAW,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAA;QACvD,OAAO,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC,CAAA;QACnD,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,CACf,IAAI;YACF,CAAC,CAAC;gBACE,GAAG,IAAI;gBACP,UAAU,EAAE,SAAS;gBACrB,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC;aACjE;YACH,CAAC,CAAC,IAAI,CACT,CAAA;IACH,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAA;IAEzD,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,OAAO,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAA;QACxB,YAAY,CAAC,MAAM,CAAC,CAAA;IACtB,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAA;IAEd,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnC,IAAI,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC,SAAS;YAAE,OAAM;QACvC,WAAW,CAAC,IAAI,CAAC,CAAA;QACjB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAsC,gBAAgB,EAAE;YAC9E,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;SACrD,CAAC,CAAA;QACF,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC;aAAM,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YACpB,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;YACrB,KAAK,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAA;QACrC,CAAC;QACD,WAAW,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAA;IAErB,OAAO;QACL,IAAI;QACJ,QAAQ;QACR,UAAU;QACV,KAAK;QACL,QAAQ;QACR,QAAQ;QACR,SAAS;QACT,IAAI;QACJ,QAAQ;QACR,KAAK;QACL,SAAS;QACT,SAAS;QACT,SAAS;QACT,IAAI;QACJ,KAAK;QACL,KAAK;QACL,KAAK;KACN,CAAA;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/admin",
3
- "version": "1.17.3",
3
+ "version": "1.17.5",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -99,15 +99,15 @@
99
99
  "typescript": "^5.7.0",
100
100
  "vitest": "^4.1.0",
101
101
  "@prenta/component-blocks": "0.3.1",
102
- "@prenta/core": "7.1.0",
103
102
  "@prenta/plugin-ai": "3.0.0",
104
103
  "@prenta/plugin-blocks": "0.2.0",
104
+ "@prenta/core": "7.2.1",
105
105
  "@prenta/plugin-commerce": "0.2.1",
106
106
  "@prenta/plugin-forms": "0.1.0",
107
- "@prenta/plugin-media": "0.2.0",
108
- "@prenta/plugin-navigation": "0.3.0",
109
107
  "@prenta/plugin-seo": "1.0.0",
110
- "@prenta/sections-react": "0.17.43"
108
+ "@prenta/plugin-navigation": "0.3.0",
109
+ "@prenta/plugin-media": "0.2.0",
110
+ "@prenta/sections-react": "0.17.46"
111
111
  },
112
112
  "scripts": {
113
113
  "build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/prenta-admin.css --minify",
@@ -56,6 +56,7 @@ describe('pluggable admin section renderers', () => {
56
56
  )
57
57
  const options = seen[0] as { sanitizeHtml: (h: string) => string; baseUrl: string }
58
58
  expect(options.sanitizeHtml('<b>x</b>')).toBe('<b>x</b>')
59
+ expect(options.sanitizeHtml('<p>ok</p><script>alert(1)</script>')).not.toContain('script')
59
60
  expect(options.baseUrl).toBe('')
60
61
  expect(screen.getByAltText('x')).toBeTruthy()
61
62
  })
@@ -29,4 +29,18 @@ describe('ArticleBodySection', () => {
29
29
  expect(container.querySelector('.prenta-rich-text')).toBeTruthy()
30
30
  expect(container.querySelector('.prose')).toBeNull()
31
31
  })
32
+
33
+ it('strips script tags from draft HTML before rendering (#396)', () => {
34
+ const { container } = render(
35
+ <ArticleBodySection
36
+ section={section({ source: 'field' })}
37
+ context={{ body: '<p>Safe copy</p><script>alert(1)</script>' }}
38
+ />,
39
+ )
40
+
41
+ const preview = container.querySelector('.prenta-rich-text')
42
+ expect(preview?.querySelector('script')).toBeNull()
43
+ expect(preview?.innerHTML).toContain('Safe copy')
44
+ expect(preview?.innerHTML).not.toContain('alert')
45
+ })
32
46
  })
@@ -137,8 +137,7 @@ describe('SeoAutopilotCard', () => {
137
137
 
138
138
  it('a blur-commit followed immediately by a checkbox click lands two PUTs, in order', async () => {
139
139
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
140
- await screen.findByText('SEO Autopilot')
141
- const brand = screen.getByLabelText(/minimum brand alignment/i)
140
+ const brand = await screen.findByLabelText(/minimum brand alignment/i)
142
141
  const checkbox = screen.getByLabelText('Missing meta title') as HTMLInputElement
143
142
  fireEvent.change(brand, { target: { value: '90' } })
144
143
  // mousedown on the checkbox blurs the input (commit → PUT #1) and the click
@@ -161,9 +160,9 @@ describe('SeoAutopilotCard', () => {
161
160
  const gate = deferred()
162
161
  putGate = gate.promise
163
162
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
164
- await screen.findByText('SEO Autopilot')
163
+ // The card region also wraps the loading skeleton; wait on a loaded control.
164
+ const checkbox = (await screen.findByLabelText('Missing meta title')) as HTMLInputElement
165
165
  const card = screen.getByRole('region', { name: 'SEO Autopilot' })
166
- const checkbox = screen.getByLabelText('Missing meta title') as HTMLInputElement
167
166
  checkbox.focus()
168
167
  fireEvent.click(checkbox)
169
168
  await waitFor(() => expect(card.getAttribute('aria-busy')).toBe('true'))
@@ -184,8 +183,7 @@ describe('SeoAutopilotCard', () => {
184
183
  runGate = gate.promise
185
184
  current = { ...SETTINGS, enabled: true, enabledById: 'a' }
186
185
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
187
- await screen.findByText('SEO Autopilot')
188
- fireEvent.click(screen.getByRole('button', { name: /run now/i }))
186
+ fireEvent.click(await screen.findByRole('button', { name: /run now/i }))
189
187
  await waitFor(() =>
190
188
  expect(cmsApi).toHaveBeenCalledWith('/seo/autopilot/run', expect.anything()),
191
189
  )
@@ -199,8 +197,7 @@ describe('SeoAutopilotCard', () => {
199
197
 
200
198
  it('toggling a type PUTs a types patch', async () => {
201
199
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
202
- await screen.findByText('SEO Autopilot')
203
- fireEvent.click(screen.getByLabelText('Missing meta title'))
200
+ fireEvent.click(await screen.findByLabelText('Missing meta title'))
204
201
  await waitFor(() =>
205
202
  expect(cmsApi).toHaveBeenCalledWith(
206
203
  '/seo/autopilot/settings',
@@ -218,8 +215,7 @@ describe('SeoAutopilotCard', () => {
218
215
 
219
216
  it('toggling the master switch PUTs { enabled }', async () => {
220
217
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
221
- await screen.findByText('SEO Autopilot')
222
- fireEvent.click(screen.getByRole('switch', { name: /enable autopilot/i }))
218
+ fireEvent.click(await screen.findByRole('switch', { name: /enable autopilot/i }))
223
219
  await waitFor(() => expect(putBodies()).toEqual([{ enabled: true }]))
224
220
  await waitFor(() =>
225
221
  expect(
@@ -230,8 +226,7 @@ describe('SeoAutopilotCard', () => {
230
226
 
231
227
  it('commits a clamped threshold on blur and sends the outcome-watch ratio as a fraction', async () => {
232
228
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
233
- await screen.findByText('SEO Autopilot')
234
- const brand = screen.getByLabelText(/minimum brand alignment/i)
229
+ const brand = await screen.findByLabelText(/minimum brand alignment/i)
235
230
  fireEvent.change(brand, { target: { value: '140' } })
236
231
  fireEvent.blur(brand)
237
232
  await waitFor(() => expect(putBodies()).toEqual([{ minBrandAlignment: 100 }]))
@@ -244,8 +239,7 @@ describe('SeoAutopilotCard', () => {
244
239
 
245
240
  it('does not PUT when a number field is blurred unchanged or with garbage', async () => {
246
241
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
247
- await screen.findByText('SEO Autopilot')
248
- const perRun = screen.getByLabelText(/max changes per run/i)
242
+ const perRun = await screen.findByLabelText(/max changes per run/i)
249
243
  fireEvent.blur(perRun)
250
244
  fireEvent.change(perRun, { target: { value: '' } })
251
245
  fireEvent.blur(perRun)
@@ -256,8 +250,7 @@ describe('SeoAutopilotCard', () => {
256
250
  it('surfaces a PUT validation error verbatim and keeps the previous state', async () => {
257
251
  putResponse = { error: 'maxPerRun must be an integer between 1 and 25', status: 400 }
258
252
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
259
- await screen.findByText('SEO Autopilot')
260
- fireEvent.click(screen.getByLabelText('Missing meta title'))
253
+ fireEvent.click(await screen.findByLabelText('Missing meta title'))
261
254
  await waitFor(() =>
262
255
  expect(toastError).toHaveBeenCalledWith('maxPerRun must be an integer between 1 and 25'),
263
256
  )
@@ -268,8 +261,7 @@ describe('SeoAutopilotCard', () => {
268
261
  it('resets a number field to the last saved value when its PUT fails', async () => {
269
262
  putResponse = { error: 'maxPerRun must be an integer between 1 and 25', status: 400 }
270
263
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
271
- await screen.findByText('SEO Autopilot')
272
- const perRun = screen.getByLabelText(/max changes per run/i) as HTMLInputElement
264
+ const perRun = (await screen.findByLabelText(/max changes per run/i)) as HTMLInputElement
273
265
  fireEvent.change(perRun, { target: { value: '9' } })
274
266
  fireEvent.blur(perRun)
275
267
  await waitFor(() =>
@@ -368,8 +360,7 @@ describe('SeoAutopilotCard', () => {
368
360
  pausedTypes: { 'missing-meta-title': reason },
369
361
  }
370
362
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
371
- await screen.findByText('SEO Autopilot')
372
- const list = screen.getByRole('list', { name: /paused fix types/i })
363
+ const list = await screen.findByRole('list', { name: /paused fix types/i })
373
364
  expect(list.textContent ?? '').toContain(`Missing meta title — ${reason}`)
374
365
  fireEvent.click(within(list).getByRole('button', { name: /resume/i }))
375
366
  await waitFor(() => expect(putBodies()).toEqual([{ resumeTypes: ['missing-meta-title'] }]))
@@ -377,8 +368,7 @@ describe('SeoAutopilotCard', () => {
377
368
 
378
369
  it('Run now is disabled while the policy is disabled and explains why', async () => {
379
370
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
380
- await screen.findByText('SEO Autopilot')
381
- const run = screen.getByRole('button', { name: /run now/i }) as HTMLButtonElement
371
+ const run = (await screen.findByRole('button', { name: /run now/i })) as HTMLButtonElement
382
372
  expect(run.disabled).toBe(true)
383
373
  expect(run.getAttribute('title')).toMatch(/enable autopilot to run it now/i)
384
374
  const hintId = run.getAttribute('aria-describedby')
@@ -397,8 +387,7 @@ describe('SeoAutopilotCard', () => {
397
387
  pausedReason: 'manual',
398
388
  }
399
389
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
400
- await screen.findByText('SEO Autopilot')
401
- const run = screen.getByRole('button', { name: /run now/i }) as HTMLButtonElement
390
+ const run = (await screen.findByRole('button', { name: /run now/i })) as HTMLButtonElement
402
391
  expect(run.disabled).toBe(true)
403
392
  expect(run.getAttribute('title')).toMatch(/resume autopilot to run it now/i)
404
393
  const hintId = run.getAttribute('aria-describedby')
@@ -410,8 +399,7 @@ describe('SeoAutopilotCard', () => {
410
399
  it('Run now carries no hint once it is runnable', async () => {
411
400
  current = { ...SETTINGS, enabled: true, enabledById: 'a' }
412
401
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
413
- await screen.findByText('SEO Autopilot')
414
- const run = screen.getByRole('button', { name: /run now/i }) as HTMLButtonElement
402
+ const run = (await screen.findByRole('button', { name: /run now/i })) as HTMLButtonElement
415
403
  expect(run.disabled).toBe(false)
416
404
  expect(run.getAttribute('title')).toBeNull()
417
405
  expect(run.getAttribute('aria-describedby')).toBeNull()
@@ -420,8 +408,7 @@ describe('SeoAutopilotCard', () => {
420
408
  it('Run now POSTs /seo/autopilot/run and reloads', async () => {
421
409
  current = { ...SETTINGS, enabled: true, enabledById: 'a' }
422
410
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
423
- await screen.findByText('SEO Autopilot')
424
- const run = screen.getByRole('button', { name: /run now/i })
411
+ const run = await screen.findByRole('button', { name: /run now/i })
425
412
  expect((run as HTMLButtonElement).disabled).toBe(false)
426
413
  fireEvent.click(run)
427
414
  // Second click while in flight must not queue a second run.
@@ -457,8 +444,7 @@ describe('SeoAutopilotCard', () => {
457
444
  },
458
445
  }
459
446
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
460
- await screen.findByText('SEO Autopilot')
461
- const summary = screen.getByRole('status', { name: /last run/i })
447
+ const summary = await screen.findByRole('status', { name: /last run/i })
462
448
  expect(summary.textContent ?? '').toMatch(/4 applied/)
463
449
  expect(summary.textContent ?? '').toMatch(/1 verify-failed/)
464
450
  expect(summary.textContent ?? '').toMatch(/2 reverted/)
@@ -475,8 +461,7 @@ describe('SeoAutopilotCard', () => {
475
461
  lastRun: { ...RUN_SUMMARY, halted: true, haltedReason: 'verify_failures' },
476
462
  }
477
463
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
478
- await screen.findByText('SEO Autopilot')
479
- const halted = screen.getByRole('status', { name: /run stopped/i })
464
+ const halted = await screen.findByRole('status', { name: /run stopped/i })
480
465
  expect(halted.textContent ?? '').toMatch(/verification failures/i)
481
466
  expect(halted.className).toContain('text-warning')
482
467
  })
@@ -491,8 +476,7 @@ describe('SeoAutopilotCard', () => {
491
476
  lastRun: { ...RUN_SUMMARY, halted: true, haltedReason: 'budget_exhausted' },
492
477
  }
493
478
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
494
- await screen.findByText('SEO Autopilot')
495
- const halted = screen.getByRole('status', { name: /run stopped/i })
479
+ const halted = await screen.findByRole('status', { name: /run stopped/i })
496
480
  expect(halted.textContent ?? '').toMatch(/ran out of time/i)
497
481
  expect(halted.textContent ?? '').not.toMatch(/daily/i)
498
482
  })
@@ -510,8 +494,7 @@ describe('SeoAutopilotCard', () => {
510
494
  lastRun: { ...RUN_SUMMARY, halted: true, haltedReason: reason },
511
495
  }
512
496
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
513
- await screen.findByText('SEO Autopilot')
514
- const halted = screen.getByRole('status', { name: /run stopped/i })
497
+ const halted = await screen.findByRole('status', { name: /run stopped/i })
515
498
  expect(halted.textContent ?? '').toMatch(copy)
516
499
  expect(halted.textContent ?? '').not.toMatch(/stopped with an error/i)
517
500
  expect(halted.className).toContain('text-warning')
@@ -524,10 +507,8 @@ describe('SeoAutopilotCard', () => {
524
507
  lastRunAt: new Date(Date.now() - 5 * 60_000).toISOString(),
525
508
  }
526
509
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
527
- await screen.findByText('SEO Autopilot')
528
- expect(screen.getByRole('status', { name: /last run/i }).textContent ?? '').toMatch(
529
- /last run 5m ago/i,
530
- )
510
+ const summary = await screen.findByRole('status', { name: /last run/i })
511
+ expect(summary.textContent ?? '').toMatch(/last run 5m ago/i)
531
512
  })
532
513
 
533
514
  it('offers every autopilot issue type exactly once (mirror of SEO_AUTOPILOT_ISSUE_TYPES)', () => {
@@ -539,10 +520,8 @@ describe('SeoAutopilotCard', () => {
539
520
  it('read-only for non-admins', async () => {
540
521
  current = { ...SETTINGS, enabled: true, enabledById: 'a' }
541
522
  render(<SeoAutopilotCard canEdit={false} onNavigate={() => {}} />)
542
- await screen.findByText('SEO Autopilot')
543
- expect(
544
- (screen.getByRole('switch', { name: /enable autopilot/i }) as HTMLInputElement).disabled,
545
- ).toBe(true)
523
+ const toggle = await screen.findByRole('switch', { name: /enable autopilot/i })
524
+ expect((toggle as HTMLInputElement).disabled).toBe(true)
546
525
  expect((screen.getByLabelText('Missing meta title') as HTMLInputElement).disabled).toBe(true)
547
526
  expect((screen.getByLabelText(/minimum brand alignment/i) as HTMLInputElement).disabled).toBe(
548
527
  true,
@@ -561,8 +540,7 @@ describe('SeoAutopilotCard', () => {
561
540
  }
562
541
  const onNavigate = vi.fn()
563
542
  render(<SeoAutopilotCard canEdit onNavigate={onNavigate} />)
564
- await screen.findByText('SEO Autopilot')
565
- const notice = screen.getByRole('region', { name: /governance/i })
543
+ const notice = await screen.findByRole('region', { name: /governance/i })
566
544
  expect(notice.getAttribute('role')).not.toBe('status')
567
545
  expect(notice.textContent ?? '').toMatch(/requires approval/i)
568
546
  fireEvent.click(within(notice).getByRole('button', { name: /settings → ai/i }))
@@ -577,7 +555,9 @@ describe('SeoAutopilotCard', () => {
577
555
  governance: { issueFixApprovalRequired: true, redirectApprovalRequired: false },
578
556
  }
579
557
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
580
- await screen.findByText('SEO Autopilot')
558
+ // Wait on a loaded control: against the skeleton the negative check below
559
+ // would pass vacuously.
560
+ await screen.findByRole('switch', { name: /enable autopilot/i })
581
561
  expect(screen.queryByRole('region', { name: /governance/i })).toBeNull()
582
562
  })
583
563
 
@@ -593,8 +573,7 @@ describe('SeoAutopilotCard', () => {
593
573
  governance: { issueFixApprovalRequired: true, redirectApprovalRequired: false },
594
574
  }
595
575
  render(<SeoAutopilotCard canEdit onNavigate={() => {}} />)
596
- await screen.findByText('SEO Autopilot')
597
- const live = screen.getAllByRole('status').map((el) => el.getAttribute('aria-label'))
576
+ const live = (await screen.findAllByRole('status')).map((el) => el.getAttribute('aria-label'))
598
577
  expect(live.sort()).toEqual(['Last run', 'Run stopped'])
599
578
  })
600
579
 
@@ -5,10 +5,15 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
5
5
  // The Updates tab reads `/updates/check`, saves GitHub config via
6
6
  // PUT `/updates/config`, and opens an upgrade PR via POST `/updates/apply`.
7
7
  let checkData: any
8
+ /** When set, `/updates/check` waits so a test can type before the response lands. */
9
+ let checkGate: Promise<void> | null = null
8
10
 
9
11
  const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string; body?: unknown }) => {
10
12
  const method = options?.method ?? 'GET'
11
- if (endpoint === '/updates/check' && method === 'GET') return { data: checkData, status: 200 }
13
+ if (endpoint === '/updates/check' && method === 'GET') {
14
+ if (checkGate) await checkGate
15
+ return { data: checkData, status: 200 }
16
+ }
12
17
  if (endpoint === '/updates/config' && method === 'PUT')
13
18
  return { data: { success: true }, status: 200 }
14
19
  if (endpoint === '/updates/apply' && method === 'POST')
@@ -65,6 +70,7 @@ function updateAvailable(overrides: Record<string, unknown> = {}) {
65
70
  beforeEach(() => {
66
71
  cmsApi.mockClear()
67
72
  checkData = upToDate()
73
+ checkGate = null
68
74
  })
69
75
 
70
76
  describe('UpdatesTab', () => {
@@ -108,9 +114,50 @@ describe('UpdatesTab', () => {
108
114
  expect(screen.getByText(/Ask an administrator/i)).toBeTruthy()
109
115
  })
110
116
 
117
+ it('keeps a repo and token typed before the check response lands (#929)', async () => {
118
+ let release: () => void = () => {}
119
+ checkGate = new Promise((resolve) => {
120
+ release = resolve
121
+ })
122
+ checkData = upToDate({ githubRepo: 'saved/repo' })
123
+ render(<UpdatesTab role="ADMIN" />)
124
+
125
+ const repo = screen.getByLabelText('Repository') as HTMLInputElement
126
+ const token = screen.getByLabelText(/GitHub token/i) as HTMLInputElement
127
+ fireEvent.change(repo, { target: { value: 'typed/repo' } })
128
+ fireEvent.change(token, { target: { value: 'ghp_secret' } })
129
+
130
+ release()
131
+ await screen.findByText("You're up to date!")
132
+ expect(repo.value).toBe('typed/repo')
133
+ expect(token.value).toBe('ghp_secret')
134
+ })
135
+
136
+ it('fills the repository from the check when the form is untouched', async () => {
137
+ checkData = upToDate({ githubRepo: 'saved/repo' })
138
+ render(<UpdatesTab role="ADMIN" />)
139
+ await screen.findByText("You're up to date!")
140
+ expect((screen.getByLabelText('Repository') as HTMLInputElement).value).toBe('saved/repo')
141
+ })
142
+
143
+ it('does not clear edits when Check for Updates is clicked again', async () => {
144
+ checkData = upToDate({ githubRepo: 'saved/repo' })
145
+ render(<UpdatesTab role="ADMIN" />)
146
+ await screen.findByText("You're up to date!")
147
+
148
+ const repo = screen.getByLabelText('Repository') as HTMLInputElement
149
+ fireEvent.change(repo, { target: { value: 'typed/repo' } })
150
+ fireEvent.click(screen.getByRole('button', { name: 'Check for Updates' }))
151
+ await waitFor(() => expect(cmsApi.mock.calls.length).toBeGreaterThan(1))
152
+ expect(repo.value).toBe('typed/repo')
153
+ })
154
+
111
155
  it('enables Save when the repo changes and persists via PUT /updates/config', async () => {
112
156
  render(<UpdatesTab role="ADMIN" />)
113
- await screen.findByText('GitHub Integration')
157
+ // The GitHub card renders optimistically for admins before /updates/check
158
+ // resolves, and that response resets the form — wait for the check to land
159
+ // before typing so the typed value cannot be wiped mid-test.
160
+ await screen.findByText("You're up to date!")
114
161
 
115
162
  const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
116
163
  expect(save.disabled).toBe(true)
@@ -133,7 +180,8 @@ describe('UpdatesTab', () => {
133
180
 
134
181
  it('blocks Save on an invalid repository format', async () => {
135
182
  render(<UpdatesTab role="ADMIN" />)
136
- await screen.findByText('GitHub Integration')
183
+ // See above: wait for the check response, not the optimistic card heading.
184
+ await screen.findByText("You're up to date!")
137
185
 
138
186
  const repo = screen.getByLabelText('Repository') as HTMLInputElement
139
187
  fireEvent.change(repo, { target: { value: 'not a repo' } })
@@ -1,3 +1,5 @@
1
+ import { sanitizeHtml } from '@prenta/core/sanitize'
2
+
1
3
  import type { PageSection } from '../../../lib/page-editor-service.js'
2
4
  import { bool, str, type PostRenderContext } from './parts.js'
3
5
 
@@ -6,10 +8,9 @@ import { bool, str, type PostRenderContext } from './parts.js'
6
8
  * post's own Body field (passed via context); otherwise it renders the
7
9
  * section's own `body` copy.
8
10
  *
9
- * The Body field is a `richText` field (sanitized HTML on write), so this
10
- * canvas preview renders it as HTML — exactly like the public site's
11
- * article-body renderer — to stay WYSIWYG. It previews the author's own
12
- * in-progress draft; the server sanitizes (sanitize-html) on save.
11
+ * The canvas previews the author's in-progress draft, which has not been
12
+ * through the write-time sanitizer yet. Strip disallowed tags here with the
13
+ * cms-core allow-list before injecting HTML.
13
14
  */
14
15
  export function ArticleBodySection({
15
16
  section,
@@ -20,7 +21,8 @@ export function ArticleBodySection({
20
21
  }) {
21
22
  const c = section.content
22
23
  const source = str(c, 'source', 'field')
23
- const html = source === 'field' ? (context?.body ?? '') : str(c, 'body')
24
+ const rawHtml = source === 'field' ? (context?.body ?? '') : str(c, 'body')
25
+ const html = sanitizeHtml(rawHtml)
24
26
  const wide = str(c, 'width', 'normal') === 'wide'
25
27
  const showToc = bool(c, 'showToc')
26
28
 
@@ -32,6 +32,7 @@
32
32
  * renderer falls back to the built-in for that type, or to the placeholder if
33
33
  * the type has no built-in, instead of taking down the editor.
34
34
  */
35
+ import { sanitizeHtml } from '@prenta/core/sanitize'
35
36
  import { createContext, useContext, type ReactNode } from 'react'
36
37
 
37
38
  /** Structural mirror of `RenderImageProps` from `@prenta/sections-react`. */
@@ -109,13 +110,12 @@ export type AdminSectionRenderer = (props: AdminSectionRendererProps) => ReactNo
109
110
  export type AdminSectionRendererMap = Record<string, AdminSectionRenderer>
110
111
 
111
112
  /**
112
- * Render options passed to every admin renderer. Rich text is sanitized on
113
- * write by cms-core, so the canvas (same trust domain as the public default)
114
- * uses the identity sanitizer; images render as plain lazy `<img>` since the
115
- * editor canvas has no next/image pipeline.
113
+ * Render options passed to every admin renderer. Draft HTML is sanitized
114
+ * with the cms-core allow-list before any renderer injects it. Images render
115
+ * as plain lazy `<img>` since the editor canvas has no next/image pipeline.
116
116
  */
117
117
  export const ADMIN_RENDER_OPTIONS: AdminSectionRendererProps['options'] = {
118
- sanitizeHtml: (html) => html,
118
+ sanitizeHtml,
119
119
  baseUrl: '',
120
120
  // Never grants the priority slot. On a public page exactly one image may
121
121
  // load eagerly; in the canvas nothing competes for bandwidth and every