@semiont/react-ui 0.2.43 → 0.2.46
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/index.d.mts +50 -42
- package/dist/index.mjs +710 -663
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/resource/ResourceViewer.tsx +2 -2
- package/src/components/resource/panels/AssessmentPanel.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.tsx +2 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.tsx +1 -1
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +3 -3
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +4 -4
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +11 -11
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +2 -2
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +40 -1
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +7 -2
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import { BrowseView } from './BrowseView';
|
|
|
7
7
|
import { PopupContainer } from '../annotation-popups/SharedPopupElements';
|
|
8
8
|
import { JsonLdView } from '../annotation-popups/JsonLdView';
|
|
9
9
|
import type { components } from '@semiont/core';
|
|
10
|
-
import { resourceUri } from '@semiont/core';
|
|
10
|
+
import { resourceUri, annotationId as toAnnotationId } from '@semiont/core';
|
|
11
11
|
import { getExactText, getTargetSelector, isHighlight, isAssessment, isReference, isComment, isTag, getBodySource } from '@semiont/api-client';
|
|
12
12
|
import { useEventBus } from '../../contexts/EventBusContext';
|
|
13
13
|
import { useEventSubscriptions } from '../../contexts/useEventSubscription';
|
|
@@ -251,7 +251,7 @@ export function ResourceViewer({
|
|
|
251
251
|
|
|
252
252
|
// Handle deleting annotations - emit event instead of direct call
|
|
253
253
|
const handleDeleteAnnotation = useCallback((id: string) => {
|
|
254
|
-
eventBus.get('mark:delete').next({ annotationId: id });
|
|
254
|
+
eventBus.get('mark:delete').next({ annotationId: toAnnotationId(id) });
|
|
255
255
|
}, []); // eventBus is stable
|
|
256
256
|
|
|
257
257
|
// Handle annotation clicks - memoized
|
|
@@ -141,7 +141,7 @@ export function AssessmentPanel({
|
|
|
141
141
|
? [{ type: 'TextualBody' as const, value: newAssessmentText, purpose: 'assessing' as const }]
|
|
142
142
|
: [];
|
|
143
143
|
|
|
144
|
-
eventBus.get('mark:
|
|
144
|
+
eventBus.get('mark:submit').next({
|
|
145
145
|
motivation: 'assessing',
|
|
146
146
|
selector: pendingAnnotation.selector,
|
|
147
147
|
body,
|
|
@@ -167,7 +167,7 @@ export function CommentsPanel({
|
|
|
167
167
|
|
|
168
168
|
const handleSaveNewComment = () => {
|
|
169
169
|
if (newCommentText.trim() && pendingAnnotation) {
|
|
170
|
-
eventBus.get('mark:
|
|
170
|
+
eventBus.get('mark:submit').next({
|
|
171
171
|
motivation: 'commenting',
|
|
172
172
|
selector: pendingAnnotation.selector,
|
|
173
173
|
body: [{ type: 'TextualBody', value: newCommentText, purpose: 'commenting' }],
|
|
@@ -129,10 +129,10 @@ export function HighlightPanel({
|
|
|
129
129
|
});
|
|
130
130
|
|
|
131
131
|
// Highlights auto-create: when pendingAnnotation arrives with highlighting motivation,
|
|
132
|
-
// immediately emit mark:
|
|
132
|
+
// immediately emit mark:submit event
|
|
133
133
|
useEffect(() => {
|
|
134
134
|
if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
|
|
135
|
-
eventBus.get('mark:
|
|
135
|
+
eventBus.get('mark:submit').next({
|
|
136
136
|
motivation: 'highlighting',
|
|
137
137
|
selector: pendingAnnotation.selector,
|
|
138
138
|
body: [],
|
|
@@ -245,7 +245,7 @@ export function ReferencesPanel({
|
|
|
245
245
|
const handleCreateReference = () => {
|
|
246
246
|
if (pendingAnnotation) {
|
|
247
247
|
const entityType = pendingEntityTypes.join(',') || undefined;
|
|
248
|
-
eventBus.get('mark:
|
|
248
|
+
eventBus.get('mark:submit').next({
|
|
249
249
|
motivation: 'linking',
|
|
250
250
|
selector: pendingAnnotation.selector,
|
|
251
251
|
body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
|
|
@@ -274,7 +274,7 @@ export function TaggingPanel({
|
|
|
274
274
|
className="semiont-select"
|
|
275
275
|
onChange={(e) => {
|
|
276
276
|
if (e.target.value && pendingAnnotation) {
|
|
277
|
-
eventBus.get('mark:
|
|
277
|
+
eventBus.get('mark:submit').next({
|
|
278
278
|
motivation: 'tagging',
|
|
279
279
|
selector: pendingAnnotation.selector,
|
|
280
280
|
body: [
|
|
@@ -29,7 +29,7 @@ function createEventTracker() {
|
|
|
29
29
|
events.push({ event: eventName, payload });
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const panelEvents = ['mark:
|
|
32
|
+
const panelEvents = ['mark:submit'] as const;
|
|
33
33
|
|
|
34
34
|
panelEvents.forEach(eventName => {
|
|
35
35
|
const handler = trackEvent(eventName);
|
|
@@ -359,7 +359,7 @@ describe('AssessmentPanel Component', () => {
|
|
|
359
359
|
expect(textarea).toHaveFocus();
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
-
it('should emit mark:
|
|
362
|
+
it('should emit mark:submitevent when save is clicked', async () => {
|
|
363
363
|
const tracker = createEventTracker();
|
|
364
364
|
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
365
365
|
|
|
@@ -379,7 +379,7 @@ describe('AssessmentPanel Component', () => {
|
|
|
379
379
|
|
|
380
380
|
await waitFor(() => {
|
|
381
381
|
expect(tracker.events.some(e =>
|
|
382
|
-
e.event === 'mark:
|
|
382
|
+
e.event === 'mark:submit' &&
|
|
383
383
|
e.payload?.motivation === 'assessing' &&
|
|
384
384
|
e.payload?.body?.[0]?.value === 'My assessment'
|
|
385
385
|
)).toBe(true);
|
|
@@ -420,7 +420,7 @@ describe('AssessmentPanel Component', () => {
|
|
|
420
420
|
|
|
421
421
|
await waitFor(() => {
|
|
422
422
|
expect(tracker.events.some(e =>
|
|
423
|
-
e.event === 'mark:
|
|
423
|
+
e.event === 'mark:submit' &&
|
|
424
424
|
e.payload?.motivation === 'assessing' &&
|
|
425
425
|
Array.isArray(e.payload?.body) &&
|
|
426
426
|
e.payload.body.length === 0
|
|
@@ -29,7 +29,7 @@ function createEventTracker() {
|
|
|
29
29
|
events.push({ event: eventName, payload });
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const panelEvents = ['mark:
|
|
32
|
+
const panelEvents = ['mark:submit'] as const;
|
|
33
33
|
|
|
34
34
|
panelEvents.forEach(eventName => {
|
|
35
35
|
const handler = trackEvent(eventName);
|
|
@@ -396,7 +396,7 @@ describe('CommentsPanel Component', () => {
|
|
|
396
396
|
expect(textarea).toHaveFocus();
|
|
397
397
|
});
|
|
398
398
|
|
|
399
|
-
it('should emit mark:
|
|
399
|
+
it('should emit mark:submitevent when save is clicked', async () => {
|
|
400
400
|
const tracker = createEventTracker();
|
|
401
401
|
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
402
402
|
|
|
@@ -416,7 +416,7 @@ describe('CommentsPanel Component', () => {
|
|
|
416
416
|
|
|
417
417
|
await waitFor(() => {
|
|
418
418
|
expect(tracker.events.some(e =>
|
|
419
|
-
e.event === 'mark:
|
|
419
|
+
e.event === 'mark:submit' &&
|
|
420
420
|
e.payload?.motivation === 'commenting' &&
|
|
421
421
|
e.payload?.body?.[0]?.value === 'My new comment'
|
|
422
422
|
)).toBe(true);
|
|
@@ -29,7 +29,7 @@ function createEventTracker() {
|
|
|
29
29
|
events.push({ event: eventName, payload });
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
const panelEvents = ['mark:
|
|
32
|
+
const panelEvents = ['mark:submit', 'mark:assist-request'] as const;
|
|
33
33
|
|
|
34
34
|
panelEvents.forEach(eventName => {
|
|
35
35
|
const handler = trackEvent(eventName);
|
|
@@ -354,7 +354,7 @@ describe('TaggingPanel Component', () => {
|
|
|
354
354
|
expect(screen.getByText(/Select category/)).toBeInTheDocument();
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
-
it('should emit mark:
|
|
357
|
+
it('should emit mark:submitevent when category is selected', async () => {
|
|
358
358
|
const tracker = createEventTracker();
|
|
359
359
|
const pendingAnnotation = createPendingAnnotation('Selected text');
|
|
360
360
|
|
|
@@ -378,7 +378,7 @@ describe('TaggingPanel Component', () => {
|
|
|
378
378
|
|
|
379
379
|
await waitFor(() => {
|
|
380
380
|
expect(tracker.events.some(e =>
|
|
381
|
-
e.event === 'mark:
|
|
381
|
+
e.event === 'mark:submit' &&
|
|
382
382
|
e.payload?.motivation === 'tagging' &&
|
|
383
383
|
e.payload?.body?.[0]?.value === 'Issue' &&
|
|
384
384
|
e.payload?.body?.[0]?.type === 'TextualBody'
|
|
@@ -407,7 +407,7 @@ describe('TaggingPanel Component', () => {
|
|
|
407
407
|
await userEvent.selectOptions(categorySelect!, 'Rule');
|
|
408
408
|
|
|
409
409
|
await waitFor(() => {
|
|
410
|
-
const createEvent = tracker.events.find(e => e.event === 'mark:
|
|
410
|
+
const createEvent = tracker.events.find(e => e.event === 'mark:submit');
|
|
411
411
|
expect(createEvent).toBeDefined();
|
|
412
412
|
const body: any[] = createEvent!.payload.body;
|
|
413
413
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Regression test: pendingAnnotation cleared after mark:
|
|
2
|
+
* Regression test: pendingAnnotation cleared after mark:submit succeeds
|
|
3
3
|
*
|
|
4
4
|
* Bug: handleAnnotationCreate in useMarkFlow called the API and emitted
|
|
5
5
|
* mark:created, but never called setPendingAnnotation(null). The pending
|
|
@@ -110,7 +110,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
110
110
|
resetEventBusForTesting();
|
|
111
111
|
createAnnotationSpy = vi
|
|
112
112
|
.spyOn(SemiontApiClient.prototype, 'createAnnotation')
|
|
113
|
-
.mockResolvedValue({
|
|
113
|
+
.mockResolvedValue({ annotationId: MOCK_ANNOTATION.id } as any);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
afterEach(() => {
|
|
@@ -129,9 +129,9 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
129
129
|
expect(screen.getByTestId('pending-motivation')).toHaveTextContent('linking');
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
// Emit mark:
|
|
132
|
+
// Emit mark:submit(what ReferencesPanel does when user clicks "Create Reference")
|
|
133
133
|
await act(async () => {
|
|
134
|
-
emit('mark:
|
|
134
|
+
emit('mark:submit', {
|
|
135
135
|
motivation: 'linking',
|
|
136
136
|
selector: TEXT_SELECTOR,
|
|
137
137
|
body: [{ type: 'TextualBody', value: 'Person', purpose: 'tagging' }],
|
|
@@ -158,7 +158,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
158
158
|
});
|
|
159
159
|
|
|
160
160
|
await act(async () => {
|
|
161
|
-
emit('mark:
|
|
161
|
+
emit('mark:submit', {
|
|
162
162
|
motivation: 'assessing',
|
|
163
163
|
selector: SVG_SELECTOR,
|
|
164
164
|
body: [{ type: 'TextualBody', value: 'Looks good', purpose: 'assessing' }],
|
|
@@ -183,7 +183,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
183
183
|
|
|
184
184
|
// Empty body is valid for assessments
|
|
185
185
|
await act(async () => {
|
|
186
|
-
emit('mark:
|
|
186
|
+
emit('mark:submit', {
|
|
187
187
|
motivation: 'assessing',
|
|
188
188
|
selector: SVG_SELECTOR,
|
|
189
189
|
body: [],
|
|
@@ -207,7 +207,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
207
207
|
});
|
|
208
208
|
|
|
209
209
|
await act(async () => {
|
|
210
|
-
emit('mark:
|
|
210
|
+
emit('mark:submit', {
|
|
211
211
|
motivation: 'commenting',
|
|
212
212
|
selector: TEXT_SELECTOR,
|
|
213
213
|
body: [{ type: 'TextualBody', value: 'Great point', purpose: 'commenting' }],
|
|
@@ -231,7 +231,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
231
231
|
});
|
|
232
232
|
|
|
233
233
|
await act(async () => {
|
|
234
|
-
emit('mark:
|
|
234
|
+
emit('mark:submit', {
|
|
235
235
|
motivation: 'tagging',
|
|
236
236
|
selector: SVG_SELECTOR,
|
|
237
237
|
body: [{ type: 'TextualBody', value: 'concept:trust', purpose: 'tagging' }],
|
|
@@ -256,7 +256,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
await act(async () => {
|
|
259
|
-
emit('mark:
|
|
259
|
+
emit('mark:submit', {
|
|
260
260
|
motivation: 'linking',
|
|
261
261
|
selector: TEXT_SELECTOR,
|
|
262
262
|
body: [],
|
|
@@ -265,7 +265,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
265
265
|
|
|
266
266
|
await waitFor(() => {
|
|
267
267
|
expect(createdListener).toHaveBeenCalledTimes(1);
|
|
268
|
-
expect(createdListener).toHaveBeenCalledWith({
|
|
268
|
+
expect(createdListener).toHaveBeenCalledWith({ annotationId: 'new-1' });
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
subscription.unsubscribe();
|
|
@@ -285,7 +285,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
|
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
await act(async () => {
|
|
288
|
-
emit('mark:
|
|
288
|
+
emit('mark:submit', {
|
|
289
289
|
motivation: 'linking',
|
|
290
290
|
selector: TEXT_SELECTOR,
|
|
291
291
|
body: [],
|
|
@@ -41,7 +41,7 @@ vi.mock('../../../components/Toast', () => ({
|
|
|
41
41
|
import { ApiClientProvider } from '../../../contexts/ApiClientContext';
|
|
42
42
|
import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
|
|
43
43
|
import { SemiontApiClient } from '@semiont/api-client';
|
|
44
|
-
import { resourceUri, accessToken } from '@semiont/core';
|
|
44
|
+
import { resourceUri, accessToken, annotationId as toAnnotationId } from '@semiont/core';
|
|
45
45
|
|
|
46
46
|
describe('Annotation Deletion - Feature Integration', () => {
|
|
47
47
|
let deleteAnnotationSpy: ReturnType<typeof vi.fn>;
|
|
@@ -89,7 +89,7 @@ describe('Annotation Deletion - Feature Integration', () => {
|
|
|
89
89
|
return {
|
|
90
90
|
emitDelete: (annotationId: string) => {
|
|
91
91
|
act(() => {
|
|
92
|
-
eventBusInstance!.get('mark:delete').next({ annotationId });
|
|
92
|
+
eventBusInstance!.get('mark:delete').next({ annotationId: toAnnotationId(annotationId) });
|
|
93
93
|
});
|
|
94
94
|
},
|
|
95
95
|
eventBus: eventBusInstance!,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
-
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import { render, screen, act } from '@testing-library/react';
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { ResourceViewerPage } from '../components/ResourceViewerPage';
|
|
12
12
|
import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
|
|
@@ -118,6 +118,13 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
|
|
|
118
118
|
ResourceAnnotationsProvider: ({ children }: any) => children,
|
|
119
119
|
}));
|
|
120
120
|
|
|
121
|
+
// Mock useEventSubscription at the direct path used by ResourceViewerPage
|
|
122
|
+
// (the barrel export mock doesn't intercept direct context imports)
|
|
123
|
+
const mockUseEventSubscriptions = vi.fn();
|
|
124
|
+
vi.mock('../../../contexts/useEventSubscription', () => ({
|
|
125
|
+
useEventSubscriptions: (...args: unknown[]) => mockUseEventSubscriptions(...args),
|
|
126
|
+
}));
|
|
127
|
+
|
|
121
128
|
vi.mock('@/components/toolbar/ToolbarPanels', () => ({
|
|
122
129
|
ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
|
|
123
130
|
}));
|
|
@@ -281,6 +288,38 @@ describe('ResourceViewerPage', () => {
|
|
|
281
288
|
// Archived badge only shows in annotate mode, which defaults to false
|
|
282
289
|
expect(screen.queryByText('📦 Archived')).not.toBeInTheDocument();
|
|
283
290
|
});
|
|
291
|
+
|
|
292
|
+
it('shows archived badge after mark:mode-toggled event fires', () => {
|
|
293
|
+
localStorage.setItem('annotateMode', 'false');
|
|
294
|
+
localStorage.setItem('activeToolbarPanel', 'annotations');
|
|
295
|
+
|
|
296
|
+
const props = createMockProps({
|
|
297
|
+
resource: {
|
|
298
|
+
...createMockProps().resource,
|
|
299
|
+
archived: true,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
renderWithProviders(<ResourceViewerPage {...props} />);
|
|
304
|
+
|
|
305
|
+
// Before toggle: annotateMode is false, so archived badge is hidden
|
|
306
|
+
expect(screen.queryByText('📦 Archived')).not.toBeInTheDocument();
|
|
307
|
+
|
|
308
|
+
// Get the handler map that ResourceViewerPage passed to useEventSubscriptions
|
|
309
|
+
const handlerMap = mockUseEventSubscriptions.mock.calls[mockUseEventSubscriptions.mock.calls.length - 1]?.[0] as Record<string, () => void>;
|
|
310
|
+
expect(handlerMap).toBeDefined();
|
|
311
|
+
expect(handlerMap['mark:mode-toggled']).toBeDefined();
|
|
312
|
+
|
|
313
|
+
// Fire the mode toggle — this is what the toolbar emits
|
|
314
|
+
act(() => {
|
|
315
|
+
handlerMap['mark:mode-toggled']();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// After toggle: annotateMode is true, so archived badge should appear
|
|
319
|
+
expect(screen.getByText('📦 Archived')).toBeInTheDocument();
|
|
320
|
+
|
|
321
|
+
localStorage.clear();
|
|
322
|
+
});
|
|
284
323
|
});
|
|
285
324
|
|
|
286
325
|
describe('Modals', () => {
|
|
@@ -361,8 +361,13 @@ export function ResourceViewerPage({
|
|
|
361
361
|
}
|
|
362
362
|
}, [routes.know]); // eventBus is stable singleton - never in deps
|
|
363
363
|
|
|
364
|
+
const handleModeToggled = useCallback(() => {
|
|
365
|
+
setAnnotateMode(prev => !prev);
|
|
366
|
+
}, []);
|
|
367
|
+
|
|
364
368
|
// Event bus subscriptions (combined into single useEventSubscriptions call to prevent hook ordering issues)
|
|
365
369
|
useEventSubscriptions({
|
|
370
|
+
'mark:mode-toggled': handleModeToggled,
|
|
366
371
|
'mark:archive': handleResourceArchive,
|
|
367
372
|
'mark:unarchive': handleResourceUnarchive,
|
|
368
373
|
'yield:clone': handleResourceClone,
|
|
@@ -406,8 +411,8 @@ export function ResourceViewerPage({
|
|
|
406
411
|
const primaryMediaType = primaryRep?.mediaType;
|
|
407
412
|
const primaryByteSize = primaryRep?.byteSize;
|
|
408
413
|
|
|
409
|
-
// Annotate mode state -
|
|
410
|
-
const [annotateMode,
|
|
414
|
+
// Annotate mode state - synced via mark:mode-toggled event from AnnotateToolbar
|
|
415
|
+
const [annotateMode, setAnnotateMode] = useState(() => {
|
|
411
416
|
if (typeof window !== 'undefined') {
|
|
412
417
|
return localStorage.getItem('annotateMode') === 'true';
|
|
413
418
|
}
|