@kywi-software/core 0.10.0 → 0.12.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/AGENT-PATTERNS.md +111 -23
- package/README.md +67 -0
- package/dist/admin/content-form.d.ts.map +1 -1
- package/dist/admin/content-form.js +97 -10
- package/dist/admin/content-form.js.map +1 -1
- package/dist/admin/layout-editor/component-actions.d.ts +219 -22
- package/dist/admin/layout-editor/component-actions.d.ts.map +1 -1
- package/dist/admin/layout-editor/component-actions.js +356 -36
- package/dist/admin/layout-editor/component-actions.js.map +1 -1
- package/dist/admin/layout-editor/component-fork.d.ts +224 -0
- package/dist/admin/layout-editor/component-fork.d.ts.map +1 -0
- package/dist/admin/layout-editor/component-fork.js +342 -0
- package/dist/admin/layout-editor/component-fork.js.map +1 -0
- package/dist/admin/layout-editor/editor-canvas.d.ts +28 -7
- package/dist/admin/layout-editor/editor-canvas.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-canvas.js +145 -32
- package/dist/admin/layout-editor/editor-canvas.js.map +1 -1
- package/dist/admin/layout-editor/editor-dialog.d.ts +164 -0
- package/dist/admin/layout-editor/editor-dialog.d.ts.map +1 -0
- package/dist/admin/layout-editor/editor-dialog.js +195 -0
- package/dist/admin/layout-editor/editor-dialog.js.map +1 -0
- package/dist/admin/layout-editor/editor-state.d.ts +76 -0
- package/dist/admin/layout-editor/editor-state.d.ts.map +1 -1
- package/dist/admin/layout-editor/editor-state.js +47 -1
- package/dist/admin/layout-editor/editor-state.js.map +1 -1
- package/dist/admin/layout-editor/editor.css +366 -4
- package/dist/admin/layout-editor/index.d.ts +10 -1
- package/dist/admin/layout-editor/index.d.ts.map +1 -1
- package/dist/admin/layout-editor/index.js +11 -1
- package/dist/admin/layout-editor/index.js.map +1 -1
- package/dist/admin/layout-editor/inline-text.d.ts +56 -4
- package/dist/admin/layout-editor/inline-text.d.ts.map +1 -1
- package/dist/admin/layout-editor/inline-text.js +134 -11
- package/dist/admin/layout-editor/inline-text.js.map +1 -1
- package/dist/admin/layout-editor/layout-editor.d.ts +58 -1
- package/dist/admin/layout-editor/layout-editor.d.ts.map +1 -1
- package/dist/admin/layout-editor/layout-editor.js +160 -190
- package/dist/admin/layout-editor/layout-editor.js.map +1 -1
- package/dist/admin/layout-editor/linked-component.d.ts +39 -0
- package/dist/admin/layout-editor/linked-component.d.ts.map +1 -0
- package/dist/admin/layout-editor/linked-component.js +42 -0
- package/dist/admin/layout-editor/linked-component.js.map +1 -0
- package/dist/admin/layout-editor/module-overlay.d.ts +36 -14
- package/dist/admin/layout-editor/module-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/module-overlay.js +233 -25
- package/dist/admin/layout-editor/module-overlay.js.map +1 -1
- package/dist/admin/layout-editor/node-lookup.d.ts +35 -0
- package/dist/admin/layout-editor/node-lookup.d.ts.map +1 -1
- package/dist/admin/layout-editor/node-lookup.js +91 -0
- package/dist/admin/layout-editor/node-lookup.js.map +1 -1
- package/dist/admin/layout-editor/overlay-shell.d.ts.map +1 -1
- package/dist/admin/layout-editor/overlay-shell.js +150 -32
- package/dist/admin/layout-editor/overlay-shell.js.map +1 -1
- package/dist/admin/layout-editor/props-panel.d.ts +54 -1
- package/dist/admin/layout-editor/props-panel.d.ts.map +1 -1
- package/dist/admin/layout-editor/props-panel.js +119 -10
- package/dist/admin/layout-editor/props-panel.js.map +1 -1
- package/dist/admin/layout-editor/section-overlay.d.ts +16 -1
- package/dist/admin/layout-editor/section-overlay.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-overlay.js +21 -11
- package/dist/admin/layout-editor/section-overlay.js.map +1 -1
- package/dist/admin/layout-editor/section-template-picker.d.ts +27 -1
- package/dist/admin/layout-editor/section-template-picker.d.ts.map +1 -1
- package/dist/admin/layout-editor/section-template-picker.js +71 -17
- package/dist/admin/layout-editor/section-template-picker.js.map +1 -1
- package/dist/admin/layout-editor/section-variant-container-config.js +5 -5
- package/dist/admin/layout-editor/section-variant-container-config.js.map +1 -1
- package/dist/admin/layout-editor/use-chrome-flip.d.ts +111 -11
- package/dist/admin/layout-editor/use-chrome-flip.d.ts.map +1 -1
- package/dist/admin/layout-editor/use-chrome-flip.js +233 -33
- package/dist/admin/layout-editor/use-chrome-flip.js.map +1 -1
- package/dist/admin/layout-editor/use-component-connect.d.ts +61 -0
- package/dist/admin/layout-editor/use-component-connect.d.ts.map +1 -0
- package/dist/admin/layout-editor/use-component-connect.js +292 -0
- package/dist/admin/layout-editor/use-component-connect.js.map +1 -0
- package/dist/admin/layout-editor/use-component-fork.d.ts +123 -0
- package/dist/admin/layout-editor/use-component-fork.d.ts.map +1 -0
- package/dist/admin/layout-editor/use-component-fork.js +346 -0
- package/dist/admin/layout-editor/use-component-fork.js.map +1 -0
- package/dist/admin/layout-editor/use-component-library.d.ts +94 -0
- package/dist/admin/layout-editor/use-component-library.d.ts.map +1 -0
- package/dist/admin/layout-editor/use-component-library.js +55 -0
- package/dist/admin/layout-editor/use-component-library.js.map +1 -0
- package/dist/admin/registry.d.ts +11 -2
- package/dist/admin/registry.d.ts.map +1 -1
- package/dist/admin/registry.js +40 -4
- package/dist/admin/registry.js.map +1 -1
- package/dist/admin/server.d.ts +1 -0
- package/dist/admin/server.d.ts.map +1 -1
- package/dist/admin/server.js +5 -0
- package/dist/admin/server.js.map +1 -1
- package/dist/admin/styles/admin.css +57 -1
- package/dist/admin/surfaces/components/component-edit.d.ts +3 -0
- package/dist/admin/surfaces/components/component-edit.d.ts.map +1 -0
- package/dist/admin/surfaces/components/component-edit.js +235 -0
- package/dist/admin/surfaces/components/component-edit.js.map +1 -0
- package/dist/admin/surfaces/components/components-data.d.ts +153 -0
- package/dist/admin/surfaces/components/components-data.d.ts.map +1 -0
- package/dist/admin/surfaces/components/components-data.js +154 -0
- package/dist/admin/surfaces/components/components-data.js.map +1 -0
- package/dist/admin/surfaces/components/components-list.d.ts +3 -0
- package/dist/admin/surfaces/components/components-list.d.ts.map +1 -0
- package/dist/admin/surfaces/components/components-list.js +196 -0
- package/dist/admin/surfaces/components/components-list.js.map +1 -0
- package/dist/admin/surfaces/components/definition-canvas.d.ts +73 -0
- package/dist/admin/surfaces/components/definition-canvas.d.ts.map +1 -0
- package/dist/admin/surfaces/components/definition-canvas.js +207 -0
- package/dist/admin/surfaces/components/definition-canvas.js.map +1 -0
- package/dist/admin/surfaces/layouts/layout-editor-client.d.ts.map +1 -1
- package/dist/admin/surfaces/layouts/layout-editor-client.js +14 -2
- package/dist/admin/surfaces/layouts/layout-editor-client.js.map +1 -1
- package/dist/admin/surfaces/layouts/layouts-data.d.ts +3 -14
- package/dist/admin/surfaces/layouts/layouts-data.d.ts.map +1 -1
- package/dist/admin/surfaces/layouts/layouts-data.js +4 -7
- package/dist/admin/surfaces/layouts/layouts-data.js.map +1 -1
- package/dist/admin/surfaces/layouts/layouts-list.d.ts.map +1 -1
- package/dist/admin/surfaces/layouts/layouts-list.js +16 -22
- package/dist/admin/surfaces/layouts/layouts-list.js.map +1 -1
- package/dist/admin/surfaces/settings/admin-features.d.ts.map +1 -1
- package/dist/admin/surfaces/settings/admin-features.js +1 -0
- package/dist/admin/surfaces/settings/admin-features.js.map +1 -1
- package/dist/api/bulk-routes.d.ts.map +1 -1
- package/dist/api/bulk-routes.js +25 -0
- package/dist/api/bulk-routes.js.map +1 -1
- package/dist/api/changeset-routes.d.ts.map +1 -1
- package/dist/api/changeset-routes.js +17 -3
- package/dist/api/changeset-routes.js.map +1 -1
- package/dist/api/component-routes.d.ts.map +1 -1
- package/dist/api/component-routes.js +228 -8
- package/dist/api/component-routes.js.map +1 -1
- package/dist/api/content-routes.d.ts.map +1 -1
- package/dist/api/content-routes.js +64 -0
- package/dist/api/content-routes.js.map +1 -1
- package/dist/api/feed-routes.d.ts +6 -6
- package/dist/api/graphql/resolvers.d.ts +4 -4
- package/dist/api/layout-routes.d.ts.map +1 -1
- package/dist/api/layout-routes.js +4 -17
- package/dist/api/layout-routes.js.map +1 -1
- package/dist/api/layout-write-guard.d.ts +80 -0
- package/dist/api/layout-write-guard.d.ts.map +1 -0
- package/dist/api/layout-write-guard.js +88 -0
- package/dist/api/layout-write-guard.js.map +1 -0
- package/dist/api/section-template-routes.d.ts.map +1 -1
- package/dist/api/section-template-routes.js +11 -0
- package/dist/api/section-template-routes.js.map +1 -1
- package/dist/api/translation-routes.d.ts.map +1 -1
- package/dist/api/translation-routes.js +8 -0
- package/dist/api/translation-routes.js.map +1 -1
- package/dist/api/version-routes.d.ts.map +1 -1
- package/dist/api/version-routes.js +20 -8
- package/dist/api/version-routes.js.map +1 -1
- package/dist/api/workflow-routes.d.ts.map +1 -1
- package/dist/api/workflow-routes.js +53 -8
- package/dist/api/workflow-routes.js.map +1 -1
- package/dist/db/analytics-queries.d.ts +3 -3
- package/dist/db/apply-snapshot.d.ts +75 -8
- package/dist/db/apply-snapshot.d.ts.map +1 -1
- package/dist/db/apply-snapshot.js +104 -8
- package/dist/db/apply-snapshot.js.map +1 -1
- package/dist/db/audit-queries.d.ts +12 -12
- package/dist/db/category-queries.d.ts +3 -3
- package/dist/db/changeset-audit-queries.d.ts +4 -4
- package/dist/db/changeset-queries.d.ts +7 -7
- package/dist/db/comment-queries.d.ts +8 -8
- package/dist/db/comment-subscription-queries.d.ts +2 -2
- package/dist/db/component-instance-queries.d.ts +197 -0
- package/dist/db/component-instance-queries.d.ts.map +1 -0
- package/dist/db/component-instance-queries.js +405 -0
- package/dist/db/component-instance-queries.js.map +1 -0
- package/dist/db/content-queries.d.ts.map +1 -1
- package/dist/db/content-queries.js +39 -2
- package/dist/db/content-queries.js.map +1 -1
- package/dist/db/extension-queries.d.ts +7 -7
- package/dist/db/feed-queries.d.ts +3 -3
- package/dist/db/index.d.ts +1 -0
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -0
- package/dist/db/index.js.map +1 -1
- package/dist/db/layout-queries.d.ts +13 -1
- package/dist/db/layout-queries.d.ts.map +1 -1
- package/dist/db/layout-queries.js +52 -7
- package/dist/db/layout-queries.js.map +1 -1
- package/dist/db/locale-queries.d.ts.map +1 -1
- package/dist/db/locale-queries.js +0 -0
- package/dist/db/locale-queries.js.map +1 -1
- package/dist/db/media-queries.d.ts +12 -12
- package/dist/db/revision-queries.d.ts +3 -3
- package/dist/db/site-admin-queries.d.ts +2 -2
- package/dist/db/site-membership-queries.d.ts +3 -3
- package/dist/db/site-plugin-queries.d.ts +2 -2
- package/dist/db/site-queries.d.ts +6 -6
- package/dist/db/version-queries.d.ts +12 -12
- package/dist/db/webhook-queries.d.ts +11 -11
- package/dist/db/workflow-queries.d.ts +6 -6
- package/dist/db/workflow-queries.d.ts.map +1 -1
- package/dist/db/workflow-queries.js +11 -2
- package/dist/db/workflow-queries.js.map +1 -1
- package/dist/layout/comments-module.d.ts.map +1 -1
- package/dist/layout/comments-module.js +6 -2
- package/dist/layout/comments-module.js.map +1 -1
- package/dist/layout/component-refs.d.ts +465 -0
- package/dist/layout/component-refs.d.ts.map +1 -0
- package/dist/layout/component-refs.js +848 -0
- package/dist/layout/component-refs.js.map +1 -0
- package/dist/layout/index.d.ts +2 -1
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +1 -0
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/module-permissions.d.ts +49 -1
- package/dist/layout/module-permissions.d.ts.map +1 -1
- package/dist/layout/module-permissions.js +117 -6
- package/dist/layout/module-permissions.js.map +1 -1
- package/dist/layout/module-render-context.d.ts +32 -0
- package/dist/layout/module-render-context.d.ts.map +1 -1
- package/dist/layout/module-render-context.js +32 -1
- package/dist/layout/module-render-context.js.map +1 -1
- package/dist/layout/modules-extended.d.ts.map +1 -1
- package/dist/layout/modules-extended.js +21 -11
- package/dist/layout/modules-extended.js.map +1 -1
- package/dist/layout/modules.d.ts.map +1 -1
- package/dist/layout/modules.js +15 -10
- package/dist/layout/modules.js.map +1 -1
- package/dist/layout/renderer.d.ts +51 -4
- package/dist/layout/renderer.d.ts.map +1 -1
- package/dist/layout/renderer.js +36 -38
- package/dist/layout/renderer.js.map +1 -1
- package/dist/layout/types.d.ts +42 -0
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/layout/types.js +54 -11
- package/dist/layout/types.js.map +1 -1
- package/dist/schema/base.d.ts +1 -1
- package/dist/schema/components.d.ts +150 -0
- package/dist/schema/components.d.ts.map +1 -1
- package/dist/schema/components.js +50 -4
- package/dist/schema/components.js.map +1 -1
- package/dist/scope/inline-editing.d.ts.map +1 -1
- package/dist/scope/inline-editing.js +10 -0
- package/dist/scope/inline-editing.js.map +1 -1
- package/dist/scope/sticky-offset.d.ts +185 -0
- package/dist/scope/sticky-offset.d.ts.map +1 -0
- package/dist/scope/sticky-offset.js +432 -0
- package/dist/scope/sticky-offset.js.map +1 -0
- package/dist/site/styles.css +41 -3
- package/dist/versioning/scheduler.d.ts +8 -0
- package/dist/versioning/scheduler.d.ts.map +1 -1
- package/dist/versioning/scheduler.js +21 -7
- package/dist/versioning/scheduler.js.map +1 -1
- package/package.json +1 -1
package/AGENT-PATTERNS.md
CHANGED
|
@@ -76,7 +76,7 @@ the full proposal — ten minutes of sign-off beats re-entering fifty nodes.
|
|
|
76
76
|
| Hardcode a list (testimonials, logos, posts, products, team) | Folder of nodes + a Feed + the Feed Display module (§1) |
|
|
77
77
|
| Create one-off pages with identical field shapes | A custom content type + one node per item (§2) |
|
|
78
78
|
| Filter/group content by topic, industry, audience | Categories & tags, queried by feeds (§3) |
|
|
79
|
-
| Copy-paste a CTA/banner/snippet across pages | Save it as a reusable component (§4) |
|
|
79
|
+
| Copy-paste a CTA/banner/snippet across pages | Save it as a reusable component — linked, so one edit updates them all (§4) |
|
|
80
80
|
| Write page sections as JSX | Build them as layout sections/modules the owner can edit (§5) |
|
|
81
81
|
| Build a page as one column of full-width headings and paragraphs | Compose: section bands, columns, object modules (§5) |
|
|
82
82
|
| Put a custom module's editable copy in a `json` prop | One `text`/`textarea`/`richText` prop per field the owner edits (§5) |
|
|
@@ -179,14 +179,99 @@ Anything designed once and used on multiple pages — CTA bands, promo banners,
|
|
|
179
179
|
newsletter signup blocks, campaign snippets, "as seen in" strips — should be a
|
|
180
180
|
**reusable component**, not copy-pasted sections.
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
A component is a **live source**, not a snapshot. Every page that places one
|
|
183
|
+
holds a *link* to it (a `componentId` on the node), and the link is resolved on
|
|
184
|
+
every request — so writing a new definition changes every page that uses it
|
|
185
|
+
immediately, published pages included, with nothing to re-publish. That is the
|
|
186
|
+
whole point, and it is also the thing to be careful with.
|
|
187
|
+
|
|
188
|
+
**What can be one.** A single module, a whole section (band), or a personalized /
|
|
189
|
+
A-B tested band with its variants intact — the three `type` values are `module`,
|
|
190
|
+
`section` and `variantContainer`. A band's variants travel with it; which arm a
|
|
191
|
+
visitor sees is still decided per request, per placement, so the same personalized
|
|
192
|
+
component can sit on five pages and target correctly on each.
|
|
193
|
+
|
|
194
|
+
**In the editors** (front-of-site overlay or the admin Layout tab): select the
|
|
195
|
+
module, section or band → **Save as component** → name it. The selection is
|
|
196
|
+
replaced in place by a linked instance, marked with an outline and a pill naming
|
|
197
|
+
the component. Place it again from the section picker's *Components* rows.
|
|
198
|
+
|
|
199
|
+
**Editing a linked instance ends in a question.** There are no per-prop overrides
|
|
200
|
+
in v1: an instance renders its component exactly. So when you edit one and save,
|
|
201
|
+
Kywi asks, naming the blast radius ("updates 3 instances across 2 pages, including
|
|
202
|
+
published"):
|
|
203
|
+
|
|
204
|
+
- **Save and update all instances** — the edit becomes the component. Every
|
|
205
|
+
instance everywhere follows it, live.
|
|
206
|
+
- **Save to this page only** — this instance *detaches*: it keeps the edit as its
|
|
207
|
+
own independent content and stops following the component. Everything else is
|
|
208
|
+
untouched.
|
|
209
|
+
|
|
210
|
+
**Rule of thumb:** the second time you paste the same section, stop and make it a
|
|
211
|
+
component. The owner should be able to update the sitewide CTA in one place, ten
|
|
212
|
+
minutes before their webinar.
|
|
213
|
+
|
|
214
|
+
**Managing them:** *Components* in the admin nav is the library — every component
|
|
215
|
+
with the number of pages using it, rename, an editing canvas for the definition,
|
|
216
|
+
and delete. Deleting **detaches every instance first**: each page keeps rendering
|
|
217
|
+
exactly what it rendered, as its own copy, and stops following.
|
|
218
|
+
|
|
219
|
+
### Components over the API
|
|
220
|
+
|
|
221
|
+
The admin UI is one client; the endpoints are the contract. `siteId` is optional
|
|
222
|
+
and defaults to the resolved site — components are **per-site** in v1, and
|
|
223
|
+
per-site authorization applies.
|
|
224
|
+
|
|
225
|
+
1. `POST /api/v1/components` — `{ name, type, definition, moduleType?, description? }`.
|
|
226
|
+
The `definition` is the node itself: a `section` component stores the whole
|
|
227
|
+
`LayoutSection`, a `variantContainer` the whole container, a `module` component
|
|
228
|
+
stores `{ props, style }` and names its `moduleType`.
|
|
229
|
+
2. **Place it** by writing a layout that carries the link. There is no separate
|
|
230
|
+
"insert" endpoint: `PUT /api/v1/content/:type/:id/layout` with the definition's
|
|
231
|
+
own shape plus `componentId` on the node, giving the placement a fresh `id` of
|
|
232
|
+
its own. The node's inline content is the *fallback copy* — what renders if the
|
|
233
|
+
component ever stops resolving — so write the definition's content there, not a
|
|
234
|
+
placeholder.
|
|
235
|
+
|
|
236
|
+
**An `ab_test` placement must mint its own `experimentId`.** A
|
|
237
|
+
`variantContainer` definition normally carries an `experimentId` — the one the
|
|
238
|
+
band had on the page it was saved from — because `mode: 'ab_test'` without one
|
|
239
|
+
is invalid, so it cannot simply be left out of the definition. A placement that
|
|
240
|
+
names no binding of its own **inherits the definition's**. Place that component
|
|
241
|
+
on three pages and all three arms report into a *single* experiment run; a
|
|
242
|
+
`winnerId` that travels the same way locks every placement to a finished test,
|
|
243
|
+
permanently serving one arm on pages nobody tested. So when you write an
|
|
244
|
+
`ab_test` placement: set a **fresh `experimentId`** on the placement node (any
|
|
245
|
+
uuid — one run per placement is the whole model) and **never copy `winnerId`**.
|
|
246
|
+
The editors do exactly this (`buildConnectedVariantContainerNode`), and a
|
|
247
|
+
definition saved from an editor carries no `winnerId` at all; a definition
|
|
248
|
+
POSTed straight over the API can, so read what you are placing. A
|
|
249
|
+
`personalization` container has no binding and needs none.
|
|
250
|
+
3. `GET /api/v1/components/:id/usage` — where it is placed (`instances`, `pages`,
|
|
251
|
+
the page ids and node ids, and trashed pages counted separately). This is the
|
|
252
|
+
blast radius; read it *before* changing a definition.
|
|
253
|
+
4. `PUT /api/v1/components/:id` — a body with `definition` **is the propagation
|
|
254
|
+
write**: every linked instance renders the new definition on its next request.
|
|
255
|
+
A body with only `name` / `description` is a rename and changes nothing any page
|
|
256
|
+
renders.
|
|
257
|
+
5. `POST /api/v1/components/:id/detach-all` — materialize the definition into every
|
|
258
|
+
page that links it and drop the links; each page keeps rendering identically.
|
|
259
|
+
6. `DELETE /api/v1/components/:id` — refuses (409) while instances exist; add
|
|
260
|
+
`?detachAll=1` to detach-then-delete. `GET /api/v1/components?withUsage=1` folds
|
|
261
|
+
the counts into a list, in one read.
|
|
262
|
+
|
|
263
|
+
**Two refusals to design around.** A component definition may not link to another
|
|
264
|
+
component (one level only, deliberately: a nested link is invisible to usage counts
|
|
265
|
+
and the delete gate, so the editors *materialize* an inner component when you save
|
|
266
|
+
a section that contains one — the inner instance stops following its own source, and
|
|
267
|
+
the dialog says so). And a stored layout that a live layout could not hold is
|
|
268
|
+
rejected at the door, because detaching would copy it into every page that links it.
|
|
269
|
+
|
|
270
|
+
**Component writes bypass approvals in v1.** They are not routed through
|
|
271
|
+
changesets or the review workflow — a `PUT` with a definition is live on publish
|
|
272
|
+
pages the moment it returns. On a site with an editorial workflow, treat a
|
|
273
|
+
definition write as a publish, not a draft: no draft, schedule or review step
|
|
274
|
+
holds it back. Changeset integration is a follow-up.
|
|
190
275
|
|
|
191
276
|
## 5. Page layouts & modules — not hardcoded JSX
|
|
192
277
|
|
|
@@ -441,20 +526,22 @@ interchangeable.
|
|
|
441
526
|
page is complete with JavaScript off. This is the default for anything the
|
|
442
527
|
server can know — rule-based audiences (UTM, referrer, the `kywi_audience`
|
|
443
528
|
pin) and A/B experiments. Since 0.6.3 it also renders in the layout editor as
|
|
444
|
-
a badged block with
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
529
|
+
a badged block with a **variant switcher** (internal editor code —
|
|
530
|
+
`activeArms`, the `VariantArm` type, `data-arm-id` — still says "arm"; the
|
|
531
|
+
persisted layout field is `variants`), each variant editable with the
|
|
532
|
+
ordinary section/column/module tools, so server-resolution no longer costs
|
|
533
|
+
the owner their editing surface. Since kywi-cms#119, the owner doesn't need
|
|
534
|
+
an agent to hand-author this shape at all: the section chrome — in the
|
|
535
|
+
admin layout editor and in the in-place front-of-site overlay alike —
|
|
536
|
+
offers **Personalize this section** / **A/B test** actions that wrap an
|
|
537
|
+
ordinary section into a section-level variant container on the spot,
|
|
538
|
+
seeding the default variant from the section's current content and
|
|
539
|
+
opening straight into its config (audience/experiment picker included).
|
|
540
|
+
An agent only needs to reach for `update_layout` when scripting bulk
|
|
541
|
+
changes or building a container the UI can't reach.
|
|
455
542
|
- **Module-level** — the `variantContainer` *module*, placed in a column; its
|
|
456
|
-
|
|
457
|
-
content}]`). Every
|
|
543
|
+
variants are HTML strings (`defaultContent`, `variants: [{audienceId, label,
|
|
544
|
+
content}]`). Every variant ships in the HTML (default visible, the rest
|
|
458
545
|
`display:none`) and the browser runtime reveals the matching one. Use it
|
|
459
546
|
**only for signals the server cannot read at first paint** — in practice, a
|
|
460
547
|
self-ID answer held in `localStorage`.
|
|
@@ -586,7 +673,8 @@ early, so ask at scoping time.
|
|
|
586
673
|
- Creating types and entering content before the owner signed off on a
|
|
587
674
|
content model.
|
|
588
675
|
- A hardcoded array of testimonials/logos/posts in a page component.
|
|
589
|
-
- The same CTA JSX pasted on four pages
|
|
676
|
+
- The same CTA JSX pasted on four pages — or the same section duplicated on four
|
|
677
|
+
pages in the editor, which the owner then has to update four times (§4).
|
|
590
678
|
- A `<form>` that POSTs to a hand-rolled route (or nowhere).
|
|
591
679
|
- Marketing imagery in `/public`.
|
|
592
680
|
- A "blog" that is a folder of `.mdx` files the owner can't edit.
|
package/README.md
CHANGED
|
@@ -166,6 +166,73 @@ text, button …) types straight on the page (committed on blur through the same
|
|
|
166
166
|
deselects, and a persistent **Done** leaves — confirming first if there are
|
|
167
167
|
unsaved changes.
|
|
168
168
|
|
|
169
|
+
### Sticky site headers and the editor's bars
|
|
170
|
+
|
|
171
|
+
Two fixed bars can appear at the top of a public page for a signed-in editor:
|
|
172
|
+
the **browse-mode toolbar** (`.kywi-edit-toolbar`, 44px) and the **edit-mode
|
|
173
|
+
bar** (`.kywi-overlay-toolbar`, 46px). Each publishes its height as a token on
|
|
174
|
+
`:root`, so a theme never has to hard-code either number:
|
|
175
|
+
|
|
176
|
+
| Token | Bar | Default |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `--kywi-browse-bar-height` | browse-mode toolbar | `44px` |
|
|
179
|
+
| `--kywi-overlay-bar-height` | edit-mode bar | `46px` |
|
|
180
|
+
|
|
181
|
+
Both are declared on `:root` by **`./site/styles.css`**, which every public page
|
|
182
|
+
loads — and again, identically, by the layout editor's `editor.css`. The second
|
|
183
|
+
sheet alone is not enough to rely on: a generated app pulls the admin design
|
|
184
|
+
system in behind `next/dynamic`, so `editor.css` is absent from the page until
|
|
185
|
+
someone opens the overlay editor, and browse mode would resolve neither token.
|
|
186
|
+
Give every `var()` on these tokens the matching px fallback anyway (`44px` /
|
|
187
|
+
`46px`) — a `var()` whose token is undefined falls back to the property's
|
|
188
|
+
*initial* value, and `top: auto` un-sticks an element outright rather than
|
|
189
|
+
merely mis-offsetting it.
|
|
190
|
+
|
|
191
|
+
While a bar is up, the document is offset by its height, which is enough for a
|
|
192
|
+
header in normal flow. It is **not** enough for `position: sticky; top: 0` (or
|
|
193
|
+
`position: fixed; top: 0`): document padding moves an element's flow position,
|
|
194
|
+
not the line it pins to once it sticks — so a sticky nav would pin to viewport
|
|
195
|
+
y=0 underneath the bar as soon as the reader scrolled. The editor therefore
|
|
196
|
+
detects top-anchored elements when a bar mounts and patches each one's inline
|
|
197
|
+
`top` to the bar's token for the session, restoring the original exactly on the
|
|
198
|
+
way out (`packages/core/src/scope/sticky-offset.ts`). Nothing is required of a
|
|
199
|
+
theme for this to work.
|
|
200
|
+
|
|
201
|
+
Two things are worth knowing if you are writing one:
|
|
202
|
+
|
|
203
|
+
- **A sticky element that mounts after the editor opens is not offset.** The
|
|
204
|
+
scan runs once, when the bar mounts; a cookie bar on a timer or a menu that is
|
|
205
|
+
`display: none` at that moment keeps its own `top`. Anchor those yourself off
|
|
206
|
+
the class the bar puts on `<html>` — `kywi-frontend-edit--browsing` or
|
|
207
|
+
`kywi-frontend-edit--editing`, which are also how you special-case anything
|
|
208
|
+
else for an editing session:
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
.my-late-sticky { top: 0; }
|
|
212
|
+
.kywi-frontend-edit--browsing .my-late-sticky { top: var(--kywi-browse-bar-height, 44px); }
|
|
213
|
+
.kywi-frontend-edit--editing .my-late-sticky { top: var(--kywi-overlay-bar-height, 46px); }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
A second tier keeps its own offset on top of the bar:
|
|
217
|
+
|
|
218
|
+
```css
|
|
219
|
+
.my-late-subnav { top: 64px; }
|
|
220
|
+
.kywi-frontend-edit--browsing .my-late-subnav { top: calc(var(--kywi-browse-bar-height, 44px) + 64px); }
|
|
221
|
+
.kywi-frontend-edit--editing .my-late-subnav { top: calc(var(--kywi-overlay-bar-height, 46px) + 64px); }
|
|
222
|
+
```
|
|
223
|
+
- **A whole sticky STACK moves, not just its first tier.** Anything with a
|
|
224
|
+
computed `top` within 4px of zero is offset by one bar height; anything
|
|
225
|
+
between there and 200px — a sub-nav parked under the header at `top: 64px`,
|
|
226
|
+
the docs-site pattern — is offset by the bar *plus its own original value*, so
|
|
227
|
+
the gaps a theme built survive the session. Moving only the first tier would
|
|
228
|
+
make the header cover the sub-nav, an overlap the offset itself would have
|
|
229
|
+
created.
|
|
230
|
+
- **Further down than that, and inside a scroll pane, is left alone.** A sticky
|
|
231
|
+
filter panel at `top: 320px` is anchored to page content rather than to the
|
|
232
|
+
top of the viewport, and a sticky table head inside an `overflow: auto` pane
|
|
233
|
+
pins to that pane, not to the window — the bar never covers either, so neither
|
|
234
|
+
moves.
|
|
235
|
+
|
|
169
236
|
A host must therefore NOT wrap the editor in `.kywi-admin-shell`: that class is
|
|
170
237
|
the admin design system's base + reset and would re-typeset the page being
|
|
171
238
|
edited. Pass the page's own body-wrapper class as `pageClassName` instead, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-form.d.ts","sourceRoot":"","sources":["../../src/admin/content-form.tsx"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"content-form.d.ts","sourceRoot":"","sources":["../../src/admin/content-form.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
3
|
import { FieldRenderer, CategoryTreePicker, TagInput, RelatedContentPicker } from './field-components.js';
|
|
4
4
|
import { MediaPickerField } from './media-picker-field.js';
|
|
5
5
|
import { TiptapEditor } from './tiptap-editor.js';
|
|
@@ -54,10 +54,41 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
54
54
|
// old local-state behavior unchanged.
|
|
55
55
|
const [uncontrolledActiveTab, setUncontrolledActiveTab] = useState('content');
|
|
56
56
|
const activeTab = controlledActiveTab ?? uncontrolledActiveTab;
|
|
57
|
+
// Does the Layout tab have its own save path? (kywi-cms#147 T4 review, I1.) A
|
|
58
|
+
// saved page does: the layout goes through `PUT .../layout`, the endpoint that
|
|
59
|
+
// validates it, gates admin-only modules and keeps the component-instance index in
|
|
60
|
+
// step. A page being CREATED does not — there is no row to PUT against — so for
|
|
61
|
+
// that one case the layout still rides the create payload, which is the only door
|
|
62
|
+
// it has.
|
|
63
|
+
const canPersistLayout = Boolean(extensionFetcher && initialValues?.id != null);
|
|
64
|
+
// The Layout tab has unsaved edits. Tracked as a FLAG rather than by staging the
|
|
65
|
+
// document into `values`: staging made a provisional layout permanent (the page
|
|
66
|
+
// form submitted it, and a tab switch remounted the editor with it as the new
|
|
67
|
+
// baseline), which is the second save door the component save fork could not
|
|
68
|
+
// guard — see use-component-fork.tsx.
|
|
69
|
+
const [layoutDirty, setLayoutDirty] = useState(false);
|
|
70
|
+
// Bumped on every unsaved layout edit (kywi-cms#147 T4 review round 2). A PUT to
|
|
71
|
+
// the layout endpoint can take long enough for the canvas — which stays
|
|
72
|
+
// interactive throughout — to receive another edit mid-flight; that edit's
|
|
73
|
+
// `onChange` sets `layoutDirty` true, but without this counter the PUT's own
|
|
74
|
+
// `.then` would unconditionally clear it right back to false on resolve,
|
|
75
|
+
// silently discarding the "still dirty" signal the tab-switch confirm and the
|
|
76
|
+
// beforeunload guard depend on. `handleLayoutSave` snapshots this counter
|
|
77
|
+
// before awaiting the PUT and only clears `layoutDirty` if nothing bumped it
|
|
78
|
+
// since — see the callback below.
|
|
79
|
+
const layoutChangeCounter = useRef(0);
|
|
57
80
|
const setActiveTab = useCallback((tab) => {
|
|
81
|
+
// Leaving the Layout tab unmounts the editor and its unsaved work with it, so
|
|
82
|
+
// ask — the same bargain the editor's own Cancel and the overlay's Done strike.
|
|
83
|
+
if (activeTab === 'layout' && tab !== 'layout' && layoutDirty &&
|
|
84
|
+
!window.confirm("This layout has changes you haven't saved yet. Leaving the Layout tab discards them.")) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (tab !== 'layout')
|
|
88
|
+
setLayoutDirty(false);
|
|
58
89
|
setUncontrolledActiveTab(tab);
|
|
59
90
|
onActiveTabChange?.(tab);
|
|
60
|
-
}, [onActiveTabChange]);
|
|
91
|
+
}, [onActiveTabChange, activeTab, layoutDirty]);
|
|
61
92
|
const [approvalComment, setApprovalComment] = useState('');
|
|
62
93
|
// Names of custom fields currently reporting invalid input (e.g. unparseable
|
|
63
94
|
// JSON). Save is disabled while any is present.
|
|
@@ -124,8 +155,11 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
124
155
|
}, [users]);
|
|
125
156
|
// Browser-level unsaved-changes guard: warn before unload (close/reload/
|
|
126
157
|
// cross-origin nav) whenever there are pending edits.
|
|
158
|
+
// Unsaved layout edits count (kywi-cms#147 T4 review, I1): they are no longer
|
|
159
|
+
// staged into `values`, so `isDirty` alone would leave the Layout tab's work
|
|
160
|
+
// unguarded against a reload.
|
|
127
161
|
useEffect(() => {
|
|
128
|
-
if (!isDirty)
|
|
162
|
+
if (!isDirty && !layoutDirty)
|
|
129
163
|
return;
|
|
130
164
|
function handleBeforeUnload(e) {
|
|
131
165
|
e.preventDefault();
|
|
@@ -134,7 +168,7 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
134
168
|
}
|
|
135
169
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
136
170
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
137
|
-
}, [isDirty]);
|
|
171
|
+
}, [isDirty, layoutDirty]);
|
|
138
172
|
const handleFieldValidity = useCallback((name, valid) => {
|
|
139
173
|
setInvalidFields(prev => {
|
|
140
174
|
if (valid) {
|
|
@@ -168,10 +202,19 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
168
202
|
// MARK_SAVED baseline — `false` on either failure path below, true/undefined
|
|
169
203
|
// (the implicit return) once the PUT actually lands.
|
|
170
204
|
const handleLayoutSave = useCallback(async (layout) => {
|
|
171
|
-
handleChange('layout', layout);
|
|
172
205
|
const contentId = initialValues?.id;
|
|
173
|
-
if (!extensionFetcher || contentId == null)
|
|
206
|
+
if (!extensionFetcher || contentId == null) {
|
|
207
|
+
// Create mode: nothing to PUT against, so the only thing that can be done is
|
|
208
|
+
// stage it for the create payload (kywi-cms#19). Reported as NOT saved, which
|
|
209
|
+
// is the truth — the editor keeps its unsaved state until the page is created.
|
|
210
|
+
handleChange('layout', layout);
|
|
174
211
|
return false;
|
|
212
|
+
}
|
|
213
|
+
// Snapshot BEFORE the await (kywi-cms#147 T4 review round 2): if the canvas
|
|
214
|
+
// fires another edit while this PUT is in flight, that edit's `onChange` bumps
|
|
215
|
+
// `layoutChangeCounter` past what was seen here, and the resolve below must
|
|
216
|
+
// leave `layoutDirty` alone rather than clear a still-unsaved edit.
|
|
217
|
+
const seen = layoutChangeCounter.current;
|
|
175
218
|
setLayoutSaveState({ status: 'saving' });
|
|
176
219
|
try {
|
|
177
220
|
const res = await extensionFetcher(`/api/v1/content/${contentType.name}/${contentId}/layout`, {
|
|
@@ -183,6 +226,20 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
183
226
|
throw new Error(body?.error?.message ?? `Layout save failed (${res.status})`);
|
|
184
227
|
}
|
|
185
228
|
setLayoutSaveState({ status: 'saved' });
|
|
229
|
+
// The layout is now what the server holds, so the copy this form keeps for
|
|
230
|
+
// seeding the editor on a tab switch moves with it — via `setValues`, not
|
|
231
|
+
// `handleChange`, because this is reconciliation with a completed save and not
|
|
232
|
+
// a user edit (same distinction as the expiration-user effect above). This
|
|
233
|
+
// reconciliation happens regardless of a mid-flight edit — it's still correct
|
|
234
|
+
// to know what the server has — but `layoutDirty` only clears if nothing
|
|
235
|
+
// edited the canvas since the PUT started; otherwise the mid-flight edit's
|
|
236
|
+
// dirty signal would be silently wiped and the tab-switch/beforeunload guards
|
|
237
|
+
// would go quiet on unsaved work. A FAILED save deliberately stages nothing:
|
|
238
|
+
// the editor keeps the document, the error is on screen, and nothing
|
|
239
|
+
// unvalidated is left behind to ride another write.
|
|
240
|
+
setValues(prev => ({ ...prev, layout }));
|
|
241
|
+
if (layoutChangeCounter.current === seen)
|
|
242
|
+
setLayoutDirty(false);
|
|
186
243
|
return true;
|
|
187
244
|
}
|
|
188
245
|
catch (err) {
|
|
@@ -193,15 +250,35 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
193
250
|
return false;
|
|
194
251
|
}
|
|
195
252
|
}, [extensionFetcher, initialValues?.id, contentType.name, handleChange]);
|
|
253
|
+
/**
|
|
254
|
+
* The payload for a CONTENT write from this form.
|
|
255
|
+
*
|
|
256
|
+
* `layout` is excluded whenever the Layout tab can save itself (kywi-cms#147 T4
|
|
257
|
+
* review, I1). It is not a form field: it is edited by a component with its own
|
|
258
|
+
* save button, its own endpoint and its own unsaved-changes guard, and letting it
|
|
259
|
+
* ride this payload meant a page-level Save could persist a layout the layout
|
|
260
|
+
* editor had never been asked about — in particular an edited linked component
|
|
261
|
+
* instance, which the save fork exists to ask about, and which for a section or
|
|
262
|
+
* container ends up in a fallback copy that is stored and never rendered.
|
|
263
|
+
*
|
|
264
|
+
* In create mode it still rides along, because there is no other door (see
|
|
265
|
+
* `canPersistLayout`).
|
|
266
|
+
*/
|
|
267
|
+
const contentPayload = useCallback(() => {
|
|
268
|
+
if (!canPersistLayout)
|
|
269
|
+
return formDataToApiPayload(values, contentType.fields);
|
|
270
|
+
const { layout: _editorOwned, ...rest } = values;
|
|
271
|
+
return formDataToApiPayload(rest, contentType.fields);
|
|
272
|
+
}, [values, contentType.fields, canPersistLayout]);
|
|
196
273
|
const handleSubmit = useCallback((e) => {
|
|
197
274
|
e.preventDefault();
|
|
198
275
|
if (invalidFields.size > 0)
|
|
199
276
|
return;
|
|
200
|
-
onSubmit(
|
|
277
|
+
onSubmit(contentPayload());
|
|
201
278
|
// Optimistically clear the guard — the submit path owns persistence and
|
|
202
279
|
// (in the reference app) refetches on success, remounting the form.
|
|
203
280
|
setIsDirty(false);
|
|
204
|
-
}, [
|
|
281
|
+
}, [contentPayload, onSubmit, invalidFields]);
|
|
205
282
|
const canApprove = APPROVAL_ROLES.has(userRole ?? '');
|
|
206
283
|
const isAdmin = ADMIN_ROLES.has(userRole ?? '');
|
|
207
284
|
return (_jsxs("form", { onSubmit: handleSubmit, className: "kywi-content-form", children: [_jsx("div", { className: "kywi-form-header", children: _jsx("h1", { children: mode === 'create' ? `New ${contentType.label}` : `Edit ${contentType.label}` }) }), isLockedByOther && (_jsxs("div", { className: "kywi-lock-banner", role: "alert", children: [_jsx("span", { className: "kywi-lock-banner-text", children: `This content is locked by ${lockInfo.lockedBy}. Editing is disabled.` }), canForceUnlock && onForceUnlock && (_jsx("button", { type: "button", onClick: () => onForceUnlock(), className: "kywi-btn kywi-btn-danger kywi-force-unlock-btn", children: "Force Unlock" }))] })), _jsx("div", { className: "kywi-tab-bar", role: "tablist", children: TABS.map(tab => (_jsx("button", { type: "button", role: "tab", "aria-selected": activeTab === tab.id, "aria-controls": `kywi-tab-panel-${tab.id}`, className: `kywi-tab${activeTab === tab.id ? ' kywi-tab-active' : ''}`, onClick: () => setActiveTab(tab.id), children: tab.label }, tab.id))) }), _jsxs("div", { className: "kywi-tab-content", children: [activeTab === 'content' && (_jsxs("div", { id: "kywi-tab-panel-content", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsxs("label", { htmlFor: "field-title", className: "kywi-field-label", children: ["Title", titleRequired && _jsx("span", { className: "kywi-required", "aria-hidden": "true", children: " *" })] }), _jsx("input", { id: "field-title", type: "text", name: "title", value: String(values.title ?? ''), onChange: e => handleChange('title', e.target.value), className: "kywi-input", required: titleRequired })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-slug", className: "kywi-field-label", children: "Slug" }), _jsx("input", { id: "field-slug", type: "text", name: "slug", value: String(values.slug ?? ''), onChange: e => handleChange('slug', e.target.value), className: "kywi-input", placeholder: "Auto-generated from title if empty" })] }), contentType.baseType === 'content' && (_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-body", className: "kywi-field-label", children: "Body" }), _jsx(TiptapEditor, { id: "field-body", value: String(values.body ?? ''), onChange: html => handleChange('body', html), className: "kywi-richtext" })] })), _jsxs("div", { className: "kywi-field", "data-field-name": "featuredImageId", children: [_jsx("label", { className: "kywi-field-label", htmlFor: "field-featuredImageId", children: "Featured Image" }), _jsx(MediaPickerField, { id: "field-featuredImageId", value: typeof values.featuredImageId === 'string' ? values.featuredImageId : null, accept: "image/*", onChange: mediaId => handleChange('featuredImageId', mediaId) })] }), customFields.length > 0 && (_jsx("div", { className: "kywi-form-custom-fields", children: customFields.map(field => (_jsx(FieldRenderer, { field: field, value: values[field.name], onChange: val => handleChange(field.name, val), onValidityChange: valid => handleFieldValidity(field.name, valid), ...(resolveRelationship ? { resolveRelationship } : {}) }, field.name))) })), extensionFetcher && initialValues?.id != null && (_jsx(ExtensionSets, { contentId: String(initialValues.id), baseTypes: [contentType.name, contentType.baseType], fetcher: extensionFetcher }))] })), activeTab === 'publishing' && (_jsxs("div", { id: "kywi-tab-panel-publishing", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-status", className: "kywi-field-label", children: "Status" }), _jsxs("select", { id: "field-status", name: "status", value: String(values.status ?? 'draft'), onChange: e => handleChange('status', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "draft", children: "Draft" }), _jsx("option", { value: "pending", children: "Pending Review" }), _jsx("option", { value: "approved", children: "Approved" }), _jsx("option", { value: "published", children: "Published" }), _jsx("option", { value: "archived", children: "Archived" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-publishedAt", className: "kywi-field-label", children: "Published At" }), _jsx("input", { id: "field-publishedAt", type: "datetime-local", name: "publishedAt", value: String(values.publishedAt ?? ''), onChange: e => handleChange('publishedAt', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-unpublishAt", className: "kywi-field-label", children: "Unpublish At" }), _jsx("input", { id: "field-unpublishAt", type: "datetime-local", name: "unpublishAt", value: String(values.unpublishAt ?? ''), onChange: e => handleChange('unpublishAt', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-expiresAt", className: "kywi-field-label", children: "Expires At" }), _jsx("input", { id: "field-expiresAt", type: "datetime-local", name: "expiresAt", value: String(values.expiresAt ?? ''), onChange: e => handleChange('expiresAt', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-featureInSection", className: "kywi-field-label", children: "Feature In Section" }), _jsxs("select", { id: "field-featureInSection", name: "featureInSection", value: String(values.featureInSection ?? 'no'), onChange: e => handleChange('featureInSection', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "no", children: "No" }), _jsx("option", { value: "yes", children: "Yes" }), _jsx("option", { value: "scheduled", children: "Scheduled" })] })] }), String(values.featureInSection) === 'scheduled' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-featureStartAt", className: "kywi-field-label", children: "Feature Start" }), _jsx("input", { id: "field-featureStartAt", type: "datetime-local", name: "featureStartAt", value: String(values.featureStartAt ?? ''), onChange: e => handleChange('featureStartAt', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-featureEndAt", className: "kywi-field-label", children: "Feature End" }), _jsx("input", { id: "field-featureEndAt", type: "datetime-local", name: "featureEndAt", value: String(values.featureEndAt ?? ''), onChange: e => handleChange('featureEndAt', e.target.value), className: "kywi-input" })] })] })), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-expirationNotifyUser", className: "kywi-field-label", children: "Expiration Notify User" }), users.length > 0 ? (_jsxs("select", { id: "field-expirationNotifyUser", name: "expirationNotifyUser", value: String(values.expirationNotifyUser ?? ''), onChange: e => handleChange('expirationNotifyUser', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "", children: "\u2014 No one \u2014" }), users.map(u => (_jsx("option", { value: u.id, children: u.label }, u.id)))] })) : (_jsx("input", { id: "field-expirationNotifyUser", type: "text", name: "expirationNotifyUser", value: String(values.expirationNotifyUser ?? ''), onChange: e => handleChange('expirationNotifyUser', e.target.value), className: "kywi-input", placeholder: "User ID to notify on expiration" }))] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-internalNotes", className: "kywi-field-label", children: "Internal Notes" }), _jsx("textarea", { id: "field-internalNotes", name: "internalNotes", value: String(values.internalNotes ?? ''), onChange: e => handleChange('internalNotes', e.target.value), className: "kywi-textarea", rows: 4 })] }), openChangesets.length > 0 && (_jsxs("div", { className: "kywi-changeset-section", "data-testid": "changeset-dropdown", children: [_jsx("h3", { className: "kywi-section-heading", children: "Add to Changeset" }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-changeset", className: "kywi-field-label", children: "Changeset" }), _jsxs("select", { id: "field-changeset", className: "kywi-select", defaultValue: "", onChange: e => {
|
|
@@ -211,9 +288,19 @@ export function ContentEditForm({ contentType, initialValues, hostModules, hostM
|
|
|
211
288
|
// Carry the editor's current edits so Submit-for-Review no
|
|
212
289
|
// longer discards unsaved changes (#45.3) and can fork a
|
|
213
290
|
// pending revision of published content (#42).
|
|
214
|
-
onApprovalAction?.('submit', approvalComment,
|
|
291
|
+
onApprovalAction?.('submit', approvalComment, contentPayload()), children: "Submit for Review" }), isAdmin && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-primary", onClick: () => onApprovalAction?.('approve', approvalComment), children: "Approve" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-danger", onClick: () => onApprovalAction?.('reject', approvalComment), children: "Reject" })] }))] })] })), approvalHistory.length > 0 && (_jsxs("div", { className: "kywi-approval-history", children: [_jsx("h3", { className: "kywi-section-heading", children: "Approval History" }), _jsx("ul", { className: "kywi-approval-list", children: approvalHistory.map(entry => (_jsxs("li", { className: "kywi-approval-entry", children: [_jsx("strong", { children: entry.action }), " by ", entry.by, " at ", entry.at, entry.comment && _jsx("p", { children: entry.comment })] }, entry.id))) })] }))] })), activeTab === 'navigation' && (_jsxs("div", { id: "kywi-tab-panel-navigation", role: "tabpanel", className: "kywi-tab-panel", children: [_jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "isNav", checked: Boolean(values.isNav ?? false), onChange: e => handleChange('isNav', e.target.checked), className: "kywi-checkbox" }), ' ', "Show in Navigation"] }) }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-menuTitle", className: "kywi-field-label", children: "Menu Title" }), _jsx("input", { id: "field-menuTitle", type: "text", name: "menuTitle", value: String(values.menuTitle ?? ''), onChange: e => handleChange('menuTitle', e.target.value), className: "kywi-input", placeholder: "Override title in navigation" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-parentId", className: "kywi-field-label", children: "Parent" }), _jsx("input", { id: "field-parentId", type: "text", name: "parentId", value: String(values.parentId ?? ''), onChange: e => handleChange('parentId', e.target.value), className: "kywi-input", placeholder: "Content ID of parent" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-sortOrder", className: "kywi-field-label", children: "Sort Order" }), _jsx("input", { id: "field-sortOrder", type: "number", name: "sortOrder", value: String(values.sortOrder ?? ''), onChange: e => handleChange('sortOrder', e.target.value === '' ? '' : Number(e.target.value)), className: "kywi-input" })] }), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "openInNewWindow", checked: Boolean(values.openInNewWindow ?? false), onChange: e => handleChange('openInNewWindow', e.target.checked), className: "kywi-checkbox" }), ' ', "Open in New Window"] }) }), groups.length > 0 && (_jsxs("div", { className: "kywi-field", children: [_jsx("span", { className: "kywi-field-label", children: "Nav Visibility by Group" }), _jsx("div", { className: "kywi-group-visibility", children: groups.map(g => {
|
|
215
292
|
const vis = values.navVisibility ?? {};
|
|
216
293
|
return (_jsxs("label", { className: "kywi-field-label kywi-group-vis-label", children: [_jsx("input", { type: "checkbox", checked: vis[g.id] ?? true, onChange: e => handleChange('navVisibility', { ...vis, [g.id]: e.target.checked }), className: "kywi-checkbox" }), ' ', g.name] }, g.id));
|
|
217
|
-
}) })] })), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "excludeFromSitemap", checked: Boolean(values.excludeFromSitemap ?? false), onChange: e => handleChange('excludeFromSitemap', e.target.checked), className: "kywi-checkbox" }), ' ', "Exclude from Sitemap"] }) }), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "excludeFromSearch", checked: Boolean(values.excludeFromSearch ?? false), onChange: e => handleChange('excludeFromSearch', e.target.checked), className: "kywi-checkbox" }), ' ', "Exclude from search & AX"] }) }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-urlOverride", className: "kywi-field-label", children: "URL Override" }), _jsx("input", { id: "field-urlOverride", type: "text", name: "urlOverride", value: String(values.urlOverride ?? ''), onChange: e => handleChange('urlOverride', e.target.value), className: "kywi-input", placeholder: "/custom-path" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-redirectUrl", className: "kywi-field-label", children: "Redirect URL" }), _jsx("input", { id: "field-redirectUrl", type: "text", name: "redirectUrl", value: String(values.redirectUrl ?? ''), onChange: e => handleChange('redirectUrl', e.target.value), className: "kywi-input", placeholder: "https://..." })] })] })), activeTab === 'seo' && (_jsxs("div", { id: "kywi-tab-panel-seo", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaTitle", className: "kywi-field-label", children: "Meta Title" }), _jsx("input", { id: "field-metaTitle", type: "text", name: "metaTitle", value: String(values.metaTitle ?? ''), onChange: e => handleChange('metaTitle', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaDescription", className: "kywi-field-label", children: "Meta Description" }), _jsx("textarea", { id: "field-metaDescription", name: "metaDescription", value: String(values.metaDescription ?? ''), onChange: e => handleChange('metaDescription', e.target.value), className: "kywi-textarea", rows: 3 })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaKeywords", className: "kywi-field-label", children: "Meta Keywords" }), _jsx("input", { id: "field-metaKeywords", type: "text", name: "metaKeywords", value: String(values.metaKeywords ?? ''), onChange: e => handleChange('metaKeywords', e.target.value), className: "kywi-input", placeholder: "keyword1, keyword2, ..." })] }), _jsxs("div", { className: "kywi-field", "data-field-name": "metaImageId", children: [_jsx("label", { htmlFor: "field-metaImageId", className: "kywi-field-label", children: "OG Image" }), _jsx(MediaPickerField, { id: "field-metaImageId", value: typeof values.metaImageId === 'string' ? values.metaImageId : null, accept: "image/*", onChange: mediaId => handleChange('metaImageId', mediaId) })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-canonicalUrl", className: "kywi-field-label", children: "Canonical URL" }), _jsx("input", { id: "field-canonicalUrl", type: "text", name: "canonicalUrl", value: String(values.canonicalUrl ?? ''), onChange: e => handleChange('canonicalUrl', e.target.value), className: "kywi-input", placeholder: "https://..." })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-robotsIndex", className: "kywi-field-label", children: "Robots Index" }), _jsxs("select", { id: "field-robotsIndex", name: "robotsIndex", value: String(values.robotsIndex ?? 'index'), onChange: e => handleChange('robotsIndex', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "index", children: "Index" }), _jsx("option", { value: "noindex", children: "No Index" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-robotsFollow", className: "kywi-field-label", children: "Robots Follow" }), _jsxs("select", { id: "field-robotsFollow", name: "robotsFollow", value: String(values.robotsFollow ?? 'follow'), onChange: e => handleChange('robotsFollow', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "follow", children: "Follow" }), _jsx("option", { value: "nofollow", children: "No Follow" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-changeFreq", className: "kywi-field-label", children: "Change Frequency" }), _jsxs("select", { id: "field-changeFreq", name: "changeFreq", value: String(values.changeFreq ?? ''), onChange: e => handleChange('changeFreq', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "", children: "-- Select --" }), _jsx("option", { value: "always", children: "Always" }), _jsx("option", { value: "hourly", children: "Hourly" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Yearly" }), _jsx("option", { value: "never", children: "Never" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-sitemapPriority", className: "kywi-field-label", children: "Sitemap Priority (0-1)" }), _jsx("input", { id: "field-sitemapPriority", type: "number", name: "sitemapPriority", min: "0", max: "1", step: "0.1", value: String(values.sitemapPriority ?? ''), onChange: e => handleChange('sitemapPriority', e.target.value === '' ? '' : Number(e.target.value)), className: "kywi-input" })] })] })), activeTab === 'layout' && (_jsxs("div", { id: "kywi-tab-panel-layout", role: "tabpanel", className: "kywi-tab-panel", children: [layoutSaveState.status === 'error' && (_jsx("div", { className: "kywi-field-error", role: "alert", children: layoutSaveState.message })), layoutSaveState.status === 'saved' && (_jsx("p", { className: "kywi-field-hint", role: "status", children: "Layout saved." })), _jsx(LayoutEditor, { initialLayout: values.layout ?? { regions: {} }, contentId: String(initialValues?.id ?? ''), contentType: contentType.name, moduleRegistry: layoutModuleRegistry, moduleComponents: layoutModuleComponents, audiences: audiences, onChange: layout => handleChange('layout', layout), onSave: handleLayoutSave, ...(userRole ? { userRole } : {}) })] })), activeTab === 'categories-tags' && (_jsxs("div", { id: "kywi-tab-panel-categories-tags", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { className: "kywi-field-label", children: "Categories" }), _jsx(CategoryTreePicker, { categories: availableCategories, selected: values.categoryIds ?? [], onChange: ids => handleChange('categoryIds', ids) })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { className: "kywi-field-label", children: "Tags" }), _jsx(TagInput, { tags: Array.isArray(values.tags) ? values.tags : [], suggestions: availableTags, onChange: tags => handleChange('tags', tags) })] }), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "featuredInCategory", checked: Boolean(values.featuredInCategory ?? false), onChange: e => handleChange('featuredInCategory', e.target.checked), className: "kywi-checkbox" }), ' ', "Featured in Category"] }) })] })), activeTab === 'related-content' && (_jsx("div", { id: "kywi-tab-panel-related-content", role: "tabpanel", className: "kywi-tab-panel", children: _jsx(RelatedContentPicker, { selected: values.relatedContentIds ?? [], onChange: ids => handleChange('relatedContentIds', ids) }) })), activeTab === 'permissions' && (_jsx("div", { id: "kywi-tab-panel-permissions", role: "tabpanel", className: "kywi-tab-panel", children: _jsx(ContentPermissionsTab, { contentId: String(initialValues?.id ?? ''), groups: groups, userRole: userRole }) })), activeTab === 'version-history' && (_jsx("div", { id: "kywi-tab-panel-version-history", role: "tabpanel", className: "kywi-tab-panel", children: versions.length === 0 ? (_jsx("p", { className: "kywi-empty-state", children: "No versions recorded yet." })) : (_jsxs("table", { className: "kywi-table kywi-version-table", "data-testid": "version-list", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Version" }), _jsx("th", { children: "Created" }), _jsx("th", { children: "By" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: versions.map(v => (_jsxs("tr", { "data-version-id": v.id, children: [_jsxs("td", { children: ["v", v.version] }), _jsx("td", { children: v.createdAt }), _jsx("td", { children: v.createdBy }), _jsxs("td", { className: "kywi-version-actions", children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-secondary kywi-btn-sm", "data-action": "diff", onClick: () => onDiffVersion?.(v.id), children: "Diff" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-secondary kywi-btn-sm", "data-action": "restore", onClick: () => onRestoreVersion?.(v.id), children: "Restore" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-danger kywi-btn-sm", "data-action": "delete", onClick: () => onDeleteVersion?.(v.id), children: "Delete" })] })] }, v.id))) })] })) }))] }), _jsxs("div", { className: "kywi-form-actions", children: [_jsx("button", { type: "submit", className: "kywi-btn kywi-btn-primary", disabled: isSubmitting || isLockedByOther || hasInvalidField, children: isSubmitting ? 'Saving...' : mode === 'create' ? 'Create' : 'Save' }), onCancel && (_jsx("button", { type: "button", onClick: onCancel, className: "kywi-btn kywi-btn-secondary", children: "Cancel" })), hasInvalidField && (_jsx("span", { className: "kywi-field-error", role: "alert", children: "Fix the highlighted fields before saving." }))] })] }));
|
|
294
|
+
}) })] })), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "excludeFromSitemap", checked: Boolean(values.excludeFromSitemap ?? false), onChange: e => handleChange('excludeFromSitemap', e.target.checked), className: "kywi-checkbox" }), ' ', "Exclude from Sitemap"] }) }), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "excludeFromSearch", checked: Boolean(values.excludeFromSearch ?? false), onChange: e => handleChange('excludeFromSearch', e.target.checked), className: "kywi-checkbox" }), ' ', "Exclude from search & AX"] }) }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-urlOverride", className: "kywi-field-label", children: "URL Override" }), _jsx("input", { id: "field-urlOverride", type: "text", name: "urlOverride", value: String(values.urlOverride ?? ''), onChange: e => handleChange('urlOverride', e.target.value), className: "kywi-input", placeholder: "/custom-path" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-redirectUrl", className: "kywi-field-label", children: "Redirect URL" }), _jsx("input", { id: "field-redirectUrl", type: "text", name: "redirectUrl", value: String(values.redirectUrl ?? ''), onChange: e => handleChange('redirectUrl', e.target.value), className: "kywi-input", placeholder: "https://..." })] })] })), activeTab === 'seo' && (_jsxs("div", { id: "kywi-tab-panel-seo", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaTitle", className: "kywi-field-label", children: "Meta Title" }), _jsx("input", { id: "field-metaTitle", type: "text", name: "metaTitle", value: String(values.metaTitle ?? ''), onChange: e => handleChange('metaTitle', e.target.value), className: "kywi-input" })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaDescription", className: "kywi-field-label", children: "Meta Description" }), _jsx("textarea", { id: "field-metaDescription", name: "metaDescription", value: String(values.metaDescription ?? ''), onChange: e => handleChange('metaDescription', e.target.value), className: "kywi-textarea", rows: 3 })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-metaKeywords", className: "kywi-field-label", children: "Meta Keywords" }), _jsx("input", { id: "field-metaKeywords", type: "text", name: "metaKeywords", value: String(values.metaKeywords ?? ''), onChange: e => handleChange('metaKeywords', e.target.value), className: "kywi-input", placeholder: "keyword1, keyword2, ..." })] }), _jsxs("div", { className: "kywi-field", "data-field-name": "metaImageId", children: [_jsx("label", { htmlFor: "field-metaImageId", className: "kywi-field-label", children: "OG Image" }), _jsx(MediaPickerField, { id: "field-metaImageId", value: typeof values.metaImageId === 'string' ? values.metaImageId : null, accept: "image/*", onChange: mediaId => handleChange('metaImageId', mediaId) })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-canonicalUrl", className: "kywi-field-label", children: "Canonical URL" }), _jsx("input", { id: "field-canonicalUrl", type: "text", name: "canonicalUrl", value: String(values.canonicalUrl ?? ''), onChange: e => handleChange('canonicalUrl', e.target.value), className: "kywi-input", placeholder: "https://..." })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-robotsIndex", className: "kywi-field-label", children: "Robots Index" }), _jsxs("select", { id: "field-robotsIndex", name: "robotsIndex", value: String(values.robotsIndex ?? 'index'), onChange: e => handleChange('robotsIndex', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "index", children: "Index" }), _jsx("option", { value: "noindex", children: "No Index" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-robotsFollow", className: "kywi-field-label", children: "Robots Follow" }), _jsxs("select", { id: "field-robotsFollow", name: "robotsFollow", value: String(values.robotsFollow ?? 'follow'), onChange: e => handleChange('robotsFollow', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "follow", children: "Follow" }), _jsx("option", { value: "nofollow", children: "No Follow" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-changeFreq", className: "kywi-field-label", children: "Change Frequency" }), _jsxs("select", { id: "field-changeFreq", name: "changeFreq", value: String(values.changeFreq ?? ''), onChange: e => handleChange('changeFreq', e.target.value), className: "kywi-select", children: [_jsx("option", { value: "", children: "-- Select --" }), _jsx("option", { value: "always", children: "Always" }), _jsx("option", { value: "hourly", children: "Hourly" }), _jsx("option", { value: "daily", children: "Daily" }), _jsx("option", { value: "weekly", children: "Weekly" }), _jsx("option", { value: "monthly", children: "Monthly" }), _jsx("option", { value: "yearly", children: "Yearly" }), _jsx("option", { value: "never", children: "Never" })] })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { htmlFor: "field-sitemapPriority", className: "kywi-field-label", children: "Sitemap Priority (0-1)" }), _jsx("input", { id: "field-sitemapPriority", type: "number", name: "sitemapPriority", min: "0", max: "1", step: "0.1", value: String(values.sitemapPriority ?? ''), onChange: e => handleChange('sitemapPriority', e.target.value === '' ? '' : Number(e.target.value)), className: "kywi-input" })] })] })), activeTab === 'layout' && (_jsxs("div", { id: "kywi-tab-panel-layout", role: "tabpanel", className: "kywi-tab-panel", children: [layoutSaveState.status === 'error' && (_jsx("div", { className: "kywi-field-error", role: "alert", children: layoutSaveState.message })), layoutSaveState.status === 'saved' && (_jsx("p", { className: "kywi-field-hint", role: "status", children: "Layout saved." })), _jsx(LayoutEditor, { initialLayout: values.layout ?? { regions: {} }, contentId: String(initialValues?.id ?? ''), contentType: contentType.name, moduleRegistry: layoutModuleRegistry, moduleComponents: layoutModuleComponents, audiences: audiences,
|
|
295
|
+
// Records THAT there are unsaved layout edits, not the document
|
|
296
|
+
// itself (kywi-cms#147 T4 review, I1 — see `layoutDirty`). In create
|
|
297
|
+
// mode there is no layout endpoint to save through, so the old
|
|
298
|
+
// staging is what keeps the work reachable by the create payload.
|
|
299
|
+
// The counter bump (T4 review round 2) is what lets `handleLayoutSave`
|
|
300
|
+
// tell a mid-flight edit apart from "nothing changed since I started
|
|
301
|
+
// saving" — see `layoutChangeCounter`.
|
|
302
|
+
onChange: canPersistLayout
|
|
303
|
+
? () => { layoutChangeCounter.current += 1; setLayoutDirty(true); }
|
|
304
|
+
: layout => handleChange('layout', layout), onSave: handleLayoutSave, ...(userRole ? { userRole } : {}) })] })), activeTab === 'categories-tags' && (_jsxs("div", { id: "kywi-tab-panel-categories-tags", role: "tabpanel", className: "kywi-tab-panel", children: [_jsxs("div", { className: "kywi-field", children: [_jsx("label", { className: "kywi-field-label", children: "Categories" }), _jsx(CategoryTreePicker, { categories: availableCategories, selected: values.categoryIds ?? [], onChange: ids => handleChange('categoryIds', ids) })] }), _jsxs("div", { className: "kywi-field", children: [_jsx("label", { className: "kywi-field-label", children: "Tags" }), _jsx(TagInput, { tags: Array.isArray(values.tags) ? values.tags : [], suggestions: availableTags, onChange: tags => handleChange('tags', tags) })] }), _jsx("div", { className: "kywi-field kywi-field-checkbox", children: _jsxs("label", { className: "kywi-field-label", children: [_jsx("input", { type: "checkbox", name: "featuredInCategory", checked: Boolean(values.featuredInCategory ?? false), onChange: e => handleChange('featuredInCategory', e.target.checked), className: "kywi-checkbox" }), ' ', "Featured in Category"] }) })] })), activeTab === 'related-content' && (_jsx("div", { id: "kywi-tab-panel-related-content", role: "tabpanel", className: "kywi-tab-panel", children: _jsx(RelatedContentPicker, { selected: values.relatedContentIds ?? [], onChange: ids => handleChange('relatedContentIds', ids) }) })), activeTab === 'permissions' && (_jsx("div", { id: "kywi-tab-panel-permissions", role: "tabpanel", className: "kywi-tab-panel", children: _jsx(ContentPermissionsTab, { contentId: String(initialValues?.id ?? ''), groups: groups, userRole: userRole }) })), activeTab === 'version-history' && (_jsx("div", { id: "kywi-tab-panel-version-history", role: "tabpanel", className: "kywi-tab-panel", children: versions.length === 0 ? (_jsx("p", { className: "kywi-empty-state", children: "No versions recorded yet." })) : (_jsxs("table", { className: "kywi-table kywi-version-table", "data-testid": "version-list", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Version" }), _jsx("th", { children: "Created" }), _jsx("th", { children: "By" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: versions.map(v => (_jsxs("tr", { "data-version-id": v.id, children: [_jsxs("td", { children: ["v", v.version] }), _jsx("td", { children: v.createdAt }), _jsx("td", { children: v.createdBy }), _jsxs("td", { className: "kywi-version-actions", children: [_jsx("button", { type: "button", className: "kywi-btn kywi-btn-secondary kywi-btn-sm", "data-action": "diff", onClick: () => onDiffVersion?.(v.id), children: "Diff" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-secondary kywi-btn-sm", "data-action": "restore", onClick: () => onRestoreVersion?.(v.id), children: "Restore" }), _jsx("button", { type: "button", className: "kywi-btn kywi-btn-danger kywi-btn-sm", "data-action": "delete", onClick: () => onDeleteVersion?.(v.id), children: "Delete" })] })] }, v.id))) })] })) }))] }), _jsxs("div", { className: "kywi-form-actions", children: [_jsx("button", { type: "submit", className: "kywi-btn kywi-btn-primary", disabled: isSubmitting || isLockedByOther || hasInvalidField, children: isSubmitting ? 'Saving...' : mode === 'create' ? 'Create' : 'Save' }), onCancel && (_jsx("button", { type: "button", onClick: onCancel, className: "kywi-btn kywi-btn-secondary", children: "Cancel" })), hasInvalidField && (_jsx("span", { className: "kywi-field-error", role: "alert", children: "Fix the highlighted fields before saving." }))] })] }));
|
|
218
305
|
}
|
|
219
306
|
//# sourceMappingURL=content-form.js.map
|