@startsimpli/ui 0.4.64 → 0.4.65
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/package.json
CHANGED
|
@@ -67,15 +67,26 @@ export function InlineReviewActions({ client, type, record, config, onSaved }: I
|
|
|
67
67
|
() => [...reviewDecisions(cfg)].sort((a, b) => ORDER.indexOf(a.key) - ORDER.indexOf(b.key)),
|
|
68
68
|
[cfg],
|
|
69
69
|
);
|
|
70
|
-
|
|
70
|
+
// Which decision the record currently reflects — by verdict when the type has
|
|
71
|
+
// one (topics), else by status (news: acceptable→approve, rejected→reject). The
|
|
72
|
+
// matching button shows active. Optimistically updated on click.
|
|
73
|
+
const initialKey = useMemo(() => {
|
|
74
|
+
const v = fieldStr(record.data, cfg.verdictAttr);
|
|
75
|
+
const s = fieldStr(record.data, cfg.statusName);
|
|
76
|
+
return (
|
|
77
|
+
decisions.find((d) => (d.verdict ? d.verdict === v : d.status ? d.status === s : false))?.key ??
|
|
78
|
+
null
|
|
79
|
+
);
|
|
80
|
+
}, [record.data, cfg, decisions]);
|
|
81
|
+
const [appliedKey, setAppliedKey] = useState<string | null>(initialKey);
|
|
71
82
|
const [busy, setBusy] = useState<string | null>(null);
|
|
72
83
|
const [noteOpen, setNoteOpen] = useState(false);
|
|
73
84
|
const [note, setNote] = useState('');
|
|
74
85
|
|
|
75
86
|
/** Apply a decision (status + verdict), optionally attaching a note. Optimistic. */
|
|
76
87
|
async function apply(d: ReviewDecision, noteText?: string) {
|
|
77
|
-
const prev =
|
|
78
|
-
|
|
88
|
+
const prev = appliedKey;
|
|
89
|
+
setAppliedKey(d.key); // optimistic
|
|
79
90
|
setBusy(d.key);
|
|
80
91
|
try {
|
|
81
92
|
const data = applyDecisionToData(record.data, d, cfg);
|
|
@@ -94,7 +105,7 @@ export function InlineReviewActions({ client, type, record, config, onSaved }: I
|
|
|
94
105
|
onSaved?.();
|
|
95
106
|
return true;
|
|
96
107
|
} catch (err) {
|
|
97
|
-
|
|
108
|
+
setAppliedKey(prev); // roll back
|
|
98
109
|
notify.error(err instanceof Error ? err.message : 'Could not save.');
|
|
99
110
|
return false;
|
|
100
111
|
} finally {
|
|
@@ -104,7 +115,7 @@ export function InlineReviewActions({ client, type, record, config, onSaved }: I
|
|
|
104
115
|
|
|
105
116
|
function iconButton(d: ReviewDecision, extraOnClick?: () => void) {
|
|
106
117
|
const Icon = ICON[d.tone];
|
|
107
|
-
const active =
|
|
118
|
+
const active = d.key === appliedKey;
|
|
108
119
|
return (
|
|
109
120
|
<button
|
|
110
121
|
type="button"
|
|
@@ -124,6 +124,21 @@ describe('reviewDecisions', () => {
|
|
|
124
124
|
expect(keys).not.toContain('reject');
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('omitNeedsWork + custom transitions gives a binary accept/reject (news curation)', () => {
|
|
128
|
+
const news = type([enumAttr('status', ['new', 'surfaced', 'acceptable', 'rejected'])]);
|
|
129
|
+
const d = reviewDecisions(
|
|
130
|
+
resolveReviewConfig(news, {
|
|
131
|
+
approveStatus: 'acceptable',
|
|
132
|
+
rejectStatus: 'rejected',
|
|
133
|
+
verdicts: [],
|
|
134
|
+
omitNeedsWork: true,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
expect(d.map((x) => x.key).sort()).toEqual(['approve', 'reject']);
|
|
138
|
+
expect(d.find((x) => x.key === 'approve')).toMatchObject({ status: 'acceptable', verdict: null });
|
|
139
|
+
expect(d.find((x) => x.key === 'reject')).toMatchObject({ status: 'rejected', verdict: null });
|
|
140
|
+
});
|
|
141
|
+
|
|
127
142
|
it('uses custom verdict vocab tones', () => {
|
|
128
143
|
const d = reviewDecisions(
|
|
129
144
|
resolveReviewConfig(topic, {
|
|
@@ -167,6 +167,8 @@ export interface ReviewConfig {
|
|
|
167
167
|
metaAttrs?: string[];
|
|
168
168
|
/** Attrs holding source urls/lines (default: source_1..3 + "sources"). */
|
|
169
169
|
sourceAttrs?: string[];
|
|
170
|
+
/** Drop the middle "Needs work" decision — a binary accept/reject flow (default false). */
|
|
171
|
+
omitNeedsWork?: boolean;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
export interface ResolvedReview {
|
|
@@ -182,6 +184,7 @@ export interface ResolvedReview {
|
|
|
182
184
|
rankAttr: string;
|
|
183
185
|
metaAttrs: string[];
|
|
184
186
|
sourceAttrs: string[];
|
|
187
|
+
omitNeedsWork: boolean;
|
|
185
188
|
}
|
|
186
189
|
|
|
187
190
|
const DEFAULT_SOURCE_ATTRS = ['source_1', 'source_2', 'source_3', 'sources'];
|
|
@@ -212,6 +215,7 @@ export function resolveReviewConfig(
|
|
|
212
215
|
metaAttrs: (config.metaAttrs ?? DEFAULT_META_ATTRS).filter((a) => declared.has(a)),
|
|
213
216
|
// Keep source attrs the caller asked for; default to the ones this type declares.
|
|
214
217
|
sourceAttrs: (config.sourceAttrs ?? DEFAULT_SOURCE_ATTRS).filter((a) => declared.has(a)),
|
|
218
|
+
omitNeedsWork: config.omitNeedsWork ?? false,
|
|
215
219
|
};
|
|
216
220
|
}
|
|
217
221
|
|
|
@@ -269,7 +273,7 @@ export function reviewDecisions(r: ResolvedReview): ReviewDecision[] {
|
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
const editVerdict = verdictOf('neutral');
|
|
272
|
-
if (entry && (editVerdict || entry !== r.transitions.approve)) {
|
|
276
|
+
if (!r.omitNeedsWork && entry && (editVerdict || entry !== r.transitions.approve)) {
|
|
273
277
|
out.push({
|
|
274
278
|
key: 'needs_work',
|
|
275
279
|
label: 'Needs work',
|