@startsimpli/ui 0.4.74 → 0.4.75
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.
|
|
3
|
+
"version": "0.4.75",
|
|
4
4
|
"description": "Shared UI components package for StartSimpli applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"next": "^14.0.0 || ^15.0.0 || ^16.0.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0",
|
|
44
44
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
|
-
"@startsimpli/
|
|
46
|
-
"@startsimpli/
|
|
45
|
+
"@startsimpli/api": "^0.5.46",
|
|
46
|
+
"@startsimpli/auth": "^0.4.31"
|
|
47
47
|
},
|
|
48
48
|
"peerDependenciesMeta": {
|
|
49
49
|
"@startsimpli/api": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { useMemo, useState } from 'react';
|
|
12
12
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
13
|
-
import { ShieldCheck, AlertTriangle, ExternalLink, CheckCircle2, Circle } from 'lucide-react';
|
|
13
|
+
import { ShieldCheck, AlertTriangle, ExternalLink, CheckCircle2, Circle, ChevronDown } from 'lucide-react';
|
|
14
14
|
|
|
15
15
|
import { Button } from '../ui/button';
|
|
16
16
|
import { Badge } from '../ui/badge';
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
wordTargets,
|
|
29
29
|
sourceOverlapCheck,
|
|
30
30
|
pickChanges,
|
|
31
|
+
isRejectedTopicGroup,
|
|
31
32
|
type DraftGroup,
|
|
32
33
|
type DraftSource,
|
|
33
34
|
type CheckStatus,
|
|
@@ -66,6 +67,22 @@ export function DraftReviewWorkspace({ client }: DraftReviewWorkspaceProps) {
|
|
|
66
67
|
});
|
|
67
68
|
const records = recordsQ.data ?? [];
|
|
68
69
|
|
|
70
|
+
// A story whose topic was rejected shouldn't keep cluttering the review — its
|
|
71
|
+
// drafts collapse into the "Rejected" section, mirroring the topic list
|
|
72
|
+
// (startsim-ay9l follow-up). draft.topic_ref is the topic's entity id.
|
|
73
|
+
const topicsQ = useQuery({
|
|
74
|
+
queryKey: ['entities', 'topic', 'all'],
|
|
75
|
+
queryFn: () => client.listAllEntities('topic'),
|
|
76
|
+
enabled: !!draftType,
|
|
77
|
+
});
|
|
78
|
+
const rejectedTopicIds = useMemo(() => {
|
|
79
|
+
const s = new Set<string>();
|
|
80
|
+
for (const t of topicsQ.data ?? []) {
|
|
81
|
+
if (String(readData(t.data, 'status') ?? '') === 'rejected') s.add(String(t.id));
|
|
82
|
+
}
|
|
83
|
+
return s;
|
|
84
|
+
}, [topicsQ.data]);
|
|
85
|
+
|
|
69
86
|
const sourcesQ = useQuery({
|
|
70
87
|
queryKey: ['entities', 'source', 'all'],
|
|
71
88
|
queryFn: () => client.listAllEntities('source'),
|
|
@@ -82,8 +99,17 @@ export function DraftReviewWorkspace({ client }: DraftReviewWorkspaceProps) {
|
|
|
82
99
|
}, [sourcesQ.data]);
|
|
83
100
|
|
|
84
101
|
const groups = useMemo(() => groupDrafts(records), [records]);
|
|
102
|
+
const { activeGroups, rejectedGroups } = useMemo(() => {
|
|
103
|
+
const act: DraftGroup[] = [];
|
|
104
|
+
const rej: DraftGroup[] = [];
|
|
105
|
+
for (const g of groups) {
|
|
106
|
+
(isRejectedTopicGroup(g, rejectedTopicIds) ? rej : act).push(g);
|
|
107
|
+
}
|
|
108
|
+
return { activeGroups: act, rejectedGroups: rej };
|
|
109
|
+
}, [groups, rejectedTopicIds]);
|
|
85
110
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
86
111
|
const [busyKey, setBusyKey] = useState<string | null>(null);
|
|
112
|
+
const [showRejected, setShowRejected] = useState(false);
|
|
87
113
|
|
|
88
114
|
async function writeEntity(record: EntityRecord, changes: Record<string, unknown>) {
|
|
89
115
|
const data: Record<string, unknown> = { ...record.data };
|
|
@@ -128,8 +154,22 @@ export function DraftReviewWorkspace({ client }: DraftReviewWorkspaceProps) {
|
|
|
128
154
|
return <p className="text-sm text-gray-500">No “draft” type is defined for this app.</p>;
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
const pickedCount =
|
|
132
|
-
const readyCount =
|
|
157
|
+
const pickedCount = activeGroups.filter((g) => g.chosen).length;
|
|
158
|
+
const readyCount = activeGroups.filter((g) => g.ready).length;
|
|
159
|
+
|
|
160
|
+
const renderGroup = (g: DraftGroup) => (
|
|
161
|
+
<StoryGroup
|
|
162
|
+
key={g.key}
|
|
163
|
+
group={g}
|
|
164
|
+
tierByDomain={tierByDomain}
|
|
165
|
+
openId={openId}
|
|
166
|
+
onToggleOpen={(id) => setOpenId((cur) => (cur === id ? null : id))}
|
|
167
|
+
onPick={pick}
|
|
168
|
+
onMarkReady={markReady}
|
|
169
|
+
busy={busyKey === g.key}
|
|
170
|
+
busyId={busyKey}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
133
173
|
|
|
134
174
|
return (
|
|
135
175
|
<div className="space-y-6">
|
|
@@ -142,30 +182,41 @@ export function DraftReviewWorkspace({ client }: DraftReviewWorkspaceProps) {
|
|
|
142
182
|
</p>
|
|
143
183
|
</div>
|
|
144
184
|
<span className="text-sm text-gray-500">
|
|
145
|
-
{
|
|
185
|
+
{activeGroups.length} stories · {pickedCount} picked · {readyCount} ready to post
|
|
146
186
|
</span>
|
|
147
187
|
</div>
|
|
148
188
|
|
|
149
|
-
{
|
|
189
|
+
{activeGroups.length === 0 && rejectedGroups.length === 0 ? (
|
|
150
190
|
<p className="rounded-lg border border-dashed border-gray-300 p-8 text-center text-sm text-gray-500">
|
|
151
191
|
No drafts yet. Move a topic to “ready” and the writer will generate candidates here.
|
|
152
192
|
</p>
|
|
153
193
|
) : (
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
<>
|
|
195
|
+
{activeGroups.length > 0 ? (
|
|
196
|
+
<ol className="space-y-4">{activeGroups.map(renderGroup)}</ol>
|
|
197
|
+
) : (
|
|
198
|
+
<p className="rounded-lg border border-dashed border-gray-300 p-6 text-center text-sm text-gray-500">
|
|
199
|
+
No active stories — the rest are rejected (below).
|
|
200
|
+
</p>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{rejectedGroups.length > 0 && (
|
|
204
|
+
<div className="rounded-lg border border-border">
|
|
205
|
+
<button
|
|
206
|
+
type="button"
|
|
207
|
+
onClick={() => setShowRejected((v) => !v)}
|
|
208
|
+
className="flex w-full items-center justify-between rounded-lg px-4 py-2.5 text-sm font-medium text-gray-500 hover:bg-gray-50"
|
|
209
|
+
aria-expanded={showRejected}
|
|
210
|
+
>
|
|
211
|
+
<span>Rejected ({rejectedGroups.length})</span>
|
|
212
|
+
<ChevronDown className={`h-4 w-4 transition-transform ${showRejected ? 'rotate-180' : ''}`} />
|
|
213
|
+
</button>
|
|
214
|
+
{showRejected && (
|
|
215
|
+
<ol className="space-y-4 border-t border-border p-4">{rejectedGroups.map(renderGroup)}</ol>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
</>
|
|
169
220
|
)}
|
|
170
221
|
</div>
|
|
171
222
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseSources, normUrl, groupKeyOf, groupDrafts, candidateTitle, gradeWords, wordTargets, sourceOverlapCheck, pickChanges } from '../drafts';
|
|
1
|
+
import { parseSources, normUrl, groupKeyOf, groupDrafts, candidateTitle, gradeWords, wordTargets, sourceOverlapCheck, pickChanges, isRejectedTopicGroup } from '../drafts';
|
|
2
2
|
import type { EntityRecord } from '../types';
|
|
3
3
|
|
|
4
4
|
function rec(id: string, name: string, data: Record<string, unknown>): EntityRecord {
|
|
@@ -134,3 +134,20 @@ describe('pickChanges (startsim-yvd3 — pick must not write an invalid status)'
|
|
|
134
134
|
expect(pickChanges(7 as unknown as string, 8 as unknown as string)).toEqual({ chosen: false });
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
describe('isRejectedTopicGroup (startsim-ay9l — collapse drafts of rejected topics)', () => {
|
|
139
|
+
const grp = (topicRef: string) => groupDrafts([
|
|
140
|
+
rec('d1', 'S', { candidate_index: 1, topic_ref: topicRef, topic_title: 'Story', sources: 'x (2026-01-01) https://a.com/1' }),
|
|
141
|
+
])[0];
|
|
142
|
+
|
|
143
|
+
it('is true when the group topic id is in the rejected set', () => {
|
|
144
|
+
expect(isRejectedTopicGroup(grp('topic-uuid-1'), new Set(['topic-uuid-1']))).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
it('is false when the topic is not rejected', () => {
|
|
147
|
+
expect(isRejectedTopicGroup(grp('topic-uuid-2'), new Set(['topic-uuid-1']))).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
it('is false when the group has no topic_ref', () => {
|
|
150
|
+
const g = groupDrafts([rec('d2', 'S2', { candidate_index: 1, sources: 'x (2026-01-01) https://b.com/1' })])[0];
|
|
151
|
+
expect(isRejectedTopicGroup(g, new Set(['']))).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -138,6 +138,16 @@ export function groupDrafts(records: EntityRecord[]): DraftGroup[] {
|
|
|
138
138
|
return groups.sort((a, b) => rank(a) - rank(b) || a.label.localeCompare(b.label));
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* True when a draft story-group belongs to a REJECTED topic — used to collapse
|
|
143
|
+
* those stories out of the active review (startsim-ay9l). A draft's `topic_ref`
|
|
144
|
+
* is the topic's entity id (String), so callers pass a set of rejected topic ids.
|
|
145
|
+
*/
|
|
146
|
+
export function isRejectedTopicGroup(group: DraftGroup, rejectedTopicIds: Set<string>): boolean {
|
|
147
|
+
const topicRef = str(readData(group.candidates[0]?.data, 'topic_ref'));
|
|
148
|
+
return topicRef !== '' && rejectedTopicIds.has(topicRef);
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
/** Word-count targets by content type, from the OGMC skill specs (generic defaults). */
|
|
142
152
|
export function wordTargets(contentType: string): { blog: [number, number]; linkedin: [number, number] } {
|
|
143
153
|
switch (contentType) {
|