@simmalugnt-se/payload-editor-assistant 0.12.0 → 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 +14 -0
- package/dist/components/EditorAssistantAside.d.ts +1 -1
- package/dist/components/EditorAssistantAside.js +2 -5
- package/dist/components/EditorAssistantDocumentBridge.js +1 -1
- package/dist/components/batch-link.js +2 -1
- package/dist/components/copy.d.ts +0 -4
- package/dist/components/copy.js +0 -4
- package/dist/components/proposal-review.js +2 -2
- package/dist/domain/batch.js +6 -2
- package/dist/domain/propose.js +6 -1
- package/dist/form/batch-revision.d.ts +6 -0
- package/dist/form/batch-revision.js +39 -0
- package/dist/form/field-label.d.ts +2 -0
- package/dist/form/field-label.js +10 -3
- package/dist/styles/assistant.css +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
12
|
+
## 0.12.1
|
|
13
|
+
|
|
14
|
+
- The review lists groups in the schema's order (a group's own position, blocks and rows by their
|
|
15
|
+
number) instead of alphabetically by path.
|
|
16
|
+
|
|
3
17
|
## 0.12.0
|
|
4
18
|
|
|
5
19
|
- Proposals are grouped by block, row and named group, in document order, with collapsible sections
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function EditorAssistantAside(): import("react").JSX.Element
|
|
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
|
-
//
|
|
445
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|
|
@@ -158,8 +158,6 @@ export declare const BATCH_COPY: {
|
|
|
158
158
|
readonly review: "Check the suggestions. Uncheck any you do not want, or change the wording before saving.";
|
|
159
159
|
readonly savesDraft: "Saves as a draft; nothing is published.";
|
|
160
160
|
readonly savesDirectly: "Saved directly: this collection has no drafts.";
|
|
161
|
-
readonly before: "Before";
|
|
162
|
-
readonly empty: "empty";
|
|
163
161
|
readonly retry: "Try again";
|
|
164
162
|
readonly retryWith: "What should be different? (optional)";
|
|
165
163
|
readonly retrySend: "Ask again";
|
|
@@ -197,8 +195,6 @@ export declare const BATCH_COPY: {
|
|
|
197
195
|
readonly review: "Granska förslagen. Kryssa ur dem du inte vill ha, eller ändra formuleringen innan du sparar.";
|
|
198
196
|
readonly savesDraft: "Sparas som utkast; inget publiceras.";
|
|
199
197
|
readonly savesDirectly: "Sparas direkt: den här samlingen har inga utkast.";
|
|
200
|
-
readonly before: "Före";
|
|
201
|
-
readonly empty: "tomt";
|
|
202
198
|
readonly retry: "Försök igen";
|
|
203
199
|
readonly retryWith: "Vad ska bli annorlunda? (valfritt)";
|
|
204
200
|
readonly retrySend: "Fråga igen";
|
package/dist/components/copy.js
CHANGED
|
@@ -168,8 +168,6 @@ export const BATCH_COPY = {
|
|
|
168
168
|
review: "Check the suggestions. Uncheck any you do not want, or change the wording before saving.",
|
|
169
169
|
savesDraft: "Saves as a draft; nothing is published.",
|
|
170
170
|
savesDirectly: "Saved directly: this collection has no drafts.",
|
|
171
|
-
before: "Before",
|
|
172
|
-
empty: "empty",
|
|
173
171
|
retry: "Try again",
|
|
174
172
|
retryWith: "What should be different? (optional)",
|
|
175
173
|
retrySend: "Ask again",
|
|
@@ -207,8 +205,6 @@ export const BATCH_COPY = {
|
|
|
207
205
|
review: "Granska förslagen. Kryssa ur dem du inte vill ha, eller ändra formuleringen innan du sparar.",
|
|
208
206
|
savesDraft: "Sparas som utkast; inget publiceras.",
|
|
209
207
|
savesDirectly: "Sparas direkt: den här samlingen har inga utkast.",
|
|
210
|
-
before: "Före",
|
|
211
|
-
empty: "tomt",
|
|
212
208
|
retry: "Försök igen",
|
|
213
209
|
retryWith: "Vad ska bli annorlunda? (valfritt)",
|
|
214
210
|
retrySend: "Fråga igen",
|
|
@@ -22,8 +22,8 @@ export function groupProposalLines(lines) {
|
|
|
22
22
|
? a.kind === "field"
|
|
23
23
|
? -1
|
|
24
24
|
: 1
|
|
25
|
-
: a.kind === "group"
|
|
26
|
-
? a.path.localeCompare(b.path, "en", { numeric: true })
|
|
25
|
+
: a.kind === "group" && b.kind === "group"
|
|
26
|
+
? a.group.order - b.group.order || a.path.localeCompare(b.path, "en", { numeric: true })
|
|
27
27
|
: 0);
|
|
28
28
|
for (const node of nodes)
|
|
29
29
|
if (node.kind === "group")
|
package/dist/domain/batch.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/dist/domain/propose.js
CHANGED
|
@@ -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"
|
|
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
|
+
}
|
|
@@ -4,6 +4,8 @@ export type FieldPathGroup = {
|
|
|
4
4
|
label: string;
|
|
5
5
|
kind: "container" | "block" | "row" | "group";
|
|
6
6
|
position?: number;
|
|
7
|
+
/** Where the group sits among its siblings: the schema position, or the row's own position. */
|
|
8
|
+
order: number;
|
|
7
9
|
};
|
|
8
10
|
/** Schema labels and one-based row positions for a concrete form field path. */
|
|
9
11
|
export declare function fieldPathLabels(fields: FieldNode[], path: string, data: unknown, language?: "en" | "sv"): {
|
package/dist/form/field-label.js
CHANGED
|
@@ -26,6 +26,7 @@ export function fieldPathInfo(fields, path, data, language = "en") {
|
|
|
26
26
|
label: name(block?.label ?? block?.slug ?? "block"),
|
|
27
27
|
kind: "block",
|
|
28
28
|
position,
|
|
29
|
+
order: position,
|
|
29
30
|
});
|
|
30
31
|
scope = block?.fields ?? [];
|
|
31
32
|
}
|
|
@@ -37,22 +38,28 @@ export function fieldPathInfo(fields, path, data, language = "en") {
|
|
|
37
38
|
label: `${name(row)} ${position}`,
|
|
38
39
|
kind: "row",
|
|
39
40
|
position,
|
|
41
|
+
order: position,
|
|
40
42
|
});
|
|
41
43
|
}
|
|
42
44
|
continue;
|
|
43
45
|
}
|
|
44
46
|
schemaPath = schemaPath ? `${schemaPath}.${segment}` : segment;
|
|
45
|
-
|
|
47
|
+
const at = scope.findIndex((entry) => entry.path === schemaPath);
|
|
48
|
+
field = at >= 0 ? scope[at] : undefined;
|
|
46
49
|
parts.push(name(field?.label ?? segment));
|
|
47
50
|
if (field?.type === "group" || field?.type === "blocks" || field?.type === "array") {
|
|
48
51
|
groups.push({
|
|
49
52
|
path: concretePath,
|
|
50
53
|
label: name(field.label ?? segment),
|
|
51
54
|
kind: field.type === "group" ? "group" : "container",
|
|
55
|
+
order: at,
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
|
-
else if (!field
|
|
55
|
-
|
|
58
|
+
else if (!field) {
|
|
59
|
+
const inside = scope.findIndex((entry) => entry.path.startsWith(`${schemaPath}.`));
|
|
60
|
+
if (inside >= 0) {
|
|
61
|
+
groups.push({ path: concretePath, label: name(segment), kind: "group", order: inside });
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
// Named tabs are flattened by walkFields, but their name remains in each child's path.
|
|
58
65
|
if (field)
|
|
@@ -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
|
+
}
|