@simmalugnt-se/payload-editor-assistant 0.12.1 → 0.12.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.2
4
+
5
+ - Batch links remove their parameters from Next’s router state as well as browser history, so
6
+ refreshing cannot reopen a finished batch. Field prompts use the same URL integration.
7
+ - Collapsing the assistant preserves batch progress instead of resetting completed cards.
8
+ - Batch revision checks treat schema-defined empty values consistently, so the first draft saved
9
+ in one language does not falsely invalidate another language’s proposal on Postgres. Real content
10
+ changes anywhere in the allowlisted form still refuse the save.
11
+
3
12
  ## 0.12.1
4
13
 
5
14
  - The review lists groups in the schema's order (a group's own position, blocks and rows by their
@@ -1 +1 @@
1
- export declare function EditorAssistantAside(): import("react").JSX.Element | null;
1
+ export declare function EditorAssistantAside(): import("react").JSX.Element;
@@ -441,11 +441,8 @@ export function EditorAssistantAside() {
441
441
  void send(draft);
442
442
  }
443
443
  };
444
- // Closed: nothing is shown. The component stays mounted, so an applied preview survives.
445
- if (!open) {
446
- return null;
447
- }
448
- return (_jsxs("aside", { className: "ea-aside", "aria-label": copy.assistant, [KEEP_SELECTION_ATTR]: "", children: [_jsxs("div", { className: "ea-aside__head", children: [_jsxs("div", { className: "ea-aside__title", children: [_jsx("h2", { children: copy.assistant }), bridge ? _jsx("p", { className: "ea-aside__context", children: contextLine(bridge, config) }) : null] }), !batch && (messages.length > 0 || pendingApproval) && !pendingApproval?.applied ? (_jsx(Button, { buttonStyle: "subtle", margin: false, onClick: () => resetConversation(), size: "medium", children: copy.newChat })) : null, _jsx(Button, { "aria-label": copy.collapseLabel, buttonStyle: "icon-label", className: "ea-aside__collapse", icon: _jsx(ChevronIcon, { direction: "right" }), margin: false, onClick: () => setOpen(false), tooltip: copy.collapse })] }), batch ? (_jsx("div", { className: "ea-aside__body", children: _jsx(BatchPanel, { batch: batch, onClose: () => {
444
+ // Keep batch cards mounted while collapsed, so reopening never resets saved cards to tasks.
445
+ return (_jsxs("aside", { className: "ea-aside", "aria-label": copy.assistant, hidden: !open, [KEEP_SELECTION_ATTR]: "", children: [_jsxs("div", { className: "ea-aside__head", children: [_jsxs("div", { className: "ea-aside__title", children: [_jsx("h2", { children: copy.assistant }), bridge ? _jsx("p", { className: "ea-aside__context", children: contextLine(bridge, config) }) : null] }), !batch && (messages.length > 0 || pendingApproval) && !pendingApproval?.applied ? (_jsx(Button, { buttonStyle: "subtle", margin: false, onClick: () => resetConversation(), size: "medium", children: copy.newChat })) : null, _jsx(Button, { "aria-label": copy.collapseLabel, buttonStyle: "icon-label", className: "ea-aside__collapse", icon: _jsx(ChevronIcon, { direction: "right" }), margin: false, onClick: () => setOpen(false), tooltip: copy.collapse })] }), batch ? (_jsx("div", { className: "ea-aside__body", children: _jsx(BatchPanel, { batch: batch, onClose: () => {
449
446
  releaseTakenBatches();
450
447
  setBatch(null);
451
448
  } }, batch.id) })) : (_jsxs("div", { className: "ea-aside__body", children: [_jsxs("div", { className: "ea-messages", ref: threadRef, onScroll: onThreadScroll, children: [messages.length === 0 ? (_jsx("div", { className: "ea-msg ea-msg--assistant", children: introText(view, bridge, collections, config, copy) })) : null, messages.map((message) => (_jsxs("div", { className: `ea-msg ea-msg--${message.role}`, children: [message.role === "assistant" ? _jsx(ReplyText, { text: message.text }) : message.text, message.candidates?.length ? (_jsx("div", { className: "ea-actions", children: message.candidates.map((candidate) => (_jsx(Button, { buttonStyle: "pill", margin: false, onClick: () => {
@@ -40,7 +40,7 @@ export function EditorAssistantDocumentBridge() {
40
40
  return;
41
41
  }
42
42
  url.searchParams.delete(ASK_PARAM);
43
- window.history.replaceState(window.history.state, "", url);
43
+ window.history.replaceState(null, "", url);
44
44
  if (prompt) {
45
45
  setPrefill(prompt);
46
46
  setOpen(true);
@@ -32,7 +32,8 @@ export function useBatchLink() {
32
32
  for (const name of [BATCH_PARAM, BATCH_SCOPE_PARAM, ASK_PARAM]) {
33
33
  url.searchParams.delete(name);
34
34
  }
35
- window.history.replaceState(window.history.state, "", url.toString());
35
+ // Next copies its internal state itself; passing it back skips the router URL update.
36
+ window.history.replaceState(null, "", url.toString());
36
37
  if (repeated) {
37
38
  // The batch is still in the panel, which the editor closed: show it again.
38
39
  setOpen(true);
@@ -1,3 +1,4 @@
1
+ import { hashBatchRevision } from "../form/batch-revision.js";
1
2
  import { changedPaths } from "../form/changed-paths.js";
2
3
  import { compileOperations } from "../form/compile.js";
3
4
  import { hashFormRevision } from "../form/hash.js";
@@ -188,10 +189,13 @@ export async function applyBatchTarget(req, options, input) {
188
189
  if (!saved) {
189
190
  return fail("tool_denied", "Document is not accessible.");
190
191
  }
191
- if ((await hashFormRevision(saved.form)) !== record.formRevision) {
192
+ const fields = entityFields(req, options, { collection: record.targetCollection });
193
+ const revision = record.formRevision?.startsWith("batch-v1:")
194
+ ? await hashBatchRevision(saved.form, fields)
195
+ : await hashFormRevision(saved.form);
196
+ if (revision !== record.formRevision) {
192
197
  return fail("stale_context", "The document changed after the proposal.");
193
198
  }
194
- const fields = entityFields(req, options, { collection: record.targetCollection });
195
199
  const edited = withEdits(stored, input.edits, fields, saved.form);
196
200
  if (!edited.ok) {
197
201
  // Not the editor's approval to spend: the card can still be saved without the edit.
@@ -1,3 +1,4 @@
1
+ import { hashBatchRevision } from "../form/batch-revision.js";
1
2
  import { changedPaths } from "../form/changed-paths.js";
2
3
  import { compileOperations } from "../form/compile.js";
3
4
  import { humanDiff, richTextLines } from "../form/diff.js";
@@ -85,7 +86,11 @@ export async function proposeChange(req, options, input) {
85
86
  if (!relations.ok) {
86
87
  return { ok: false, error: "tool_denied", message: relations.message };
87
88
  }
88
- const formRevision = input.capability === "draft.create" ? undefined : await hashFormRevision(current);
89
+ const formRevision = input.capability === "draft.create"
90
+ ? undefined
91
+ : input.capability === "content.update"
92
+ ? await hashBatchRevision(current, fields)
93
+ : await hashFormRevision(current);
89
94
  const canonicalHash = await hashFormRevision(typedOperations);
90
95
  const nonce = crypto.randomUUID();
91
96
  const expiresAt = new Date(Date.now() + TTL_MS).toISOString();
@@ -0,0 +1,6 @@
1
+ import type { FieldNode } from "../schema/fields.ts";
2
+ /**
3
+ * A first SQL draft may materialize absent localized columns as null. Hash their empty values
4
+ * consistently, without altering saved content or relaxing checks on nonempty fields or row ids.
5
+ */
6
+ export declare function hashBatchRevision(data: Record<string, unknown>, fields: FieldNode[]): Promise<string>;
@@ -0,0 +1,39 @@
1
+ import { hashFormRevision } from "./hash.js";
2
+ /**
3
+ * A first SQL draft may materialize absent localized columns as null. Hash their empty values
4
+ * consistently, without altering saved content or relaxing checks on nonempty fields or row ids.
5
+ */
6
+ export async function hashBatchRevision(data, fields) {
7
+ return `batch-v1:${await hashFormRevision(normalizeRecord(data, fields))}`;
8
+ }
9
+ function normalizeRecord(data, fields) {
10
+ const normalized = { ...data };
11
+ for (const field of fields) {
12
+ const value = data[field.name];
13
+ if (field.type === "group" && (value == null || isRecord(value))) {
14
+ normalized[field.name] = normalizeRecord(isRecord(value) ? value : {}, field.fields ?? []);
15
+ }
16
+ else if (field.type === "array" && Array.isArray(value)) {
17
+ normalized[field.name] = value.map((row) => isRecord(row) ? normalizeRecord(row, field.fields ?? []) : row);
18
+ }
19
+ else if (field.type === "blocks" && Array.isArray(value)) {
20
+ normalized[field.name] = value.map((row) => {
21
+ if (!isRecord(row))
22
+ return row;
23
+ const block = field.blocks?.find((entry) => entry.slug === row.blockType);
24
+ return block ? normalizeRecord(row, block.fields) : row;
25
+ });
26
+ }
27
+ else {
28
+ normalized[field.name] =
29
+ value === undefined ||
30
+ ((field.type === "text" || field.type === "textarea") && value === "")
31
+ ? null
32
+ : value;
33
+ }
34
+ }
35
+ return normalized;
36
+ }
37
+ function isRecord(value) {
38
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
39
+ }
@@ -836,3 +836,8 @@ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
836
836
  padding: calc(var(--base) * 0.5) var(--base) calc(var(--base) * 0.8);
837
837
  border-top: 1px solid var(--theme-elevation-100);
838
838
  }
839
+
840
+ /* The batch keeps its progress when the editor collapses the panel. */
841
+ .ea-aside[hidden] {
842
+ display: none;
843
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simmalugnt-se/payload-editor-assistant",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Context-aware editor assistant plugin for Payload Admin",
5
5
  "keywords": [
6
6
  "payload",