@kontourai/survey 1.12.0 → 1.14.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/dist/src/current-proposed-review-item.d.ts +6 -0
- package/dist/src/current-proposed-review-item.js +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/review-resource.d.ts +40 -0
- package/dist/src/review-workbench/review-queue-session.js +45 -12
- package/dist/src/review-workbench/review-workbench-css.generated.js +18 -2
- package/dist/src/review-workbench/review-workbench.css +18 -2
- package/dist/src/review-workbench/review-workbench.d.ts +11 -1
- package/dist/src/review-workbench/review-workbench.js +147 -12
- package/dist/src/review-workbench/review-workbench.standalone.css +18 -2
- package/package.json +1 -1
|
@@ -38,6 +38,12 @@ export interface CurrentProposedReviewItemInput {
|
|
|
38
38
|
readonly current?: string;
|
|
39
39
|
readonly proposed?: string;
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Whether the reviewer may edit the proposed value inline. Defaults to
|
|
43
|
+
* `true`. Pass `false` for keep/use/reject-only queues (see
|
|
44
|
+
* {@link ReviewItemSpec.editable}).
|
|
45
|
+
*/
|
|
46
|
+
readonly editable?: boolean;
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
43
49
|
* Builds the two-candidate current/proposed {@link ReviewItem} envelope that
|
|
@@ -38,6 +38,7 @@ export function currentProposedReviewItem(input) {
|
|
|
38
38
|
...(selectedCandidateId ? { selectedCandidateId } : {}),
|
|
39
39
|
...(input.rationale ? { rationale: input.rationale } : {}),
|
|
40
40
|
...(input.producerPolicy ? { producerPolicy: input.producerPolicy } : {}),
|
|
41
|
+
...(input.editable === false ? { editable: false } : {}),
|
|
41
42
|
projection: { ...input.projection, candidateSetId },
|
|
42
43
|
},
|
|
43
44
|
status: {
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { CandidateSetStatus, Candidate, CandidateSet, ClaimTarget, EscalationDimension, EscalationRecord, Extraction, Interpretation, LocatorScheme, ProvenanceResolution, RawSource, RawSourceKind, ReviewAuthorizing, ReviewAuthorizingAuthorizedAction, ReviewAuthorizingExchange, ReviewAuthorizingExplicitStatement, ReviewAuthorizingKind, ReviewOutcome, ReviewStatus, SurveyInput, } from "./types.js";
|
|
2
2
|
export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
3
3
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
|
-
export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
|
|
4
|
+
export type { CandidateRole, ClaimTargetHint, ProducerPolicy, ExtractionReference, ResourceEnvelope, ResourceMetadata, ReviewActor, ReviewCandidate, ReviewDecision, ReviewDecisionMode, ReviewDecisionSpec, ReviewDecisionStatus, ReviewItem, ReviewItemSpec, ReviewItemStatus, ReviewLocator, ReviewResource, ReviewResourceApiVersion, ReviewResourceKind, ReviewSession, ReviewSessionEvent, ReviewSessionEventSpec, ReviewSessionEventStatus, ReviewSessionEventType, ReviewSessionSpec, ReviewSessionStatus, ReviewValueDescriptor, ReviewValueType, SourceReference, SurveyRecordProjectionHint, } from "./review-resource.js";
|
|
5
5
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
6
6
|
export type { CandidateReviewRecordInput, SurveyClaimRecord, SurveyInputBuilderArgs, SurveyObservationInput, } from "./builder.js";
|
|
7
7
|
export { reviewedCandidateResolution } from "./reviewed-candidate-resolution.js";
|
|
@@ -105,6 +105,28 @@ export interface ReviewCandidate {
|
|
|
105
105
|
projection?: SurveyRecordProjectionHint;
|
|
106
106
|
producer?: Record<string, unknown>;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Well-known neutral value-type vocabulary for {@link ReviewValueDescriptor}.
|
|
110
|
+
* Survey defines NO field-schema system of its own; this deliberately MIRRORS
|
|
111
|
+
* the shape an upstream field-schema owner already uses (e.g. traverse's
|
|
112
|
+
* `TargetFieldSchema.type` / `ExtractionProposal.valueType`), so a producer can
|
|
113
|
+
* carry a reviewed field's declared type down to the review UI WITHOUT Survey
|
|
114
|
+
* importing that owner's package (structural match, zero coupling).
|
|
115
|
+
*/
|
|
116
|
+
export type ReviewValueType = "string" | "number" | "boolean" | "date" | "enum" | "array" | "object";
|
|
117
|
+
/**
|
|
118
|
+
* Optional, producer-supplied descriptor of a reviewed field's declared value
|
|
119
|
+
* shape. Purely descriptive: the workbench uses it ONLY to pick a typed editor
|
|
120
|
+
* (an enum `<select>`, a date/number input) and to validate a reviewer's edit
|
|
121
|
+
* before "Use proposed". Survey never re-derives, coerces, or overrides a
|
|
122
|
+
* candidate's value from it — a producer can still surface an out-of-shape
|
|
123
|
+
* candidate, which is exactly what a typed reviewer catches.
|
|
124
|
+
*/
|
|
125
|
+
export interface ReviewValueDescriptor {
|
|
126
|
+
type: ReviewValueType;
|
|
127
|
+
/** Allowed values — meaningful with `type: "enum"`; ignored otherwise. */
|
|
128
|
+
enumValues?: string[];
|
|
129
|
+
}
|
|
108
130
|
export interface ReviewItemSpec {
|
|
109
131
|
target: string;
|
|
110
132
|
candidates: ReviewCandidate[];
|
|
@@ -113,6 +135,24 @@ export interface ReviewItemSpec {
|
|
|
113
135
|
rationale?: string;
|
|
114
136
|
producerPolicy?: ProducerPolicy;
|
|
115
137
|
projection?: SurveyRecordProjectionHint;
|
|
138
|
+
/**
|
|
139
|
+
* Optional neutral descriptor of the reviewed field's declared value type.
|
|
140
|
+
* When present, the workbench renders a typed editor and validates a
|
|
141
|
+
* reviewer's inline edit against it before accepting the proposed value.
|
|
142
|
+
* Absent → a plain text editor with no validation (today's behavior).
|
|
143
|
+
*/
|
|
144
|
+
valueDescriptor?: ReviewValueDescriptor;
|
|
145
|
+
/**
|
|
146
|
+
* Whether the reviewer may edit the proposed value inline before accepting
|
|
147
|
+
* it. Defaults to `true` (today's behavior). Set `false` for queues where an
|
|
148
|
+
* edited value is meaningless or must not be accepted — an approve/keep
|
|
149
|
+
* identity decision, a value the producer will re-derive, a review that only
|
|
150
|
+
* chooses between the two candidates as given. The workbench then renders no
|
|
151
|
+
* editor at all: the decision is keep-current / use-proposed / reject only,
|
|
152
|
+
* and `effectiveValue` is always the selected candidate's own value. This is
|
|
153
|
+
* enforcement (the affordance is absent), not a cosmetic hide.
|
|
154
|
+
*/
|
|
155
|
+
editable?: boolean;
|
|
116
156
|
}
|
|
117
157
|
export interface ReviewItemStatus {
|
|
118
158
|
observedCandidateCount?: number;
|
|
@@ -197,7 +197,15 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
|
|
|
197
197
|
const candidate = candidateForDecision(item, decision);
|
|
198
198
|
const definition = workbenchDecisionDefinitions[decision];
|
|
199
199
|
const reviewDecisionName = `${item.metadata.name}-${decision}`;
|
|
200
|
-
|
|
200
|
+
// Carry the reviewer's inline edit in the event itself (accept-proposed
|
|
201
|
+
// only — it is the sole decision where an edited value is meaningful), so
|
|
202
|
+
// that replaying snapshot + events reconstructs editedValuesByItemName and
|
|
203
|
+
// the server apply boundary derives effectiveValue from it. Without this
|
|
204
|
+
// the edit lives only in browser state and never survives replay.
|
|
205
|
+
const editedValue = decision === "accept-proposed" ? session.editedValuesByItemName?.[item.metadata.name] : undefined;
|
|
206
|
+
const data = editedValue !== undefined
|
|
207
|
+
? { workbenchDecision: decision, workbenchEditedValue: editedValue }
|
|
208
|
+
: { workbenchDecision: decision };
|
|
201
209
|
events.push(buildReviewSessionEvent(session, {
|
|
202
210
|
sessionName,
|
|
203
211
|
sequence: events.length + 1,
|
|
@@ -252,24 +260,49 @@ export function replayReviewSessionEvents(startState, events) {
|
|
|
252
260
|
}
|
|
253
261
|
if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
|
|
254
262
|
&& event.spec.reviewItemName) {
|
|
263
|
+
const itemName = event.spec.reviewItemName;
|
|
255
264
|
if (isClearedWorkbenchDecisionEvent(event)) {
|
|
256
|
-
const { [
|
|
257
|
-
|
|
265
|
+
const { [itemName]: _removedDecision, ...remainingDecisions } = session.decisionsByItemName;
|
|
266
|
+
const { [itemName]: _removedEdit, ...remainingEdits } = session.editedValuesByItemName ?? {};
|
|
267
|
+
return { ...session, decisionsByItemName: remainingDecisions, editedValuesByItemName: remainingEdits };
|
|
258
268
|
}
|
|
259
269
|
const decision = workbenchDecisionFromEvent(event);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
if (!decision) {
|
|
271
|
+
return session;
|
|
272
|
+
}
|
|
273
|
+
// Restore the inline edit the event carried (accept-proposed only); any
|
|
274
|
+
// other decision, or an accept with no carried edit, clears a stale edit
|
|
275
|
+
// for this item so effectiveValue can't fall back to an edit the
|
|
276
|
+
// reviewer moved away from.
|
|
277
|
+
const editedValue = workbenchEditedValueFromEvent(event);
|
|
278
|
+
const editedValuesByItemName = { ...session.editedValuesByItemName };
|
|
279
|
+
if (decision === "accept-proposed" && editedValue !== undefined) {
|
|
280
|
+
editedValuesByItemName[itemName] = editedValue;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
delete editedValuesByItemName[itemName];
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...session,
|
|
287
|
+
decisionsByItemName: {
|
|
288
|
+
...session.decisionsByItemName,
|
|
289
|
+
[itemName]: decision,
|
|
290
|
+
},
|
|
291
|
+
editedValuesByItemName,
|
|
292
|
+
};
|
|
269
293
|
}
|
|
270
294
|
return session;
|
|
271
295
|
}, startState);
|
|
272
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Extracts a decision event's carried inline edit, or `undefined` when the
|
|
299
|
+
* event carries none. The edit rides `data.workbenchEditedValue`.
|
|
300
|
+
*/
|
|
301
|
+
function workbenchEditedValueFromEvent(event) {
|
|
302
|
+
return event.spec.data && "workbenchEditedValue" in event.spec.data
|
|
303
|
+
? event.spec.data.workbenchEditedValue
|
|
304
|
+
: undefined;
|
|
305
|
+
}
|
|
273
306
|
/**
|
|
274
307
|
* Detects the explicit "clear this ReviewItem's decision" replay signal (emitted
|
|
275
308
|
* by the workbench's "Change" / undo control): a decision event whose
|
|
@@ -558,7 +558,8 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
558
558
|
margin-top: 8px;
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
.survey-workbench-embed .editrow input
|
|
561
|
+
.survey-workbench-embed .editrow input,
|
|
562
|
+
.survey-workbench-embed .editrow select{
|
|
562
563
|
flex: 1;
|
|
563
564
|
min-width: 0;
|
|
564
565
|
font-size: 13.5px;
|
|
@@ -569,7 +570,8 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
569
570
|
padding: 6px 9px;
|
|
570
571
|
}
|
|
571
572
|
|
|
572
|
-
.survey-workbench-embed .editrow input:focus-visible
|
|
573
|
+
.survey-workbench-embed .editrow input:focus-visible,
|
|
574
|
+
.survey-workbench-embed .editrow select:focus-visible{
|
|
573
575
|
outline: 2px solid var(--k-brand);
|
|
574
576
|
outline-offset: 1px;
|
|
575
577
|
border-color: var(--k-brand);
|
|
@@ -581,6 +583,20 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
581
583
|
white-space: nowrap;
|
|
582
584
|
}
|
|
583
585
|
|
|
586
|
+
/* Validation message for a typed proposed-value edit (hidden until the mount
|
|
587
|
+
handler blocks a "Use proposed" on an out-of-shape value). */
|
|
588
|
+
.survey-workbench-embed .verr{
|
|
589
|
+
display: block;
|
|
590
|
+
margin-top: 5px;
|
|
591
|
+
font-size: 11.5px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
color: var(--k-negative);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.survey-workbench-embed .verr[hidden]{
|
|
597
|
+
display: none;
|
|
598
|
+
}
|
|
599
|
+
|
|
584
600
|
/* confidence + provenance */
|
|
585
601
|
|
|
586
602
|
.survey-workbench-embed .prov{
|
|
@@ -555,7 +555,8 @@
|
|
|
555
555
|
margin-top: 8px;
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
-
.survey-workbench-embed .editrow input
|
|
558
|
+
.survey-workbench-embed .editrow input,
|
|
559
|
+
.survey-workbench-embed .editrow select{
|
|
559
560
|
flex: 1;
|
|
560
561
|
min-width: 0;
|
|
561
562
|
font-size: 13.5px;
|
|
@@ -566,7 +567,8 @@
|
|
|
566
567
|
padding: 6px 9px;
|
|
567
568
|
}
|
|
568
569
|
|
|
569
|
-
.survey-workbench-embed .editrow input:focus-visible
|
|
570
|
+
.survey-workbench-embed .editrow input:focus-visible,
|
|
571
|
+
.survey-workbench-embed .editrow select:focus-visible{
|
|
570
572
|
outline: 2px solid var(--k-brand);
|
|
571
573
|
outline-offset: 1px;
|
|
572
574
|
border-color: var(--k-brand);
|
|
@@ -578,6 +580,20 @@
|
|
|
578
580
|
white-space: nowrap;
|
|
579
581
|
}
|
|
580
582
|
|
|
583
|
+
/* Validation message for a typed proposed-value edit (hidden until the mount
|
|
584
|
+
handler blocks a "Use proposed" on an out-of-shape value). */
|
|
585
|
+
.survey-workbench-embed .verr{
|
|
586
|
+
display: block;
|
|
587
|
+
margin-top: 5px;
|
|
588
|
+
font-size: 11.5px;
|
|
589
|
+
font-weight: 600;
|
|
590
|
+
color: var(--k-negative);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.survey-workbench-embed .verr[hidden]{
|
|
594
|
+
display: none;
|
|
595
|
+
}
|
|
596
|
+
|
|
581
597
|
/* confidence + provenance */
|
|
582
598
|
|
|
583
599
|
.survey-workbench-embed .prov{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type ReviewWorkbenchState } from "./review-queue-session.js";
|
|
2
2
|
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
3
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
4
|
-
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent } from "../review-resource.js";
|
|
4
|
+
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
|
|
5
5
|
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
6
6
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
7
7
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
@@ -179,6 +179,16 @@ export declare function createPersistentReviewSessionEventStore(options: Persist
|
|
|
179
179
|
export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState | ReviewQueueSessionState, _events?: readonly ReviewSessionEvent[], options?: {
|
|
180
180
|
readonly presentationAdapter?: ReviewPresentationAdapter;
|
|
181
181
|
}): string;
|
|
182
|
+
/**
|
|
183
|
+
* Validates a reviewer's raw (string) edit of a proposed value against the
|
|
184
|
+
* field's neutral {@link ReviewValueDescriptor}, returning a human-readable
|
|
185
|
+
* error message when the value violates the declared type/enum constraint, or
|
|
186
|
+
* `undefined` when it is acceptable — including when there is no descriptor or
|
|
187
|
+
* the type carries no single-line constraint (string/array/object). This is a
|
|
188
|
+
* FORMAT check only: it never coerces or rewrites the value (the workbench
|
|
189
|
+
* stores the reviewer's string edit unchanged, as it did before typed editors).
|
|
190
|
+
*/
|
|
191
|
+
export declare function validateProposedValue(descriptor: ReviewValueDescriptor | undefined, rawValue: string): string | undefined;
|
|
182
192
|
export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState, options?: MountReviewWorkbenchOptions): void;
|
|
183
193
|
export declare function browserReviewWorkbenchStartState(): ReviewQueueSessionState | ReviewWorkbenchState | undefined;
|
|
184
194
|
declare global {
|
|
@@ -567,7 +567,7 @@ function renderDiffRow(item, current, proposed, targetLabel, decided, effectiveP
|
|
|
567
567
|
<div class="val proposed">
|
|
568
568
|
<div class="vlbl">Proposed</div>
|
|
569
569
|
<div class="vtext" data-value data-testid="proposed-value">${escapeHtml(effectiveProposedText)}</div>
|
|
570
|
-
${decided ? "" : renderProposedValueEditor(item, proposed, targetLabel)}
|
|
570
|
+
${decided || item.spec.editable === false ? "" : renderProposedValueEditor(item, proposed, targetLabel)}
|
|
571
571
|
</div>
|
|
572
572
|
</div>
|
|
573
573
|
`;
|
|
@@ -581,25 +581,131 @@ function renderDiffRow(item, current, proposed, targetLabel, decided, effectiveP
|
|
|
581
581
|
* from an enum's allowed values, or date/number/boolean inputs — driven by an
|
|
582
582
|
* optional neutral value-type descriptor, without touching the rest of the
|
|
583
583
|
* field-card renderer. Survey deliberately has no value-type/enum system of its
|
|
584
|
-
* own (that belongs to the upstream field-schema owner)
|
|
585
|
-
*
|
|
584
|
+
* own (that belongs to the upstream field-schema owner) — but a producer MAY
|
|
585
|
+
* carry a field's declared shape down via the neutral
|
|
586
|
+
* {@link ReviewValueDescriptor} on the item spec. When present, this renders a
|
|
587
|
+
* typed control (an enum `<select>`, a date/number input, a true/false select)
|
|
588
|
+
* and a validation-error slot the mount handler populates before "Use
|
|
589
|
+
* proposed"; when absent, it renders the plain text input, matching the
|
|
590
|
+
* approved mockup.
|
|
586
591
|
*/
|
|
587
592
|
function renderProposedValueEditor(item, proposed, targetLabel) {
|
|
593
|
+
const descriptor = item.spec.valueDescriptor;
|
|
588
594
|
const valueText = formatValue(proposed.value);
|
|
589
|
-
|
|
590
|
-
<div class="editrow">
|
|
591
|
-
<input
|
|
592
|
-
type="text"
|
|
595
|
+
const commonAttrs = `
|
|
593
596
|
class="proposed-value-input"
|
|
594
597
|
data-testid="edit-proposed-value"
|
|
595
598
|
data-item-name="${escapeHtml(item.metadata.name)}"
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
599
|
+
aria-label="Edit proposed ${escapeHtml(targetLabel)}"`;
|
|
600
|
+
const control = renderTypedProposedControl(descriptor, valueText, commonAttrs);
|
|
601
|
+
return `
|
|
602
|
+
<div class="editrow">
|
|
603
|
+
${control}
|
|
604
|
+
<span class="ehint">${escapeHtml(proposedEditorHint(descriptor))}</span>
|
|
600
605
|
</div>
|
|
606
|
+
<span class="verr" data-testid="value-error" role="alert" hidden></span>
|
|
601
607
|
`;
|
|
602
608
|
}
|
|
609
|
+
/**
|
|
610
|
+
* Picks the concrete input control for the proposed-value editor from the
|
|
611
|
+
* neutral value-type descriptor. All variants carry the SAME
|
|
612
|
+
* `data-testid="edit-proposed-value"` and `.value` semantics so the mount
|
|
613
|
+
* handler reads a reviewer's edit uniformly (both `<input>` and `<select>`
|
|
614
|
+
* expose `.value`). Falls back to a plain text input when there is no
|
|
615
|
+
* descriptor, an enum without a declared set, or a free-form type
|
|
616
|
+
* (string/array/object).
|
|
617
|
+
*/
|
|
618
|
+
function renderTypedProposedControl(descriptor, valueText, commonAttrs) {
|
|
619
|
+
switch (descriptor?.type) {
|
|
620
|
+
case "enum": {
|
|
621
|
+
const allowed = descriptor.enumValues ?? [];
|
|
622
|
+
if (allowed.length === 0)
|
|
623
|
+
break;
|
|
624
|
+
// Keep an out-of-set current value selectable rather than silently
|
|
625
|
+
// rewriting it — the reviewer sees exactly what was proposed.
|
|
626
|
+
const leadingOption = allowed.includes(valueText)
|
|
627
|
+
? ""
|
|
628
|
+
: `<option value="${escapeHtml(valueText)}" selected>${escapeHtml(valueText === "" ? "(unset)" : valueText)}</option>`;
|
|
629
|
+
const options = allowed
|
|
630
|
+
.map((opt) => `<option value="${escapeHtml(opt)}"${opt === valueText ? " selected" : ""}>${escapeHtml(opt)}</option>`)
|
|
631
|
+
.join("");
|
|
632
|
+
return `<select${commonAttrs}>${leadingOption}${options}</select>`;
|
|
633
|
+
}
|
|
634
|
+
case "boolean": {
|
|
635
|
+
const options = ["true", "false"]
|
|
636
|
+
.map((opt) => `<option value="${opt}"${opt === valueText ? " selected" : ""}>${opt}</option>`)
|
|
637
|
+
.join("");
|
|
638
|
+
return `<select${commonAttrs}>${options}</select>`;
|
|
639
|
+
}
|
|
640
|
+
case "date":
|
|
641
|
+
return `<input type="date"${commonAttrs} value="${escapeHtml(toDateInputValue(valueText))}">`;
|
|
642
|
+
case "number":
|
|
643
|
+
return `<input type="number" inputmode="decimal"${commonAttrs} value="${escapeHtml(valueText)}">`;
|
|
644
|
+
default:
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
return `<input type="text"${commonAttrs} value="${escapeHtml(valueText)}">`;
|
|
648
|
+
}
|
|
649
|
+
/** The trailing editor hint, keyed to the declared value type. */
|
|
650
|
+
function proposedEditorHint(descriptor) {
|
|
651
|
+
switch (descriptor?.type) {
|
|
652
|
+
case "enum":
|
|
653
|
+
return "choose one";
|
|
654
|
+
case "boolean":
|
|
655
|
+
return "true / false";
|
|
656
|
+
case "date":
|
|
657
|
+
return "date";
|
|
658
|
+
case "number":
|
|
659
|
+
return "number";
|
|
660
|
+
default:
|
|
661
|
+
return "editable";
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/** Normalizes an arbitrary value string to the `YYYY-MM-DD` an `<input type="date">` accepts. */
|
|
665
|
+
function toDateInputValue(text) {
|
|
666
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text))
|
|
667
|
+
return text;
|
|
668
|
+
const parsed = Date.parse(text);
|
|
669
|
+
return Number.isNaN(parsed) ? "" : new Date(parsed).toISOString().slice(0, 10);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Validates a reviewer's raw (string) edit of a proposed value against the
|
|
673
|
+
* field's neutral {@link ReviewValueDescriptor}, returning a human-readable
|
|
674
|
+
* error message when the value violates the declared type/enum constraint, or
|
|
675
|
+
* `undefined` when it is acceptable — including when there is no descriptor or
|
|
676
|
+
* the type carries no single-line constraint (string/array/object). This is a
|
|
677
|
+
* FORMAT check only: it never coerces or rewrites the value (the workbench
|
|
678
|
+
* stores the reviewer's string edit unchanged, as it did before typed editors).
|
|
679
|
+
*/
|
|
680
|
+
export function validateProposedValue(descriptor, rawValue) {
|
|
681
|
+
if (!descriptor)
|
|
682
|
+
return undefined;
|
|
683
|
+
const value = rawValue.trim();
|
|
684
|
+
switch (descriptor.type) {
|
|
685
|
+
case "number":
|
|
686
|
+
if (value === "")
|
|
687
|
+
return "Enter a number.";
|
|
688
|
+
return Number.isFinite(Number(value)) ? undefined : `"${rawValue}" is not a number.`;
|
|
689
|
+
case "boolean":
|
|
690
|
+
if (value === "")
|
|
691
|
+
return "Choose true or false.";
|
|
692
|
+
return value === "true" || value === "false" ? undefined : `"${rawValue}" is not true or false.`;
|
|
693
|
+
case "date":
|
|
694
|
+
if (value === "")
|
|
695
|
+
return "Enter a date.";
|
|
696
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value))
|
|
697
|
+
? undefined
|
|
698
|
+
: `"${rawValue}" is not a valid date (YYYY-MM-DD).`;
|
|
699
|
+
case "enum": {
|
|
700
|
+
const allowed = descriptor.enumValues ?? [];
|
|
701
|
+
if (allowed.length === 0)
|
|
702
|
+
return undefined; // nothing declared to enforce
|
|
703
|
+
return allowed.includes(value) ? undefined : `Choose one of: ${allowed.join(", ")}.`;
|
|
704
|
+
}
|
|
705
|
+
default:
|
|
706
|
+
return undefined;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
603
709
|
function renderProvenanceRow(item, proposed, presentationAdapter) {
|
|
604
710
|
const excerpt = proposed.locator?.excerpt;
|
|
605
711
|
if (!excerpt) {
|
|
@@ -862,6 +968,16 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
862
968
|
const candidate = item && decision ? candidateForDecision(item, decision) : undefined;
|
|
863
969
|
const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
|
|
864
970
|
const note = itemName ? session.notesByItemName[itemName] : undefined;
|
|
971
|
+
// Carry the reviewer's inline edit in the event (accept-proposed only), so
|
|
972
|
+
// it survives replay and the server apply boundary derives the edited
|
|
973
|
+
// effectiveValue from snapshot + events alone. setDecision updates the
|
|
974
|
+
// session before this runs, so the edit is already in state.
|
|
975
|
+
const editedValue = itemName && decision === "accept-proposed" ? session.editedValuesByItemName?.[itemName] : undefined;
|
|
976
|
+
const defaultData = decision
|
|
977
|
+
? (editedValue !== undefined
|
|
978
|
+
? { workbenchDecision: decision, workbenchEditedValue: editedValue }
|
|
979
|
+
: { workbenchDecision: decision })
|
|
980
|
+
: undefined;
|
|
865
981
|
events = [
|
|
866
982
|
...events,
|
|
867
983
|
buildReviewSessionEvent(session, {
|
|
@@ -874,7 +990,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
874
990
|
candidateId: candidate?.id,
|
|
875
991
|
status: definition?.status,
|
|
876
992
|
rationale: note,
|
|
877
|
-
data: dataOverride ??
|
|
993
|
+
data: dataOverride ?? defaultData,
|
|
878
994
|
}),
|
|
879
995
|
];
|
|
880
996
|
};
|
|
@@ -976,6 +1092,25 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
976
1092
|
const itemName = button.dataset.itemName ?? "";
|
|
977
1093
|
const field = button.closest("[data-testid='review-field']");
|
|
978
1094
|
const input = field?.querySelector("[data-testid='edit-proposed-value']");
|
|
1095
|
+
const descriptor = controller
|
|
1096
|
+
.currentSession()
|
|
1097
|
+
.items.find((entry) => entry.metadata.name === itemName)?.spec.valueDescriptor;
|
|
1098
|
+
const error = validateProposedValue(descriptor, input?.value ?? "");
|
|
1099
|
+
const errorEl = field?.querySelector("[data-testid='value-error']");
|
|
1100
|
+
if (error) {
|
|
1101
|
+
// Block the decision and surface the reason inline; the transient DOM
|
|
1102
|
+
// message survives until the next interaction (no re-render on reject).
|
|
1103
|
+
if (errorEl) {
|
|
1104
|
+
errorEl.textContent = error;
|
|
1105
|
+
errorEl.hidden = false;
|
|
1106
|
+
}
|
|
1107
|
+
input?.focus?.();
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
if (errorEl) {
|
|
1111
|
+
errorEl.textContent = "";
|
|
1112
|
+
errorEl.hidden = true;
|
|
1113
|
+
}
|
|
979
1114
|
controller.setDecision(itemName, "accept-proposed", input?.value);
|
|
980
1115
|
controller.renderCurrentState();
|
|
981
1116
|
});
|
|
@@ -404,7 +404,8 @@ h1, h2, h3, p {
|
|
|
404
404
|
margin-top: 8px;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
.editrow input
|
|
407
|
+
.editrow input,
|
|
408
|
+
.editrow select {
|
|
408
409
|
flex: 1;
|
|
409
410
|
min-width: 0;
|
|
410
411
|
font-size: 13.5px;
|
|
@@ -415,7 +416,8 @@ h1, h2, h3, p {
|
|
|
415
416
|
padding: 6px 9px;
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
.editrow input:focus-visible
|
|
419
|
+
.editrow input:focus-visible,
|
|
420
|
+
.editrow select:focus-visible {
|
|
419
421
|
outline: 2px solid var(--k-brand);
|
|
420
422
|
outline-offset: 1px;
|
|
421
423
|
border-color: var(--k-brand);
|
|
@@ -427,6 +429,20 @@ h1, h2, h3, p {
|
|
|
427
429
|
white-space: nowrap;
|
|
428
430
|
}
|
|
429
431
|
|
|
432
|
+
/* Validation message for a typed proposed-value edit (hidden until the mount
|
|
433
|
+
handler blocks a "Use proposed" on an out-of-shape value). */
|
|
434
|
+
.verr {
|
|
435
|
+
display: block;
|
|
436
|
+
margin-top: 5px;
|
|
437
|
+
font-size: 11.5px;
|
|
438
|
+
font-weight: 600;
|
|
439
|
+
color: var(--k-negative);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.verr[hidden] {
|
|
443
|
+
display: none;
|
|
444
|
+
}
|
|
445
|
+
|
|
430
446
|
/* confidence + provenance */
|
|
431
447
|
|
|
432
448
|
.prov {
|
package/package.json
CHANGED