@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@semiont/react-ui",
3
- "version": "0.2.43",
3
+ "version": "0.2.46",
4
4
  "description": "React components and hooks for Semiont",
5
5
  "main": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.mts",
@@ -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:create').next({
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:create').next({
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:create event
132
+ // immediately emit mark:submit event
133
133
  useEffect(() => {
134
134
  if (pendingAnnotation && pendingAnnotation.motivation === 'highlighting') {
135
- eventBus.get('mark:create').next({
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:create').next({
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:create').next({
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:create'] as const;
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:createevent when save is clicked', async () => {
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:create' &&
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:create' &&
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:create'] as const;
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:createevent when save is clicked', async () => {
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:create' &&
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:create', 'mark:assist-request'] as const;
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:createevent when category is selected', async () => {
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:create' &&
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:create');
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:createsucceeds
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({ annotation: MOCK_ANNOTATION } as any);
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:create(what ReferencesPanel does when user clicks "Create Reference")
132
+ // Emit mark:submit(what ReferencesPanel does when user clicks "Create Reference")
133
133
  await act(async () => {
134
- emit('mark:create', {
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:create', {
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:create', {
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:create', {
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:create', {
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:create', {
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({ annotation: MOCK_ANNOTATION });
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:create', {
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 - local UI state only
410
- const [annotateMode, _setAnnotateMode] = useState(() => {
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
  }