@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.64",
3
+ "version": "0.4.65",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -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
- const [verdict, setVerdict] = useState(() => fieldStr(record.data, cfg.verdictAttr));
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 = verdict;
78
- setVerdict(d.verdict ?? ''); // optimistic
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
- setVerdict(prev); // roll back
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 = !!d.verdict && d.verdict === verdict;
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',