@simmalugnt-se/payload-editor-assistant 0.11.3 → 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 +16 -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/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,21 @@
|
|
|
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
|
+
|
|
3
19
|
## 0.11.3
|
|
4
20
|
|
|
5
21
|
- Clicking "Fix N with the assistant" again after closing the panel opens it on the batch that is
|
|
@@ -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";
|
|
@@ -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 = " ";
|
package/dist/form/diff.js
CHANGED
|
@@ -41,7 +41,7 @@ const FIELD_PREVIEW = 300;
|
|
|
41
41
|
* `labels` names related documents by id (see `labelRelations`), so a chosen image reads as its
|
|
42
42
|
* filename or alt text instead of an id. Rich text is listed by `richTextLines` instead.
|
|
43
43
|
*/
|
|
44
|
-
export function humanDiff(current, operations, language = "en", labels) {
|
|
44
|
+
export function humanDiff(current, operations, language = "en", labels, fieldLabel = label) {
|
|
45
45
|
const copy = COPY[language];
|
|
46
46
|
const nameOf = (value) => {
|
|
47
47
|
const id = typeof value === "string" || typeof value === "number"
|
|
@@ -65,30 +65,30 @@ export function humanDiff(current, operations, language = "en", labels) {
|
|
|
65
65
|
const before = getAtPath(current, parseFieldPath(operation.path));
|
|
66
66
|
return {
|
|
67
67
|
path: operation.path,
|
|
68
|
-
text: `${
|
|
68
|
+
text: `${fieldLabel(operation.path)}: ${preview(named(before), copy)} → ${preview(named(operation.value), copy)}`,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
case "unset":
|
|
72
|
-
return { path: operation.path, text: copy.clear(
|
|
72
|
+
return { path: operation.path, text: copy.clear(fieldLabel(operation.path)) };
|
|
73
73
|
case "array.insert":
|
|
74
74
|
return {
|
|
75
75
|
path: operation.path,
|
|
76
|
-
text: withFields(copy.addItem(operation.index + 1,
|
|
76
|
+
text: withFields(copy.addItem(operation.index + 1, fieldLabel(operation.path)), operation.value, copy),
|
|
77
77
|
};
|
|
78
78
|
case "array.remove":
|
|
79
79
|
return {
|
|
80
80
|
path: operation.path,
|
|
81
|
-
text: copy.removeItem(operation.index + 1,
|
|
81
|
+
text: copy.removeItem(operation.index + 1, fieldLabel(operation.path)),
|
|
82
82
|
};
|
|
83
83
|
case "array.move":
|
|
84
84
|
return {
|
|
85
85
|
path: operation.path,
|
|
86
|
-
text: copy.moveItem(operation.from + 1, operation.to + 1,
|
|
86
|
+
text: copy.moveItem(operation.from + 1, operation.to + 1, fieldLabel(operation.path)),
|
|
87
87
|
};
|
|
88
88
|
case "array.replace":
|
|
89
89
|
return {
|
|
90
90
|
path: operation.path,
|
|
91
|
-
text: withFields(copy.replaceItem(operation.index + 1,
|
|
91
|
+
text: withFields(copy.replaceItem(operation.index + 1, fieldLabel(operation.path)), operation.value, copy),
|
|
92
92
|
};
|
|
93
93
|
case "blocks.insert":
|
|
94
94
|
return {
|
|
@@ -98,7 +98,7 @@ export function humanDiff(current, operations, language = "en", labels) {
|
|
|
98
98
|
case "blocks.remove":
|
|
99
99
|
return {
|
|
100
100
|
path: operation.path,
|
|
101
|
-
text: copy.removeBlock(operation.index + 1,
|
|
101
|
+
text: copy.removeBlock(operation.index + 1, fieldLabel(operation.path)),
|
|
102
102
|
};
|
|
103
103
|
case "blocks.move":
|
|
104
104
|
return { path: operation.path, text: copy.moveBlock(operation.from + 1, operation.to + 1) };
|
|
@@ -116,9 +116,9 @@ export function humanDiff(current, operations, language = "en", labels) {
|
|
|
116
116
|
* The parts of a rich text field that changed: `before` as the model read it (Markdown blocks),
|
|
117
117
|
* `after` as compiled, where `{ keep: i }` is block `i` unchanged.
|
|
118
118
|
*/
|
|
119
|
-
export function richTextLines(path, before, after, language = "en") {
|
|
119
|
+
export function richTextLines(path, before, after, language = "en", fieldLabel = label) {
|
|
120
120
|
const copy = COPY[language];
|
|
121
|
-
const field =
|
|
121
|
+
const field = fieldLabel(path);
|
|
122
122
|
const part = (block) => typeof block === "string"
|
|
123
123
|
? preview(block, copy, FIELD_PREVIEW)
|
|
124
124
|
: `[${block.type ?? "?"}]`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FieldNode } from "../schema/fields.ts";
|
|
2
|
+
export type FieldPathGroup = {
|
|
3
|
+
path: string;
|
|
4
|
+
label: string;
|
|
5
|
+
kind: "container" | "block" | "row" | "group";
|
|
6
|
+
position?: number;
|
|
7
|
+
};
|
|
8
|
+
/** Schema labels and one-based row positions for a concrete form field path. */
|
|
9
|
+
export declare function fieldPathLabels(fields: FieldNode[], path: string, data: unknown, language?: "en" | "sv"): {
|
|
10
|
+
label: string;
|
|
11
|
+
context?: string;
|
|
12
|
+
full: string;
|
|
13
|
+
};
|
|
14
|
+
/** Structured parents let the review group fields instead of repeating breadcrumbs. */
|
|
15
|
+
export declare function fieldPathInfo(fields: FieldNode[], path: string, data: unknown, language?: "en" | "sv"): {
|
|
16
|
+
label: string;
|
|
17
|
+
context?: string;
|
|
18
|
+
full: string;
|
|
19
|
+
groups: FieldPathGroup[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Schema labels and one-based row positions for a concrete form field path. */
|
|
2
|
+
export function fieldPathLabels(fields, path, data, language = "en") {
|
|
3
|
+
const { groups: _groups, ...labels } = fieldPathInfo(fields, path, data, language);
|
|
4
|
+
return labels;
|
|
5
|
+
}
|
|
6
|
+
/** Structured parents let the review group fields instead of repeating breadcrumbs. */
|
|
7
|
+
export function fieldPathInfo(fields, path, data, language = "en") {
|
|
8
|
+
const parts = [];
|
|
9
|
+
const groups = [];
|
|
10
|
+
let scope = fields;
|
|
11
|
+
let value = data;
|
|
12
|
+
let field;
|
|
13
|
+
let schemaPath = "";
|
|
14
|
+
let concretePath = "";
|
|
15
|
+
for (const segment of path.split(".")) {
|
|
16
|
+
concretePath = concretePath ? `${concretePath}.${segment}` : segment;
|
|
17
|
+
value = value?.[segment];
|
|
18
|
+
if (/^\d+$/.test(segment)) {
|
|
19
|
+
const position = Number(segment) + 1;
|
|
20
|
+
if (field?.type === "blocks") {
|
|
21
|
+
const blockType = value?.blockType;
|
|
22
|
+
const block = field.blocks?.find((block) => block.slug === blockType);
|
|
23
|
+
parts.push(`${name(block?.label ?? block?.slug ?? "block")} (block ${position})`);
|
|
24
|
+
groups.push({
|
|
25
|
+
path: concretePath,
|
|
26
|
+
label: name(block?.label ?? block?.slug ?? "block"),
|
|
27
|
+
kind: "block",
|
|
28
|
+
position,
|
|
29
|
+
});
|
|
30
|
+
scope = block?.fields ?? [];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const row = language === "sv" ? "rad" : "row";
|
|
34
|
+
parts[parts.length - 1] = `${parts.at(-1) ?? ""} (${row} ${position})`;
|
|
35
|
+
groups.push({
|
|
36
|
+
path: concretePath,
|
|
37
|
+
label: `${name(row)} ${position}`,
|
|
38
|
+
kind: "row",
|
|
39
|
+
position,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
schemaPath = schemaPath ? `${schemaPath}.${segment}` : segment;
|
|
45
|
+
field = scope.find((entry) => entry.path === schemaPath);
|
|
46
|
+
parts.push(name(field?.label ?? segment));
|
|
47
|
+
if (field?.type === "group" || field?.type === "blocks" || field?.type === "array") {
|
|
48
|
+
groups.push({
|
|
49
|
+
path: concretePath,
|
|
50
|
+
label: name(field.label ?? segment),
|
|
51
|
+
kind: field.type === "group" ? "group" : "container",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (!field && scope.some((entry) => entry.path.startsWith(`${schemaPath}.`))) {
|
|
55
|
+
groups.push({ path: concretePath, label: name(segment), kind: "group" });
|
|
56
|
+
}
|
|
57
|
+
// Named tabs are flattened by walkFields, but their name remains in each child's path.
|
|
58
|
+
if (field)
|
|
59
|
+
scope = field.fields ?? [];
|
|
60
|
+
}
|
|
61
|
+
const label = parts.at(-1) ?? name(path);
|
|
62
|
+
const context = parts.length > 1 ? parts.slice(0, -1).join(" › ") : undefined;
|
|
63
|
+
return {
|
|
64
|
+
label,
|
|
65
|
+
...(context ? { context } : {}),
|
|
66
|
+
full: parts.join(" › ") || label,
|
|
67
|
+
groups: groups.filter((group) => group.path !== path),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function name(value) {
|
|
71
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
72
|
+
}
|
package/dist/form/shape.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FieldNode } from "../schema/fields.ts";
|
|
2
|
+
import type { FormOperation } from "./operations.ts";
|
|
2
3
|
import { type RichTextLink } from "./rich-text.ts";
|
|
4
|
+
/** Rich text fields the operations reach, whole or by block, as form paths. */
|
|
5
|
+
export declare function richTextPaths(operations: FormOperation[], fields: FieldNode[], current: Record<string, unknown>): string[];
|
|
3
6
|
/**
|
|
4
7
|
* A copy of `data` where each rich text field the converters can read is a list of `{ keep: i }`,
|
|
5
8
|
* one per stored block, so operations can set, insert or remove single blocks (`layout.1.content.2`)
|
package/dist/form/shape.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
|
+
import { fieldAtPath } from "../schema/fields.js";
|
|
1
2
|
import { fromBlocks, isLexicalState, isRichTextBlocks, toBlocks, } from "./rich-text.js";
|
|
3
|
+
/** Rich text fields the operations reach, whole or by block, as form paths. */
|
|
4
|
+
export function richTextPaths(operations, fields, current) {
|
|
5
|
+
const paths = new Set();
|
|
6
|
+
for (const operation of operations) {
|
|
7
|
+
if (!("path" in operation))
|
|
8
|
+
continue;
|
|
9
|
+
const segments = operation.path.split(".");
|
|
10
|
+
for (let end = 1; end <= segments.length; end++) {
|
|
11
|
+
const prefix = segments.slice(0, end).join(".");
|
|
12
|
+
if (fieldAtPath(fields, prefix, current)?.type === "richText") {
|
|
13
|
+
paths.add(prefix);
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [...paths];
|
|
19
|
+
}
|
|
2
20
|
/**
|
|
3
21
|
* A copy of `data` where each rich text field the converters can read is a list of `{ keep: i }`,
|
|
4
22
|
* one per stored block, so operations can set, insert or remove single blocks (`layout.1.content.2`)
|
package/dist/runtime/agent.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type AgentChatResult = {
|
|
|
13
13
|
capability: "form.propose" | "draft.create" | "content.update";
|
|
14
14
|
/** The form with the change applied, shown in Admin before the editor keeps it. */
|
|
15
15
|
preview?: ProposalPreview;
|
|
16
|
-
/**
|
|
16
|
+
/** Structured fields and rich text previews for Admin's review. */
|
|
17
17
|
lines?: ProposalLine[];
|
|
18
18
|
};
|
|
19
19
|
navigate?: {
|
package/dist/runtime/agent.js
CHANGED
|
@@ -228,7 +228,10 @@ function buildSdkTools(ai, ctx, captured, previews, emit) {
|
|
|
228
228
|
: { ...executed, preview: undefined };
|
|
229
229
|
if (preview && executed.ok && executed.pendingApproval) {
|
|
230
230
|
previews.set(executed.pendingApproval.approvalId, preview);
|
|
231
|
-
emit({
|
|
231
|
+
emit({
|
|
232
|
+
type: "proposal",
|
|
233
|
+
pendingApproval: { ...executed.pendingApproval, lines: executed.lines, preview },
|
|
234
|
+
});
|
|
232
235
|
}
|
|
233
236
|
emit({
|
|
234
237
|
type: "step",
|
package/dist/runtime/tools.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type ToolSuccess = {
|
|
|
53
53
|
nonce: string;
|
|
54
54
|
capability: "form.propose" | "draft.create" | "content.update";
|
|
55
55
|
};
|
|
56
|
-
/**
|
|
56
|
+
/** Structured review data for Admin, including full rich text previews. */
|
|
57
57
|
lines?: ProposalLine[];
|
|
58
58
|
navigate?: {
|
|
59
59
|
href: string;
|
|
@@ -94,7 +94,36 @@ export declare function executeTool(name: string, input: Record<string, unknown>
|
|
|
94
94
|
*/
|
|
95
95
|
export declare function toolModelOutput(outcome: ToolOutcome): {
|
|
96
96
|
type: "json";
|
|
97
|
-
value:
|
|
97
|
+
value: ToolFailure;
|
|
98
|
+
} | {
|
|
99
|
+
type: "json";
|
|
100
|
+
value: {
|
|
101
|
+
ok: true;
|
|
102
|
+
capability: string;
|
|
103
|
+
data?: unknown;
|
|
104
|
+
pendingApproval?: {
|
|
105
|
+
title: string;
|
|
106
|
+
summary: string;
|
|
107
|
+
approvalId: string;
|
|
108
|
+
nonce: string;
|
|
109
|
+
capability: "form.propose" | "draft.create" | "content.update";
|
|
110
|
+
};
|
|
111
|
+
navigate?: {
|
|
112
|
+
href: string;
|
|
113
|
+
label: string;
|
|
114
|
+
} | {
|
|
115
|
+
candidates: Array<{
|
|
116
|
+
href: string;
|
|
117
|
+
label: string;
|
|
118
|
+
}>;
|
|
119
|
+
};
|
|
120
|
+
/** For the model only, as file parts (see `toolModelOutput`); never sent to Admin. */
|
|
121
|
+
images?: Array<{
|
|
122
|
+
id?: string | number;
|
|
123
|
+
mediaType: string;
|
|
124
|
+
data: string;
|
|
125
|
+
}>;
|
|
126
|
+
};
|
|
98
127
|
} | {
|
|
99
128
|
type: "content";
|
|
100
129
|
value: ({
|
package/dist/runtime/tools.js
CHANGED
|
@@ -233,7 +233,7 @@ export async function executeTool(name, input, ctx) {
|
|
|
233
233
|
nonce: proposed.nonce,
|
|
234
234
|
capability,
|
|
235
235
|
},
|
|
236
|
-
|
|
236
|
+
lines: proposed.lines,
|
|
237
237
|
preview: proposed.preview,
|
|
238
238
|
};
|
|
239
239
|
}
|
|
@@ -246,10 +246,15 @@ export async function executeTool(name, input, ctx) {
|
|
|
246
246
|
* has one, and the rest as text before them.
|
|
247
247
|
*/
|
|
248
248
|
export function toolModelOutput(outcome) {
|
|
249
|
-
if (!outcome.ok
|
|
249
|
+
if (!outcome.ok) {
|
|
250
250
|
return { type: "json", value: outcome };
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
// Full previews belong to Admin; the model already has its operations and the text summary.
|
|
253
|
+
const { lines: _lines, preview: _preview, ...modelOutcome } = outcome;
|
|
254
|
+
if (!modelOutcome.images?.length) {
|
|
255
|
+
return { type: "json", value: modelOutcome };
|
|
256
|
+
}
|
|
257
|
+
const { images, ...rest } = modelOutcome;
|
|
253
258
|
return {
|
|
254
259
|
type: "content",
|
|
255
260
|
value: [
|
package/dist/schema/project.d.ts
CHANGED
|
@@ -3,10 +3,15 @@ import { type FieldNode } from "./fields.ts";
|
|
|
3
3
|
export declare function projectRecord(data: Record<string, unknown> | null | undefined, fields: FieldNode[], redact?: (input: {
|
|
4
4
|
slug: string;
|
|
5
5
|
data: Record<string, unknown>;
|
|
6
|
-
}) => Record<string, unknown>, slug?: string): {
|
|
6
|
+
}) => Record<string, unknown>, slug?: string, mode?: "model" | "form"): {
|
|
7
7
|
data: Record<string, unknown>;
|
|
8
8
|
truncated: boolean;
|
|
9
9
|
};
|
|
10
|
+
/** Allowlisted, redacted form data keeps stored rich text and is never replaced by a size excerpt. */
|
|
11
|
+
export declare function projectFormRecord(data: Record<string, unknown>, fields: FieldNode[], redact?: (input: {
|
|
12
|
+
slug: string;
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
}) => Record<string, unknown>, slug?: string): Record<string, unknown>;
|
|
10
15
|
export declare function projectIdentity(doc: Record<string, unknown>, locale?: string): Record<string, unknown>;
|
|
11
16
|
export declare function capRecord(data: Record<string, unknown>): {
|
|
12
17
|
data: Record<string, unknown>;
|
package/dist/schema/project.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isDeniedFieldName } from "../config/denied-fields.js";
|
|
|
2
2
|
import { toBlocks } from "../form/rich-text.js";
|
|
3
3
|
import { isFieldAllowed } from "./fields.js";
|
|
4
4
|
const MAX_CHARS = 16_000;
|
|
5
|
-
export function projectRecord(data, fields, redact, slug = "") {
|
|
5
|
+
export function projectRecord(data, fields, redact, slug = "", mode = "model") {
|
|
6
6
|
const projected = {};
|
|
7
7
|
if (!data) {
|
|
8
8
|
return { data: projected, truncated: false };
|
|
@@ -11,10 +11,14 @@ export function projectRecord(data, fields, redact, slug = "") {
|
|
|
11
11
|
if (!(field.name in data)) {
|
|
12
12
|
continue;
|
|
13
13
|
}
|
|
14
|
-
projected[field.name] = projectValue(data[field.name], field);
|
|
14
|
+
projected[field.name] = projectValue(data[field.name], field, mode);
|
|
15
15
|
}
|
|
16
16
|
const redacted = redact ? redact({ slug, data: projected }) : projected;
|
|
17
|
-
return capRecord(redacted);
|
|
17
|
+
return mode === "form" ? { data: redacted, truncated: false } : capRecord(redacted);
|
|
18
|
+
}
|
|
19
|
+
/** Allowlisted, redacted form data keeps stored rich text and is never replaced by a size excerpt. */
|
|
20
|
+
export function projectFormRecord(data, fields, redact, slug = "") {
|
|
21
|
+
return projectRecord(data, fields, redact, slug, "form").data;
|
|
18
22
|
}
|
|
19
23
|
export function projectIdentity(doc, locale) {
|
|
20
24
|
const identity = {
|
|
@@ -53,13 +57,14 @@ export function filterRootKeys(data, allowlist, denyFields) {
|
|
|
53
57
|
}
|
|
54
58
|
return next;
|
|
55
59
|
}
|
|
56
|
-
function projectValue(value, field) {
|
|
60
|
+
function projectValue(value, field, mode) {
|
|
57
61
|
if (value === null || value === undefined) {
|
|
58
62
|
return value;
|
|
59
63
|
}
|
|
60
64
|
if (field.type === "array" && Array.isArray(value)) {
|
|
61
65
|
return value.map((item) => item && typeof item === "object" && !Array.isArray(item)
|
|
62
|
-
? projectRecord(item, field.fields ?? [])
|
|
66
|
+
? projectRecord(item, field.fields ?? [], undefined, "", mode)
|
|
67
|
+
.data
|
|
63
68
|
: item);
|
|
64
69
|
}
|
|
65
70
|
if (field.type === "blocks" && Array.isArray(value)) {
|
|
@@ -70,7 +75,7 @@ function projectValue(value, field) {
|
|
|
70
75
|
const record = item;
|
|
71
76
|
const blockType = typeof record.blockType === "string" ? record.blockType : "";
|
|
72
77
|
const block = field.blocks?.find((entry) => entry.slug === blockType);
|
|
73
|
-
const projected = projectRecord(record, block?.fields ?? []).data;
|
|
78
|
+
const projected = projectRecord(record, block?.fields ?? [], undefined, "", mode).data;
|
|
74
79
|
return {
|
|
75
80
|
id: record.id,
|
|
76
81
|
blockType,
|
|
@@ -79,13 +84,14 @@ function projectValue(value, field) {
|
|
|
79
84
|
});
|
|
80
85
|
}
|
|
81
86
|
if (field.type === "group" && value && typeof value === "object" && !Array.isArray(value)) {
|
|
82
|
-
return projectRecord(value, field.fields ?? [])
|
|
87
|
+
return projectRecord(value, field.fields ?? [], undefined, "", mode)
|
|
88
|
+
.data;
|
|
83
89
|
}
|
|
84
90
|
if (field.type === "relationship" || field.type === "upload") {
|
|
85
91
|
return projectRelation(value);
|
|
86
92
|
}
|
|
87
93
|
if (field.type === "richText") {
|
|
88
|
-
return toBlocks(value, field.editor) ?? value;
|
|
94
|
+
return mode === "form" ? value : (toBlocks(value, field.editor) ?? value);
|
|
89
95
|
}
|
|
90
96
|
return value;
|
|
91
97
|
}
|
|
@@ -240,6 +240,12 @@ html[data-theme="dark"] .ea-aside {
|
|
|
240
240
|
line-height: 1.5;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
.ea-approval__review {
|
|
244
|
+
max-height: 50vh;
|
|
245
|
+
overflow-y: auto;
|
|
246
|
+
margin-bottom: calc(var(--base) * 0.6);
|
|
247
|
+
}
|
|
248
|
+
|
|
243
249
|
.ea-approval__hint {
|
|
244
250
|
margin: 0 0 calc(var(--base) * 0.4);
|
|
245
251
|
color: var(--theme-elevation-500);
|
|
@@ -479,7 +485,7 @@ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
|
|
|
479
485
|
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.6);
|
|
480
486
|
border: 1px solid var(--theme-elevation-150);
|
|
481
487
|
border-radius: var(--style-radius-m);
|
|
482
|
-
background: var(--theme-elevation-
|
|
488
|
+
background: var(--theme-elevation-0);
|
|
483
489
|
font-size: 13px;
|
|
484
490
|
line-height: 1.45;
|
|
485
491
|
}
|
|
@@ -504,10 +510,17 @@ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
|
|
|
504
510
|
}
|
|
505
511
|
|
|
506
512
|
.ea-batch__card-head {
|
|
513
|
+
position: sticky;
|
|
514
|
+
top: 0;
|
|
515
|
+
z-index: 1;
|
|
507
516
|
display: flex;
|
|
508
517
|
align-items: center;
|
|
509
518
|
gap: calc(var(--base) * 0.4);
|
|
510
519
|
min-width: 0;
|
|
520
|
+
margin: calc(var(--base) * -0.5) calc(var(--base) * -0.6) 0;
|
|
521
|
+
padding: calc(var(--base) * 0.5) calc(var(--base) * 0.6);
|
|
522
|
+
border-bottom: 1px solid var(--theme-elevation-150);
|
|
523
|
+
background: var(--theme-elevation-0);
|
|
511
524
|
}
|
|
512
525
|
|
|
513
526
|
.ea-batch__card-head input[type="checkbox"] {
|
|
@@ -549,29 +562,120 @@ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
|
|
|
549
562
|
font-size: 12px;
|
|
550
563
|
}
|
|
551
564
|
|
|
552
|
-
.ea-
|
|
553
|
-
|
|
554
|
-
|
|
565
|
+
.ea-review {
|
|
566
|
+
display: flex;
|
|
567
|
+
flex-direction: column;
|
|
568
|
+
gap: 16px;
|
|
569
|
+
margin-top: 16px;
|
|
570
|
+
font-size: 13px;
|
|
571
|
+
line-height: 1.5;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.ea-review__collection > h4 {
|
|
575
|
+
margin: 0 0 8px;
|
|
576
|
+
color: var(--theme-elevation-600);
|
|
577
|
+
font-size: 11px;
|
|
578
|
+
font-weight: 600;
|
|
579
|
+
letter-spacing: 0.06em;
|
|
580
|
+
text-transform: uppercase;
|
|
555
581
|
}
|
|
556
582
|
|
|
557
|
-
.ea-
|
|
583
|
+
.ea-review__children {
|
|
558
584
|
display: flex;
|
|
559
585
|
flex-direction: column;
|
|
560
|
-
gap:
|
|
561
|
-
|
|
562
|
-
list-style: none;
|
|
586
|
+
gap: 16px;
|
|
587
|
+
min-width: 0;
|
|
563
588
|
}
|
|
564
589
|
|
|
565
|
-
.ea-
|
|
590
|
+
.ea-review__group {
|
|
591
|
+
padding-top: 12px;
|
|
592
|
+
border-top: 1px solid var(--theme-elevation-150);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.ea-review__group > summary {
|
|
596
|
+
display: flex;
|
|
597
|
+
align-items: center;
|
|
598
|
+
gap: 8px;
|
|
599
|
+
cursor: pointer;
|
|
600
|
+
color: var(--theme-text);
|
|
601
|
+
font-size: 14px;
|
|
566
602
|
font-weight: 600;
|
|
603
|
+
list-style: none;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.ea-review__group > summary::-webkit-details-marker {
|
|
607
|
+
display: none;
|
|
567
608
|
}
|
|
568
609
|
|
|
569
|
-
.ea-
|
|
610
|
+
.ea-review__group > summary::before {
|
|
611
|
+
content: "";
|
|
612
|
+
width: 5px;
|
|
613
|
+
height: 5px;
|
|
614
|
+
flex-shrink: 0;
|
|
615
|
+
border-right: 1.5px solid currentColor;
|
|
616
|
+
border-bottom: 1.5px solid currentColor;
|
|
617
|
+
transform: rotate(-45deg);
|
|
618
|
+
transition: transform 100ms;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.ea-review__group[open] > summary::before {
|
|
622
|
+
transform: rotate(45deg);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.ea-review__group > summary:focus-visible {
|
|
626
|
+
outline: 2px solid var(--ea-accent);
|
|
627
|
+
outline-offset: 3px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.ea-review__group > .ea-review__children {
|
|
631
|
+
margin-top: 14px;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.ea-review__group--row,
|
|
635
|
+
.ea-review__group--group {
|
|
636
|
+
padding-top: 0;
|
|
637
|
+
padding-inline-start: 10px;
|
|
638
|
+
border-top: 0;
|
|
639
|
+
border-inline-start: 2px solid var(--theme-elevation-150);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.ea-review__group--row > summary,
|
|
643
|
+
.ea-review__group--group > summary {
|
|
644
|
+
font-size: 12px;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.ea-review__badge {
|
|
648
|
+
margin-inline-start: auto;
|
|
570
649
|
color: var(--theme-elevation-600);
|
|
571
|
-
|
|
650
|
+
font-size: 11px;
|
|
651
|
+
font-weight: 400;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.ea-review__field {
|
|
655
|
+
display: flex;
|
|
656
|
+
flex-direction: column;
|
|
657
|
+
gap: 6px;
|
|
658
|
+
min-width: 0;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.ea-review__field-head {
|
|
662
|
+
display: flex;
|
|
663
|
+
align-items: center;
|
|
664
|
+
justify-content: space-between;
|
|
665
|
+
gap: 8px;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.ea-review__field-head label {
|
|
669
|
+
font-size: 12px;
|
|
670
|
+
font-weight: 600;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.ea-review__new {
|
|
674
|
+
color: var(--theme-elevation-500);
|
|
675
|
+
font-size: 10px;
|
|
572
676
|
}
|
|
573
677
|
|
|
574
|
-
.ea-
|
|
678
|
+
.ea-review__field textarea {
|
|
575
679
|
box-sizing: border-box;
|
|
576
680
|
width: 100%;
|
|
577
681
|
padding: calc(var(--base) * 0.3) calc(var(--base) * 0.4);
|
|
@@ -586,12 +690,111 @@ html[data-theme="dark"] .ea-trigger--open .ea-trigger__icon {
|
|
|
586
690
|
min-height: calc(2lh + calc(var(--base) * 0.6));
|
|
587
691
|
}
|
|
588
692
|
|
|
589
|
-
.ea-
|
|
693
|
+
.ea-review__field textarea:focus-visible {
|
|
694
|
+
outline: 2px solid var(--ea-accent);
|
|
695
|
+
outline-offset: 1px;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.ea-review__count {
|
|
590
699
|
align-self: flex-end;
|
|
591
700
|
color: var(--theme-elevation-500);
|
|
592
701
|
font-size: 11px;
|
|
593
702
|
}
|
|
594
703
|
|
|
704
|
+
.ea-review__current {
|
|
705
|
+
color: var(--theme-elevation-600);
|
|
706
|
+
font-size: 12px;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.ea-review__current > summary {
|
|
710
|
+
cursor: pointer;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.ea-review__current > p,
|
|
714
|
+
.ea-review__value {
|
|
715
|
+
margin: 0;
|
|
716
|
+
white-space: pre-wrap;
|
|
717
|
+
overflow-wrap: anywhere;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.ea-review__current > p,
|
|
721
|
+
.ea-review__current > .ea-review__rich {
|
|
722
|
+
margin-top: 8px;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.ea-review__preview,
|
|
726
|
+
.ea-review__value {
|
|
727
|
+
padding: 12px;
|
|
728
|
+
border: 1px solid var(--theme-elevation-150);
|
|
729
|
+
border-radius: var(--style-radius-s);
|
|
730
|
+
background: var(--theme-elevation-50);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.ea-review__rich {
|
|
734
|
+
color: var(--theme-text);
|
|
735
|
+
line-height: 1.65;
|
|
736
|
+
overflow-wrap: anywhere;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
.ea-review__rich > :first-child {
|
|
740
|
+
margin-top: 0;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
.ea-review__rich > :last-child {
|
|
744
|
+
margin-bottom: 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.ea-review__rich p {
|
|
748
|
+
margin: 0 0 12px;
|
|
749
|
+
white-space: pre-wrap;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.ea-review__rich h1,
|
|
753
|
+
.ea-review__rich h2,
|
|
754
|
+
.ea-review__rich h3,
|
|
755
|
+
.ea-review__rich h4,
|
|
756
|
+
.ea-review__rich h5,
|
|
757
|
+
.ea-review__rich h6 {
|
|
758
|
+
margin: 16px 0 8px;
|
|
759
|
+
font-size: 14px;
|
|
760
|
+
font-weight: 600;
|
|
761
|
+
line-height: 1.4;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.ea-review__rich ul,
|
|
765
|
+
.ea-review__rich ol {
|
|
766
|
+
margin: 0 0 12px;
|
|
767
|
+
padding-inline-start: 20px;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.ea-review__rich li + li {
|
|
771
|
+
margin-top: 4px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.ea-review__rich blockquote {
|
|
775
|
+
margin: 12px 0;
|
|
776
|
+
padding-inline-start: 12px;
|
|
777
|
+
border-inline-start: 2px solid var(--theme-elevation-250);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.ea-review__link {
|
|
781
|
+
text-decoration: underline;
|
|
782
|
+
text-underline-offset: 2px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.ea-review__embedded {
|
|
786
|
+
display: block;
|
|
787
|
+
padding: 8px;
|
|
788
|
+
border: 1px dashed var(--theme-elevation-250);
|
|
789
|
+
color: var(--theme-elevation-600);
|
|
790
|
+
font-size: 12px;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.ea-review__change p {
|
|
794
|
+
margin: 0;
|
|
795
|
+
white-space: pre-wrap;
|
|
796
|
+
}
|
|
797
|
+
|
|
595
798
|
.ea-batch__text,
|
|
596
799
|
.ea-batch__hint {
|
|
597
800
|
margin: calc(var(--base) * 0.3) 0 0;
|