@simmalugnt-se/payload-editor-assistant 0.11.2 → 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/CHANGELOG.md +21 -0
- package/dist/components/BatchPanel.js +4 -3
- package/dist/components/EditorAssistantAside.js +3 -2
- package/dist/components/ProposalReview.d.ts +11 -0
- package/dist/components/ProposalReview.js +46 -0
- package/dist/components/RichTextPreview.d.ts +6 -0
- package/dist/components/RichTextPreview.js +76 -0
- package/dist/components/assistant-context.d.ts +2 -0
- package/dist/components/batch-link.js +10 -3
- package/dist/components/copy.d.ts +1 -1
- package/dist/components/copy.js +1 -1
- package/dist/components/proposal-review.d.ts +16 -0
- package/dist/components/proposal-review.js +34 -0
- package/dist/domain/batch.js +23 -8
- package/dist/domain/propose.d.ts +10 -0
- package/dist/domain/propose.js +42 -29
- package/dist/form/diff.d.ts +2 -2
- package/dist/form/diff.js +10 -10
- package/dist/form/field-label.d.ts +20 -0
- package/dist/form/field-label.js +72 -0
- package/dist/form/shape.d.ts +4 -1
- package/dist/form/shape.js +18 -0
- package/dist/runtime/agent.d.ts +1 -1
- package/dist/runtime/agent.js +4 -1
- package/dist/runtime/tools.d.ts +31 -2
- package/dist/runtime/tools.js +8 -3
- package/dist/schema/project.d.ts +6 -1
- package/dist/schema/project.js +14 -8
- package/dist/styles/assistant.css +216 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
- Proposals are grouped by block, row and named group, in document order, with collapsible sections
|
|
6
|
+
and field labels directly attached to their suggested values. Batches and ordinary form proposals
|
|
7
|
+
use the same review layout.
|
|
8
|
+
- Rich text proposals show a full formatted preview of the compiled Lexical content, including
|
|
9
|
+
headings, lists and text formatting. Current content can be expanded for comparison; links show
|
|
10
|
+
their text, and embedded content has a placeholder. Raw Markdown and repeated part labels are no
|
|
11
|
+
longer shown in the review.
|
|
12
|
+
- Proposal compilation and batch saving keep allowlisted rich text in its stored Lexical format,
|
|
13
|
+
separate from the model's capped Markdown projection. Unchanged nodes retain their formatting,
|
|
14
|
+
and previews match the content to be saved even on long documents.
|
|
15
|
+
- Translation batch cards use the default-language document title when the target title is empty.
|
|
16
|
+
The document heading stays visible while scrolling its suggestions. The draft-save hint uses
|
|
17
|
+
future wording so a suggestion is not mistaken for saved content.
|
|
18
|
+
|
|
19
|
+
## 0.11.3
|
|
20
|
+
|
|
21
|
+
- Clicking "Fix N with the assistant" again after closing the panel opens it on the batch that is
|
|
22
|
+
still there. The link was remembered as taken and did nothing.
|
|
23
|
+
|
|
3
24
|
## 0.11.2
|
|
4
25
|
|
|
5
26
|
- Translating a page whose text is all inside blocks or rows now works. The assistant left the
|
|
@@ -8,6 +8,7 @@ import { ENDPOINT_PREFIX } from "../package-name.js";
|
|
|
8
8
|
import { assistantRequest } from "./assistant-api.js";
|
|
9
9
|
import { cardAfterApply, cardAfterPropose, changedEdits, initialCards, savePlan, waitingKeys, } from "./batch-run.js";
|
|
10
10
|
import { BATCH_COPY, uiLanguage } from "./copy.js";
|
|
11
|
+
import { ProposalReview } from "./ProposalReview.js";
|
|
11
12
|
/**
|
|
12
13
|
* Many documents at once, from a work list link: the assistant suggests a change for each, one
|
|
13
14
|
* request after the other, and the editor saves the ones they want. Nothing is written before that.
|
|
@@ -135,12 +136,12 @@ export function BatchPanel({ batch, onClose }) {
|
|
|
135
136
|
const selected = cards.filter((card) => card.status === "proposed" && card.checked).length;
|
|
136
137
|
const waiting = waitingKeys(cards).length;
|
|
137
138
|
const savedCount = cards.filter((card) => card.status === "saved" || card.status === "draft").length;
|
|
138
|
-
return (_jsxs("div", { className: "ea-batch", children: [_jsxs("div", { className: "ea-batch__head", children: [_jsx("h3", { children: copy.title(batch.targets.length) }), _jsxs("p", { className: "ea-batch__task", children: [_jsxs("span", { children: [copy.task, ":"] }), " ", batch.prompt] }), phase === "ready" ? (_jsxs(_Fragment, { children: [_jsx("p", { children: copy.intro(waiting) }), _jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", disabled: waiting === 0, margin: false, onClick: () => void run(), size: "medium", children: copy.start }), _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: onClose, size: "medium", children: copy.cancel })] })] })) : null, phase === "running" || phase === "saving" ? (_jsx("p", { "aria-live": "polite", children: phase === "saving" ? copy.saving : copy.progress(finished, total) })) : null, phase === "review" && selected > 0 ? _jsx("p", { children: copy.review }) : null, phase === "done" ? (_jsx("p", { "aria-live": "polite", children: copy.summary(savedCount, total - savedCount) })) : null] }), _jsx("ul", { className: "ea-batch__cards", children: cards.map((card) => (_jsx(BatchCardItem, { adminRoute: config.routes.admin, card: card, copy: copy, editable: phase === "review" || phase === "done", onCheck: (checked) => update(card.key, (current) => ({ ...current, checked })), onEdit: (path, value) => update(card.key, (current) => ({
|
|
139
|
+
return (_jsxs("div", { className: "ea-batch", children: [_jsxs("div", { className: "ea-batch__head", children: [_jsx("h3", { children: copy.title(batch.targets.length) }), _jsxs("p", { className: "ea-batch__task", children: [_jsxs("span", { children: [copy.task, ":"] }), " ", batch.prompt] }), phase === "ready" ? (_jsxs(_Fragment, { children: [_jsx("p", { children: copy.intro(waiting) }), _jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", disabled: waiting === 0, margin: false, onClick: () => void run(), size: "medium", children: copy.start }), _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: onClose, size: "medium", children: copy.cancel })] })] })) : null, phase === "running" || phase === "saving" ? (_jsx("p", { "aria-live": "polite", children: phase === "saving" ? copy.saving : copy.progress(finished, total) })) : null, phase === "review" && selected > 0 ? _jsx("p", { children: copy.review }) : null, phase === "done" ? (_jsx("p", { "aria-live": "polite", children: copy.summary(savedCount, total - savedCount) })) : null] }), _jsx("ul", { className: "ea-batch__cards", children: cards.map((card) => (_jsx(BatchCardItem, { adminRoute: config.routes.admin, card: card, copy: copy, language: language, editable: phase === "review" || phase === "done", onCheck: (checked) => update(card.key, (current) => ({ ...current, checked })), onEdit: (path, value) => update(card.key, (current) => ({
|
|
139
140
|
...current,
|
|
140
141
|
edits: { ...current.edits, [path]: value },
|
|
141
142
|
})), onRetry: (instruction) => void retry(card, instruction) }, card.key))) }), phase === "ready" ? null : (_jsxs("div", { className: "ea-actions ea-batch__actions", children: [phase === "running" ? (_jsx(Button, { buttonStyle: "secondary", margin: false, onClick: stop, size: "medium", children: copy.stop })) : null, phase === "review" ? (_jsxs(_Fragment, { children: [_jsx(Button, { buttonStyle: "primary", disabled: selected === 0, margin: false, onClick: () => void save(), size: "medium", children: copy.save(selected) }), waiting > 0 ? (_jsx(Button, { buttonStyle: "secondary", margin: false, onClick: () => void run(), size: "medium", children: copy.continue })) : null, _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: () => void close(), size: "medium", children: copy.cancel })] })) : null, phase === "done" ? (_jsx(Button, { buttonStyle: "primary", margin: false, onClick: onClose, size: "medium", children: copy.close })) : null] }))] }));
|
|
142
143
|
}
|
|
143
|
-
function BatchCardItem({ adminRoute, card, copy, editable, onCheck, onEdit, onRetry, }) {
|
|
144
|
+
function BatchCardItem({ adminRoute, card, copy, language, editable, onCheck, onEdit, onRetry, }) {
|
|
144
145
|
const [retrying, setRetrying] = useState(false);
|
|
145
146
|
const [instruction, setInstruction] = useState("");
|
|
146
147
|
const { target } = card;
|
|
@@ -152,7 +153,7 @@ function BatchCardItem({ adminRoute, card, copy, editable, onCheck, onEdit, onRe
|
|
|
152
153
|
return (_jsxs("li", { className: `ea-batch__card ea-batch__card--${card.status}`, children: [_jsxs("div", { className: "ea-batch__card-head", children: [proposed ? (_jsx("input", { "aria-label": copy.select(card.title), checked: card.checked, disabled: !editable, id: checkboxId, onChange: (event) => onCheck(event.target.checked), type: "checkbox" })) : null, card.thumbnail ? (
|
|
153
154
|
// A plugin cannot set up next/image for the project's media addresses.
|
|
154
155
|
// biome-ignore lint/performance/noImgElement: a small thumbnail as Payload serves it
|
|
155
|
-
_jsx("img", { alt: "", className: "ea-batch__thumb", src: card.thumbnail })) : null, _jsxs("label", { className: "ea-batch__title", htmlFor: proposed ? checkboxId : undefined, children: [card.title, target.locale ? _jsx("span", { className: "ea-batch__pill", children: target.locale }) : null] }), _jsx("span", { className: "ea-batch__status", children: copy.status[card.status] })] }), card.proposal ? (_jsx(
|
|
156
|
+
_jsx("img", { alt: "", className: "ea-batch__thumb", src: card.thumbnail })) : null, _jsxs("label", { className: "ea-batch__title", htmlFor: proposed ? checkboxId : undefined, children: [card.title, target.locale ? _jsx("span", { className: "ea-batch__pill", children: target.locale }) : null] }), _jsx("span", { className: "ea-batch__status", children: copy.status[card.status] })] }), card.proposal ? (_jsx(ProposalReview, { lines: card.proposal.lines, language: language, editable: editable && proposed, edits: card.edits, onEdit: onEdit })) : null, card.text && (card.status === "nothing" || card.status === "failed") ? (_jsx("p", { className: "ea-batch__text", children: card.text })) : null, proposed ? (_jsx("p", { className: "ea-batch__hint", children: card.drafts ? copy.savesDraft : copy.savesDirectly })) : null, card.status === "stale" ? _jsx("p", { className: "ea-batch__hint", children: copy.staleHint }) : null, retrying ? (_jsxs("form", { className: "ea-batch__retry", onSubmit: (event) => {
|
|
156
157
|
event.preventDefault();
|
|
157
158
|
setRetrying(false);
|
|
158
159
|
onRetry(instruction);
|
|
@@ -10,6 +10,7 @@ import { useAssistantState, } from "./assistant-context.js";
|
|
|
10
10
|
import { BatchPanel } from "./BatchPanel.js";
|
|
11
11
|
import { releaseTakenBatches } from "./batch-link.js";
|
|
12
12
|
import { COPY, translateAction, uiLanguage } from "./copy.js";
|
|
13
|
+
import { ProposalReview } from "./ProposalReview.js";
|
|
13
14
|
import { isFormProposal, proposalSteps, refineProposal, replaceLatestStep, } from "./proposal-chain.js";
|
|
14
15
|
import { ReplyText } from "./ReplyText.js";
|
|
15
16
|
import { KEEP_SELECTION_ATTR } from "./selection-dom.js";
|
|
@@ -449,11 +450,11 @@ export function EditorAssistantAside() {
|
|
|
449
450
|
setBatch(null);
|
|
450
451
|
} }, 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: () => {
|
|
451
452
|
router.push(candidate.href);
|
|
452
|
-
}, size: "small", children: candidate.label }, candidate.href))) })) : null] }, message.id))), status === "streaming" ? (_jsxs("div", { className: "ea-msg ea-msg--working", children: [_jsxs("span", { className: "ea-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }), _jsx("span", { children: step ? talk.steps[step] : talk.working })] })) : null] }), pendingApproval ? (_jsxs("div", { className: "ea-approval", children: [_jsx("h3", { children: pendingApproval.title }), pendingApproval.applied ? (_jsxs("p", { className: "ea-approval__hint", children: [autosave ? copy.previewHintAutosave : copy.previewHint, pendingApproval.earlier?.length ? ` ${copy.refinedHint}` : ""] })) : null, _jsx("ul", { className: "ea-approval__diff", children: pendingApproval.summary.split("\n").map((line, index) => line.startsWith(" ") ? (
|
|
453
|
+
}, size: "small", children: candidate.label }, candidate.href))) })) : null] }, message.id))), status === "streaming" ? (_jsxs("div", { className: "ea-msg ea-msg--working", children: [_jsxs("span", { className: "ea-dots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }), _jsx("span", { children: step ? talk.steps[step] : talk.working })] })) : null] }), pendingApproval ? (_jsxs("div", { className: "ea-approval", children: [_jsx("h3", { children: pendingApproval.title }), pendingApproval.applied ? (_jsxs("p", { className: "ea-approval__hint", children: [autosave ? copy.previewHintAutosave : copy.previewHint, pendingApproval.earlier?.length ? ` ${copy.refinedHint}` : ""] })) : null, pendingApproval.lines?.length ? (_jsx("div", { className: "ea-approval__review", children: _jsx(ProposalReview, { lines: pendingApproval.lines, language: uiLanguage(i18n.language) }) })) : (_jsx("ul", { className: "ea-approval__diff", children: pendingApproval.summary.split("\n").map((line, index) => line.startsWith(" ") ? (
|
|
453
454
|
// biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
|
|
454
455
|
_jsx("li", { className: "ea-approval__sub", children: line.trim() }, index)) : (
|
|
455
456
|
// biome-ignore lint/suspicious/noArrayIndexKey: lines are static for one approval
|
|
456
|
-
_jsx("li", { children: line }, index))) }), confirmUndo ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "ea-approval__warn", children: copy.undoLosesEdits }), _jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", margin: false, onClick: () => void undoPreview(pendingApproval, true), size: "medium", children: copy.undoAnyway }), _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: () => setConfirmUndo(false), size: "medium", children: copy.cancel })] })] })) : (_jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", disabled: status === "streaming", margin: false, onClick: () => void decide("approve"), size: "medium", children: pendingApproval.applied ? copy.keep : copy.approve }), _jsx(Button, { buttonStyle: "secondary", disabled: status === "streaming", margin: false, onClick: () => void decide("reject"), size: "medium", children: pendingApproval.applied ? copy.undo : copy.reject })] }))] })) : null, status !== "idle" && status !== "streaming" ? (_jsx("div", { className: `ea-status ea-status--${status}`, children: statusText(status, talk) })) : null, _jsxs("form", { className: "ea-composer", onSubmit: onSubmit, children: [visibleShortcuts.length > 0 || translate ? (_jsxs("div", { className: "ea-shortcuts", children: [translate ? (_jsx(Button, { buttonStyle: "pill", disabled: status === "streaming", margin: false, onClick: () => {
|
|
457
|
+
_jsx("li", { children: line }, index))) })), confirmUndo ? (_jsxs(_Fragment, { children: [_jsx("p", { className: "ea-approval__warn", children: copy.undoLosesEdits }), _jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", margin: false, onClick: () => void undoPreview(pendingApproval, true), size: "medium", children: copy.undoAnyway }), _jsx(Button, { buttonStyle: "secondary", margin: false, onClick: () => setConfirmUndo(false), size: "medium", children: copy.cancel })] })] })) : (_jsxs("div", { className: "ea-actions", children: [_jsx(Button, { buttonStyle: "primary", disabled: status === "streaming", margin: false, onClick: () => void decide("approve"), size: "medium", children: pendingApproval.applied ? copy.keep : copy.approve }), _jsx(Button, { buttonStyle: "secondary", disabled: status === "streaming", margin: false, onClick: () => void decide("reject"), size: "medium", children: pendingApproval.applied ? copy.undo : copy.reject })] }))] })) : null, status !== "idle" && status !== "streaming" ? (_jsx("div", { className: `ea-status ea-status--${status}`, children: statusText(status, talk) })) : null, _jsxs("form", { className: "ea-composer", onSubmit: onSubmit, children: [visibleShortcuts.length > 0 || translate ? (_jsxs("div", { className: "ea-shortcuts", children: [translate ? (_jsx(Button, { buttonStyle: "pill", disabled: status === "streaming", margin: false, onClick: () => {
|
|
457
458
|
void send(translate.prompt);
|
|
458
459
|
}, size: "small", children: translate.label })) : null, visibleShortcuts.map((shortcut) => (_jsx(Button, { buttonStyle: "pill", disabled: status === "streaming", margin: false, onClick: () => {
|
|
459
460
|
void send(shortcut.prompt);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProposalLine } from "../domain/propose.ts";
|
|
2
|
+
type Props = {
|
|
3
|
+
lines: ProposalLine[];
|
|
4
|
+
language: "en" | "sv";
|
|
5
|
+
editable?: boolean;
|
|
6
|
+
edits?: Record<string, string>;
|
|
7
|
+
onEdit?: (path: string, value: string) => void;
|
|
8
|
+
};
|
|
9
|
+
/** One review layout for batch proposals and changes in the open form. */
|
|
10
|
+
export declare function ProposalReview(props: Props): import("react").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
import { groupProposalLines } from "./proposal-review.js";
|
|
5
|
+
import { hasRichText, RichTextPreview } from "./RichTextPreview.js";
|
|
6
|
+
const COPY = {
|
|
7
|
+
en: {
|
|
8
|
+
current: "Current text",
|
|
9
|
+
suggested: "Suggested text",
|
|
10
|
+
added: "New",
|
|
11
|
+
empty: "Cleared",
|
|
12
|
+
block: "Block",
|
|
13
|
+
},
|
|
14
|
+
sv: {
|
|
15
|
+
current: "Nuvarande text",
|
|
16
|
+
suggested: "Föreslagen text",
|
|
17
|
+
added: "Nytt",
|
|
18
|
+
empty: "Tömt",
|
|
19
|
+
block: "Block",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
/** One review layout for batch proposals and changes in the open form. */
|
|
23
|
+
export function ProposalReview(props) {
|
|
24
|
+
return (_jsx("div", { className: "ea-review", children: groupProposalLines(props.lines).map((node) => (_jsx(ReviewNodeItem, { node: node, props: props }, node.path))) }));
|
|
25
|
+
}
|
|
26
|
+
function ReviewNodeItem({ node, props }) {
|
|
27
|
+
if (node.kind === "field")
|
|
28
|
+
return _jsx(FieldReview, { lines: node.lines, props: props });
|
|
29
|
+
const { group } = node;
|
|
30
|
+
const children = node.children.map((child) => (_jsx(ReviewNodeItem, { node: child, props: props }, child.path)));
|
|
31
|
+
if (group.kind === "container")
|
|
32
|
+
return (_jsxs("section", { className: "ea-review__collection", children: [_jsx("h4", { children: group.label }), _jsx("div", { className: "ea-review__children", children: children })] }));
|
|
33
|
+
return (_jsxs("details", { className: `ea-review__group ea-review__group--${group.kind}`, open: true, children: [_jsxs("summary", { children: [_jsx("span", { children: group.label }), group.kind === "block" ? (_jsxs("span", { className: "ea-review__badge", children: [COPY[props.language].block, " ", group.position] })) : null] }), _jsx("div", { className: "ea-review__children", children: children })] }));
|
|
34
|
+
}
|
|
35
|
+
function FieldReview({ lines, props }) {
|
|
36
|
+
const id = useId();
|
|
37
|
+
const line = lines[0];
|
|
38
|
+
if (!line)
|
|
39
|
+
return null;
|
|
40
|
+
const copy = COPY[props.language];
|
|
41
|
+
const label = line.label ?? line.edit?.label;
|
|
42
|
+
const rich = line.richText;
|
|
43
|
+
const added = rich ? !hasRichText(rich.before) : line.edit ? !line.edit.before : false;
|
|
44
|
+
const value = props.edits?.[line.path] ?? line.edit?.value ?? "";
|
|
45
|
+
return (_jsxs("div", { className: "ea-review__field", children: [label ? (_jsxs("div", { className: "ea-review__field-head", children: [_jsx("label", { htmlFor: line.edit && props.onEdit ? id : undefined, children: label }), added ? _jsx("span", { className: "ea-review__new", children: copy.added }) : null] })) : null, rich ? (_jsxs(_Fragment, { children: [hasRichText(rich.before) ? (_jsxs("details", { className: "ea-review__current", children: [_jsx("summary", { children: copy.current }), _jsx(RichTextPreview, { value: rich.before, language: props.language })] })) : null, _jsx("div", { className: "ea-review__preview", role: "region", "aria-label": `${label}: ${copy.suggested}`, children: hasRichText(rich.after) ? (_jsx(RichTextPreview, { value: rich.after, language: props.language })) : (_jsx("span", { children: copy.empty })) })] })) : line.edit ? (_jsxs(_Fragment, { children: [line.edit.before ? (_jsxs("details", { className: "ea-review__current", children: [_jsx("summary", { children: copy.current }), _jsx("p", { children: line.edit.before })] })) : null, props.onEdit ? (_jsx("textarea", { id: id, "aria-label": [line.context, label].filter(Boolean).join(" › "), disabled: !props.editable, onChange: (event) => props.onEdit?.(line.path, event.target.value), rows: 2, value: value })) : (_jsx("p", { className: "ea-review__value", children: value || copy.empty })), props.onEdit ? _jsx("span", { className: "ea-review__count", children: [...value].length }) : null] })) : (_jsx("div", { className: "ea-review__change", children: lines.map((line, index) => (_jsx("p", { children: line.text }, `${line.path}:${index}`))) }))] }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function hasRichText(value: unknown): boolean;
|
|
2
|
+
/** Render stored Lexical content as safe React elements; no HTML or live links from content. */
|
|
3
|
+
export declare function RichTextPreview({ value, language }: {
|
|
4
|
+
value: unknown;
|
|
5
|
+
language: "en" | "sv";
|
|
6
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createElement, Fragment } from "react";
|
|
4
|
+
export function hasRichText(value) {
|
|
5
|
+
const visit = (value) => {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return false;
|
|
8
|
+
const node = value;
|
|
9
|
+
if (node.root)
|
|
10
|
+
return visit(node.root);
|
|
11
|
+
if (typeof node.text === "string" && node.text.trim())
|
|
12
|
+
return true;
|
|
13
|
+
if (Array.isArray(node.children) && node.children.some(visit))
|
|
14
|
+
return true;
|
|
15
|
+
return Boolean(node.type &&
|
|
16
|
+
!["root", "paragraph", "heading", "text", "linebreak", "list", "listitem"].includes(node.type));
|
|
17
|
+
};
|
|
18
|
+
return typeof value === "string" ? Boolean(value.trim()) : visit(value);
|
|
19
|
+
}
|
|
20
|
+
/** Render stored Lexical content as safe React elements; no HTML or live links from content. */
|
|
21
|
+
export function RichTextPreview({ value, language }) {
|
|
22
|
+
const root = value?.root;
|
|
23
|
+
const embedded = language === "sv" ? "Inbäddat innehåll" : "Embedded content";
|
|
24
|
+
const render = (value, key) => {
|
|
25
|
+
if (!value || typeof value !== "object")
|
|
26
|
+
return null;
|
|
27
|
+
const node = value;
|
|
28
|
+
const children = Array.isArray(node.children) ? node.children.map(render) : null;
|
|
29
|
+
const style = typeof node.format === "string" &&
|
|
30
|
+
["left", "right", "center", "justify"].includes(node.format)
|
|
31
|
+
? { textAlign: node.format }
|
|
32
|
+
: undefined;
|
|
33
|
+
if (node.type === "text") {
|
|
34
|
+
let text = typeof node.text === "string" ? node.text : "";
|
|
35
|
+
const format = typeof node.format === "number" ? node.format : 0;
|
|
36
|
+
// Lexical's serialized TextNode format flags, independent of the optional editor peer.
|
|
37
|
+
if (format & 1)
|
|
38
|
+
text = _jsx("strong", { children: text });
|
|
39
|
+
if (format & 2)
|
|
40
|
+
text = _jsx("em", { children: text });
|
|
41
|
+
if (format & 4)
|
|
42
|
+
text = _jsx("s", { children: text });
|
|
43
|
+
if (format & 8)
|
|
44
|
+
text = _jsx("u", { children: text });
|
|
45
|
+
if (format & 16)
|
|
46
|
+
text = _jsx("code", { children: text });
|
|
47
|
+
if (format & 32)
|
|
48
|
+
text = _jsx("sub", { children: text });
|
|
49
|
+
if (format & 64)
|
|
50
|
+
text = _jsx("sup", { children: text });
|
|
51
|
+
return _jsx(Fragment, { children: text }, key);
|
|
52
|
+
}
|
|
53
|
+
switch (node.type) {
|
|
54
|
+
case "paragraph":
|
|
55
|
+
return (_jsx("p", { style: style, children: children }, key));
|
|
56
|
+
case "heading":
|
|
57
|
+
return createElement(/^h[1-6]$/.test(node.tag ?? "") ? node.tag : "h3", { key, style }, children);
|
|
58
|
+
case "list":
|
|
59
|
+
return node.tag === "ol" ? (_jsx("ol", { start: node.start, children: children }, key)) : (_jsx("ul", { children: children }, key));
|
|
60
|
+
case "listitem":
|
|
61
|
+
return _jsx("li", { children: children }, key);
|
|
62
|
+
case "quote":
|
|
63
|
+
return (_jsx("blockquote", { style: style, children: children }, key));
|
|
64
|
+
case "horizontalrule":
|
|
65
|
+
return _jsx("hr", {}, key);
|
|
66
|
+
case "linebreak":
|
|
67
|
+
return _jsx("br", {}, key);
|
|
68
|
+
case "link":
|
|
69
|
+
case "autolink":
|
|
70
|
+
return (_jsx("span", { className: "ea-review__link", title: node.fields?.url ?? (language === "sv" ? "Intern länk" : "Internal link"), children: children }, key));
|
|
71
|
+
default:
|
|
72
|
+
return (_jsx("span", { className: "ea-review__embedded", children: embedded }, key));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return (_jsx("div", { className: "ea-review__rich", children: Array.isArray(root?.children) ? (root.children.map(render)) : typeof value === "string" ? (_jsx("p", { children: value })) : null }));
|
|
76
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { BatchScope, BatchTarget } from "../batch/targets.ts";
|
|
3
|
+
import type { ProposalLine } from "../domain/propose.ts";
|
|
3
4
|
import type { AssistantShortcut } from "../types.ts";
|
|
4
5
|
import type { DomSelection } from "./selection-dom.ts";
|
|
5
6
|
export type AssistantChromeStatus = "idle" | "unavailable" | "unauthorized" | "rate_limited" | "timeout" | "provider_error" | "no_model" | "tool_denied" | "stale_context" | "aborted" | "streaming";
|
|
@@ -23,6 +24,7 @@ export type FormAdapter = {
|
|
|
23
24
|
export type PendingApproval = {
|
|
24
25
|
title: string;
|
|
25
26
|
summary: string;
|
|
27
|
+
lines?: ProposalLine[];
|
|
26
28
|
approvalId: string;
|
|
27
29
|
nonce: string;
|
|
28
30
|
capability: "form.propose" | "draft.create";
|
|
@@ -17,11 +17,13 @@ export function releaseTakenBatches() {
|
|
|
17
17
|
export function useBatchLink() {
|
|
18
18
|
const params = useSearchParams();
|
|
19
19
|
const { setBatch, setOpen } = useAssistantState();
|
|
20
|
-
|
|
20
|
+
// Depends on `params`, not the value: the same batch clicked twice leaves the value unchanged.
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
|
|
22
|
+
const value = params?.get(BATCH_PARAM);
|
|
23
|
+
if (!value) {
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
26
|
+
const repeated = taken.has(value);
|
|
25
27
|
taken.add(value);
|
|
26
28
|
const url = new URL(window.location.href);
|
|
27
29
|
const targets = parseBatchTargets(url.searchParams.get(BATCH_PARAM));
|
|
@@ -31,9 +33,14 @@ export function useBatchLink() {
|
|
|
31
33
|
url.searchParams.delete(name);
|
|
32
34
|
}
|
|
33
35
|
window.history.replaceState(window.history.state, "", url.toString());
|
|
36
|
+
if (repeated) {
|
|
37
|
+
// The batch is still in the panel, which the editor closed: show it again.
|
|
38
|
+
setOpen(true);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
34
41
|
if (targets.length > 0 && prompt) {
|
|
35
42
|
setBatch({ id: crypto.randomUUID(), targets, scope, prompt });
|
|
36
43
|
setOpen(true);
|
|
37
44
|
}
|
|
38
|
-
}, [
|
|
45
|
+
}, [params, setBatch, setOpen]);
|
|
39
46
|
}
|
|
@@ -156,7 +156,7 @@ export declare const BATCH_COPY: {
|
|
|
156
156
|
readonly save: (count: number) => string;
|
|
157
157
|
readonly saving: "Saving…";
|
|
158
158
|
readonly review: "Check the suggestions. Uncheck any you do not want, or change the wording before saving.";
|
|
159
|
-
readonly savesDraft: "
|
|
159
|
+
readonly savesDraft: "Saves as a draft; nothing is published.";
|
|
160
160
|
readonly savesDirectly: "Saved directly: this collection has no drafts.";
|
|
161
161
|
readonly before: "Before";
|
|
162
162
|
readonly empty: "empty";
|
package/dist/components/copy.js
CHANGED
|
@@ -166,7 +166,7 @@ export const BATCH_COPY = {
|
|
|
166
166
|
save: (count) => `Save ${count} selected`,
|
|
167
167
|
saving: "Saving…",
|
|
168
168
|
review: "Check the suggestions. Uncheck any you do not want, or change the wording before saving.",
|
|
169
|
-
savesDraft: "
|
|
169
|
+
savesDraft: "Saves as a draft; nothing is published.",
|
|
170
170
|
savesDirectly: "Saved directly: this collection has no drafts.",
|
|
171
171
|
before: "Before",
|
|
172
172
|
empty: "empty",
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ProposalLine } from "../domain/propose.ts";
|
|
2
|
+
import type { FieldPathGroup } from "../form/field-label.ts";
|
|
3
|
+
export type ReviewField = {
|
|
4
|
+
kind: "field";
|
|
5
|
+
path: string;
|
|
6
|
+
lines: ProposalLine[];
|
|
7
|
+
};
|
|
8
|
+
export type ReviewGroup = {
|
|
9
|
+
kind: "group";
|
|
10
|
+
path: string;
|
|
11
|
+
group: FieldPathGroup;
|
|
12
|
+
children: ReviewNode[];
|
|
13
|
+
};
|
|
14
|
+
export type ReviewNode = ReviewField | ReviewGroup;
|
|
15
|
+
/** Merge fields under their actual parents and keep blocks and rows in document order. */
|
|
16
|
+
export declare function groupProposalLines(lines: ProposalLine[]): ReviewNode[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Merge fields under their actual parents and keep blocks and rows in document order. */
|
|
2
|
+
export function groupProposalLines(lines) {
|
|
3
|
+
const root = [];
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
let children = root;
|
|
6
|
+
for (const group of line.groups ?? []) {
|
|
7
|
+
let node = children.find((node) => node.kind === "group" && node.path === group.path);
|
|
8
|
+
if (!node) {
|
|
9
|
+
node = { kind: "group", path: group.path, group, children: [] };
|
|
10
|
+
children.push(node);
|
|
11
|
+
}
|
|
12
|
+
children = node.children;
|
|
13
|
+
}
|
|
14
|
+
const existing = children.find((node) => node.kind === "field" && node.path === line.path);
|
|
15
|
+
if (existing)
|
|
16
|
+
existing.lines.push(line);
|
|
17
|
+
else
|
|
18
|
+
children.push({ kind: "field", path: line.path, lines: [line] });
|
|
19
|
+
}
|
|
20
|
+
const sort = (nodes) => {
|
|
21
|
+
nodes.sort((a, b) => a.kind !== b.kind
|
|
22
|
+
? a.kind === "field"
|
|
23
|
+
? -1
|
|
24
|
+
: 1
|
|
25
|
+
: a.kind === "group"
|
|
26
|
+
? a.path.localeCompare(b.path, "en", { numeric: true })
|
|
27
|
+
: 0);
|
|
28
|
+
for (const node of nodes)
|
|
29
|
+
if (node.kind === "group")
|
|
30
|
+
sort(node.children);
|
|
31
|
+
};
|
|
32
|
+
sort(root);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
package/dist/domain/batch.js
CHANGED
|
@@ -5,14 +5,14 @@ import { isFormOperation } from "../form/operations.js";
|
|
|
5
5
|
import { cloneData, getAtPath, parseFieldPath, setAtPath } from "../form/path.js";
|
|
6
6
|
import { relationValueError } from "../form/relation-shape.js";
|
|
7
7
|
import { loadRichText } from "../form/rich-text.js";
|
|
8
|
-
import { finalizeCompiled, openRichText } from "../form/shape.js";
|
|
8
|
+
import { finalizeCompiled, openRichText, richTextPaths } from "../form/shape.js";
|
|
9
9
|
import { translationPaths } from "../form/translation.js";
|
|
10
10
|
import { DEFAULT_APPROVAL_SLUG } from "../package-name.js";
|
|
11
11
|
import { runAgentTurn } from "../runtime/agent.js";
|
|
12
12
|
import { sourceLocale } from "../runtime/translation-source.js";
|
|
13
13
|
import { configuredDefaultLocale, configuredLocales, entityFields, findCollectionConfig, hasDrafts, } from "../schema/discover.js";
|
|
14
14
|
import { fieldAtPath } from "../schema/fields.js";
|
|
15
|
-
import { projectRecord } from "../schema/project.js";
|
|
15
|
+
import { projectFormRecord, projectRecord } from "../schema/project.js";
|
|
16
16
|
import { hasCapability, resolveAllowlist } from "./allowlist.js";
|
|
17
17
|
import { writeAudit } from "./audit.js";
|
|
18
18
|
import { readDocument } from "./content.js";
|
|
@@ -110,6 +110,7 @@ async function readSaved(req, options, collection, id, locale) {
|
|
|
110
110
|
const title = useAsTitle ? read.doc[useAsTitle] : undefined;
|
|
111
111
|
return {
|
|
112
112
|
data: projectRecord(read.doc, read.fields, options.redact, read.slug).data,
|
|
113
|
+
form: projectFormRecord(read.doc, read.fields, options.redact, read.slug),
|
|
113
114
|
raw: read.doc,
|
|
114
115
|
title: typeof title === "string" && title ? title : undefined,
|
|
115
116
|
};
|
|
@@ -187,24 +188,24 @@ export async function applyBatchTarget(req, options, input) {
|
|
|
187
188
|
if (!saved) {
|
|
188
189
|
return fail("tool_denied", "Document is not accessible.");
|
|
189
190
|
}
|
|
190
|
-
if ((await hashFormRevision(saved.
|
|
191
|
+
if ((await hashFormRevision(saved.form)) !== record.formRevision) {
|
|
191
192
|
return fail("stale_context", "The document changed after the proposal.");
|
|
192
193
|
}
|
|
193
194
|
const fields = entityFields(req, options, { collection: record.targetCollection });
|
|
194
|
-
const edited = withEdits(stored, input.edits, fields, saved.
|
|
195
|
+
const edited = withEdits(stored, input.edits, fields, saved.form);
|
|
195
196
|
if (!edited.ok) {
|
|
196
197
|
// Not the editor's approval to spend: the card can still be saved without the edit.
|
|
197
198
|
return { ok: false, error: "tool_denied", message: edited.message };
|
|
198
199
|
}
|
|
199
200
|
const operations = edited.operations;
|
|
200
|
-
const compiled = compileOperations(openRichText(saved.
|
|
201
|
+
const compiled = compileOperations(openRichText(saved.form, fields), operations, {
|
|
201
202
|
allowlist: resolved.allowlist.fields,
|
|
202
203
|
denyFields: options.denyFields,
|
|
203
204
|
});
|
|
204
205
|
if (!compiled.ok) {
|
|
205
206
|
return fail("tool_denied", compiled.error);
|
|
206
207
|
}
|
|
207
|
-
const finalized = finalizeCompiled(compiled.data, fields, saved.
|
|
208
|
+
const finalized = finalizeCompiled(compiled.data, fields, saved.form);
|
|
208
209
|
if (!finalized.ok) {
|
|
209
210
|
return fail("tool_denied", finalized.error);
|
|
210
211
|
}
|
|
@@ -224,12 +225,15 @@ export async function applyBatchTarget(req, options, input) {
|
|
|
224
225
|
return fail("tool_denied", relations.message);
|
|
225
226
|
}
|
|
226
227
|
const drafts = hasDrafts(findCollectionConfig(req, record.targetCollection)?.versions);
|
|
228
|
+
// The model addresses Markdown parts; Payload stores the containing Lexical field as a unit.
|
|
229
|
+
const richPaths = richTextPaths(operations, fields, saved.form);
|
|
230
|
+
const storedPaths = paths.map((path) => richPaths.find((field) => path === field || path.startsWith(`${field}.`)) ?? path);
|
|
227
231
|
try {
|
|
228
232
|
await localApi(req).update({
|
|
229
233
|
collection: record.targetCollection,
|
|
230
234
|
id: record.documentId,
|
|
231
235
|
locale: record.locale,
|
|
232
|
-
data: changedTopLevel(saved.raw, finalized.data,
|
|
236
|
+
data: changedTopLevel(saved.raw, finalized.data, storedPaths),
|
|
233
237
|
draft: drafts,
|
|
234
238
|
depth: 0,
|
|
235
239
|
req,
|
|
@@ -322,7 +326,18 @@ export async function describeBatchTargets(req, options, targets) {
|
|
|
322
326
|
: undefined;
|
|
323
327
|
const doc = read?.ok ? read.doc : undefined;
|
|
324
328
|
const useAsTitle = config?.admin?.useAsTitle;
|
|
325
|
-
|
|
329
|
+
let title = [useAsTitle ? doc?.[useAsTitle] : undefined, doc?.filename].find((value) => typeof value === "string" && value.trim() !== "");
|
|
330
|
+
const defaultLocale = configuredDefaultLocale(req);
|
|
331
|
+
if (doc && !title && defaultLocale && target.locale && defaultLocale !== target.locale) {
|
|
332
|
+
const source = await readDocument(req, options, {
|
|
333
|
+
collection: target.collection,
|
|
334
|
+
documentId: target.id,
|
|
335
|
+
locale: defaultLocale,
|
|
336
|
+
});
|
|
337
|
+
const sourceTitle = source.ok && useAsTitle ? source.doc[useAsTitle] : undefined;
|
|
338
|
+
if (typeof sourceTitle === "string" && sourceTitle.trim())
|
|
339
|
+
title = sourceTitle;
|
|
340
|
+
}
|
|
326
341
|
infos.push({
|
|
327
342
|
key: target.key,
|
|
328
343
|
title: title ?? target.id,
|
package/dist/domain/propose.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { PayloadRequest } from "payload";
|
|
2
2
|
import type { ValidatedEditorAssistantOptions } from "../config/validate.ts";
|
|
3
|
+
import { type FieldPathGroup } from "../form/field-label.ts";
|
|
3
4
|
export type ProposeInput = {
|
|
4
5
|
capability: "form.propose" | "draft.create" | "content.update";
|
|
5
6
|
collection?: string;
|
|
@@ -22,6 +23,15 @@ export type ProposeInput = {
|
|
|
22
23
|
export type ProposalLine = {
|
|
23
24
|
path: string;
|
|
24
25
|
text: string;
|
|
26
|
+
/** Parent groups, blocks and row positions, separate from an editable field's label. */
|
|
27
|
+
context?: string;
|
|
28
|
+
label?: string;
|
|
29
|
+
groups?: FieldPathGroup[];
|
|
30
|
+
/** Complete stored content before and after compilation, not a truncated Markdown diff. */
|
|
31
|
+
richText?: {
|
|
32
|
+
before: unknown;
|
|
33
|
+
after: unknown;
|
|
34
|
+
};
|
|
25
35
|
edit?: {
|
|
26
36
|
label: string;
|
|
27
37
|
before: string;
|
package/dist/domain/propose.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { changedPaths } from "../form/changed-paths.js";
|
|
2
2
|
import { compileOperations } from "../form/compile.js";
|
|
3
3
|
import { humanDiff, richTextLines } from "../form/diff.js";
|
|
4
|
+
import { fieldPathInfo } from "../form/field-label.js";
|
|
4
5
|
import { hashFormRevision } from "../form/hash.js";
|
|
5
6
|
import { isFormOperation, normalizeOperations } from "../form/operations.js";
|
|
6
7
|
import { getAtPath, parseFieldPath } from "../form/path.js";
|
|
7
8
|
import { relationValueError } from "../form/relation-shape.js";
|
|
8
9
|
import { loadRichText, toBlocks } from "../form/rich-text.js";
|
|
9
|
-
import { finalizeCompiled, openRichText } from "../form/shape.js";
|
|
10
|
+
import { finalizeCompiled, openRichText, richTextPaths } from "../form/shape.js";
|
|
10
11
|
import { DEFAULT_APPROVAL_SLUG } from "../package-name.js";
|
|
11
12
|
import { entityFields, findCollectionConfig, hasDrafts } from "../schema/discover.js";
|
|
12
13
|
import { fieldAtPath } from "../schema/fields.js";
|
|
13
|
-
import {
|
|
14
|
+
import { projectFormRecord } from "../schema/project.js";
|
|
14
15
|
import { hasCapability, resolveAllowlist } from "./allowlist.js";
|
|
15
16
|
import { writeAudit } from "./audit.js";
|
|
16
|
-
import {
|
|
17
|
+
import { readDocument } from "./content.js";
|
|
17
18
|
import { localApi } from "./local-api.js";
|
|
18
19
|
import { labelRelations, verifyDocumentRelations } from "./relations.js";
|
|
19
20
|
/** Long enough to look at a change in the preview before keeping it. */
|
|
@@ -97,14 +98,40 @@ export async function proposeChange(req, options, input) {
|
|
|
97
98
|
: undefined;
|
|
98
99
|
const inRichText = (operation) => "path" in operation &&
|
|
99
100
|
richTextBefore.some(({ path }) => operation.path === path || operation.path.startsWith(`${path}.`));
|
|
101
|
+
const fieldLabel = (path) => fieldPathInfo(fields, path, current, input.language);
|
|
100
102
|
const diff = [
|
|
101
|
-
...humanDiff(current, typedOperations.filter((operation) => !inRichText(operation)), input.language, labels),
|
|
102
|
-
...richTextBefore.flatMap(({ path, blocks, after }) => richTextLines(path, blocks, Array.isArray(after) ? after : [after], input.language)),
|
|
103
|
+
...humanDiff(current, typedOperations.filter((operation) => !inRichText(operation)), input.language, labels, (path) => fieldLabel(path).full),
|
|
104
|
+
...richTextBefore.flatMap(({ path, blocks, after }) => richTextLines(path, blocks, Array.isArray(after) ? after : [after], input.language, (path) => fieldLabel(path).full)),
|
|
103
105
|
];
|
|
104
|
-
const lines = diff
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const lines = diff
|
|
107
|
+
.filter((line) => !richTextBefore.some(({ path }) => path === line.path))
|
|
108
|
+
.map((line) => {
|
|
109
|
+
const { context, label, groups } = fieldLabel(line.path);
|
|
110
|
+
return {
|
|
111
|
+
...line,
|
|
112
|
+
label,
|
|
113
|
+
groups,
|
|
114
|
+
...(context ? { context } : {}),
|
|
115
|
+
...editableLine(line.path, typedOperations, fields, current),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
for (const { path } of richTextBefore) {
|
|
119
|
+
const { context, label, groups } = fieldLabel(path);
|
|
120
|
+
lines.push({
|
|
121
|
+
path,
|
|
122
|
+
label,
|
|
123
|
+
groups,
|
|
124
|
+
...(context ? { context } : {}),
|
|
125
|
+
text: diff
|
|
126
|
+
.filter((line) => line.path === path)
|
|
127
|
+
.map((line) => line.text)
|
|
128
|
+
.join("\n"),
|
|
129
|
+
richText: {
|
|
130
|
+
before: getAtPath(current, parseFieldPath(path)),
|
|
131
|
+
after: getAtPath(finalized.data, parseFieldPath(path)),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
108
135
|
let created;
|
|
109
136
|
try {
|
|
110
137
|
created = await localApi(req).create({
|
|
@@ -179,7 +206,10 @@ async function resolveCurrentForm(req, options, input) {
|
|
|
179
206
|
if (input.capability !== "form.propose") {
|
|
180
207
|
return {};
|
|
181
208
|
}
|
|
182
|
-
const
|
|
209
|
+
const resolved = resolveAllowlist(options, input);
|
|
210
|
+
if (!resolved || !hasCapability(resolved.allowlist, "content.findById"))
|
|
211
|
+
return snapshot;
|
|
212
|
+
const saved = await readDocument(req, options, {
|
|
183
213
|
collection: input.collection,
|
|
184
214
|
global: input.global,
|
|
185
215
|
documentId: input.documentId,
|
|
@@ -188,7 +218,7 @@ async function resolveCurrentForm(req, options, input) {
|
|
|
188
218
|
if (!saved.ok) {
|
|
189
219
|
return snapshot;
|
|
190
220
|
}
|
|
191
|
-
return { ...saved.
|
|
221
|
+
return { ...projectFormRecord(saved.doc, saved.fields, options.redact, saved.slug), ...snapshot };
|
|
192
222
|
}
|
|
193
223
|
/** A saved document as the batch sees it: allowlisted fields in the target's language. */
|
|
194
224
|
export async function readSavedForm(req, options, target) {
|
|
@@ -203,7 +233,7 @@ export async function readSavedForm(req, options, target) {
|
|
|
203
233
|
if (!read.ok) {
|
|
204
234
|
return null;
|
|
205
235
|
}
|
|
206
|
-
return
|
|
236
|
+
return projectFormRecord(read.doc, read.fields, options.redact, read.slug);
|
|
207
237
|
}
|
|
208
238
|
/**
|
|
209
239
|
* A batch proposal sets or clears values within its target's fields: no rows or blocks added,
|
|
@@ -263,23 +293,6 @@ function draftCreateDenied(req, collection) {
|
|
|
263
293
|
}
|
|
264
294
|
return null;
|
|
265
295
|
}
|
|
266
|
-
/** Rich text fields the operations reach, whole or by block, as form paths. */
|
|
267
|
-
function richTextPaths(operations, fields, current) {
|
|
268
|
-
const paths = new Set();
|
|
269
|
-
for (const operation of operations) {
|
|
270
|
-
if (!("path" in operation))
|
|
271
|
-
continue;
|
|
272
|
-
const segments = operation.path.split(".");
|
|
273
|
-
for (let end = 1; end <= segments.length; end++) {
|
|
274
|
-
const prefix = segments.slice(0, end).join(".");
|
|
275
|
-
if (fieldAtPath(fields, prefix, current)?.type === "richText") {
|
|
276
|
-
paths.add(prefix);
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return [...paths];
|
|
282
|
-
}
|
|
283
296
|
/** The stored rich text at `path` as the model read it: Markdown blocks and `{ keep, type }`. */
|
|
284
297
|
function storedBlocks(fields, path, current) {
|
|
285
298
|
const field = fieldAtPath(fields, path, current);
|
package/dist/form/diff.d.ts
CHANGED
|
@@ -8,11 +8,11 @@ export type DiffLanguage = "sv" | "en";
|
|
|
8
8
|
* `labels` names related documents by id (see `labelRelations`), so a chosen image reads as its
|
|
9
9
|
* filename or alt text instead of an id. Rich text is listed by `richTextLines` instead.
|
|
10
10
|
*/
|
|
11
|
-
export declare function humanDiff(current: Record<string, unknown>, operations: FormOperation[], language?: DiffLanguage, labels?: Map<string, string
|
|
11
|
+
export declare function humanDiff(current: Record<string, unknown>, operations: FormOperation[], language?: DiffLanguage, labels?: Map<string, string>, fieldLabel?: (path: string) => string): HumanDiffLine[];
|
|
12
12
|
/**
|
|
13
13
|
* The parts of a rich text field that changed: `before` as the model read it (Markdown blocks),
|
|
14
14
|
* `after` as compiled, where `{ keep: i }` is block `i` unchanged.
|
|
15
15
|
*/
|
|
16
|
-
export declare function richTextLines(path: string, before: unknown[], after: unknown[], language?: DiffLanguage): HumanDiffLine[];
|
|
16
|
+
export declare function richTextLines(path: string, before: unknown[], after: unknown[], language?: DiffLanguage, fieldLabel?: (path: string) => string): HumanDiffLine[];
|
|
17
17
|
/** Sub-lines are indented two spaces; the approval panel nests them under the header. */
|
|
18
18
|
export declare const DIFF_SUBLINE_INDENT = " ";
|