@semiont/react-ui 0.5.11 → 0.5.13
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/{PdfAnnotationCanvas.client-KWQ7XDWT.js → PdfAnnotationCanvas.client-75GY2EDR.js} +12 -6
- package/dist/PdfAnnotationCanvas.client-75GY2EDR.js.map +1 -0
- package/dist/{chunk-O2MD7TGE.js → chunk-VXASXU4K.js} +10 -2
- package/dist/chunk-VXASXU4K.js.map +1 -0
- package/dist/index.d.ts +54 -16
- package/dist/index.js +395 -343
- package/dist/index.js.map +1 -1
- package/dist/test-utils.d.ts +5 -1
- package/dist/test-utils.js +3 -2
- package/dist/test-utils.js.map +1 -1
- package/package.json +5 -5
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -4
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +137 -0
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +108 -0
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +12 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +137 -1
- package/src/components/resource/BrowseView.tsx +14 -6
- package/src/components/resource/ResourceViewer.tsx +6 -4
- package/src/components/resource/__tests__/BrowseView.test.tsx +98 -31
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +40 -1
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +113 -0
- package/src/components/resource/browse-renderers.tsx +26 -4
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +8 -5
- package/src/components/resource/panels/AssistSection.tsx +4 -3
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +8 -5
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +8 -5
- package/src/components/resource/panels/ReferenceEntry.tsx +9 -10
- package/src/components/resource/panels/ReferencesPanel.tsx +11 -6
- package/src/components/resource/panels/ResourceInfoPanel.tsx +4 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -3
- package/src/components/resource/panels/TaggingPanel.tsx +7 -4
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +10 -0
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +28 -20
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +38 -28
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +46 -17
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +65 -57
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +53 -24
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +27 -17
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +23 -17
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +48 -77
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +87 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +15 -9
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +77 -59
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +11 -4
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +33 -19
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -7
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +3 -0
- package/dist/PdfAnnotationCanvas.client-KWQ7XDWT.js.map +0 -1
- package/dist/chunk-O2MD7TGE.js.map +0 -1
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
import type { Ref } from 'react';
|
|
4
4
|
import type { Annotation } from '@semiont/core';
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/core';
|
|
6
|
-
import {
|
|
7
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
7
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
9
8
|
import { renderAgentLabel } from './agent-label';
|
|
10
9
|
|
|
11
10
|
interface HighlightEntryProps {
|
|
11
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
12
|
+
session: SemiontSession | null;
|
|
12
13
|
highlight: Annotation;
|
|
13
14
|
isFocused: boolean;
|
|
14
15
|
isHovered?: boolean;
|
|
@@ -33,13 +34,13 @@ function formatRelativeTime(isoString: string): string {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export function HighlightEntry({
|
|
37
|
+
session,
|
|
36
38
|
highlight,
|
|
37
39
|
isFocused,
|
|
38
40
|
isHovered = false,
|
|
39
41
|
ref,
|
|
40
42
|
}: HighlightEntryProps) {
|
|
41
|
-
const
|
|
42
|
-
const hoverProps = useHoverEmitter(highlight.id);
|
|
43
|
+
const hoverProps = useHoverEmitter(session, highlight.id);
|
|
43
44
|
|
|
44
45
|
const selectedText = getAnnotationExactText(highlight);
|
|
45
46
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { components, Selector } from '@semiont/core';
|
|
9
8
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
10
9
|
import { HighlightEntry } from './HighlightEntry';
|
|
@@ -23,6 +22,8 @@ interface PendingAnnotation {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
interface HighlightPanelProps {
|
|
25
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
26
|
+
session: SemiontSession | null;
|
|
26
27
|
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
27
28
|
resourceId: string;
|
|
28
29
|
annotations: Annotation[];
|
|
@@ -44,6 +45,7 @@ interface HighlightPanelProps {
|
|
|
44
45
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
45
46
|
*/
|
|
46
47
|
export function HighlightPanel({
|
|
48
|
+
session,
|
|
47
49
|
resourceId,
|
|
48
50
|
annotations,
|
|
49
51
|
pendingAnnotation,
|
|
@@ -57,7 +59,6 @@ export function HighlightPanel({
|
|
|
57
59
|
}: HighlightPanelProps) {
|
|
58
60
|
|
|
59
61
|
const t = useTranslations('HighlightPanel');
|
|
60
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
61
62
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
62
63
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
63
64
|
|
|
@@ -128,7 +129,7 @@ export function HighlightPanel({
|
|
|
128
129
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
129
130
|
}, []);
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
useSessionEventSubscriptions(session, {
|
|
132
133
|
'browse:click': handleAnnotationClick,
|
|
133
134
|
});
|
|
134
135
|
|
|
@@ -155,6 +156,7 @@ export function HighlightPanel({
|
|
|
155
156
|
{/* Assist Section - only in Annotate mode and for text resources */}
|
|
156
157
|
{annotateMode && (
|
|
157
158
|
<AssistSection
|
|
159
|
+
session={session}
|
|
158
160
|
annotationType="highlight"
|
|
159
161
|
isAssisting={isAssisting}
|
|
160
162
|
progress={progress}
|
|
@@ -171,6 +173,7 @@ export function HighlightPanel({
|
|
|
171
173
|
) : (
|
|
172
174
|
sortedAnnotations.map((highlight) => (
|
|
173
175
|
<HighlightEntry
|
|
176
|
+
session={session}
|
|
174
177
|
key={highlight.id}
|
|
175
178
|
highlight={highlight}
|
|
176
179
|
isFocused={highlight.id === focusedAnnotationId}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { Ref } from 'react';
|
|
4
|
-
import type { RouteBuilder } from '../../../contexts/RoutingContext';
|
|
5
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
6
5
|
import type { Annotation } from '@semiont/core';
|
|
7
6
|
import { resourceId } from '@semiont/core';
|
|
8
7
|
import { getAnnotationExactText, isBodyResolved, getBodySource, getFragmentSelector, getSvgSelector, getTargetSelector } from '@semiont/core';
|
|
9
8
|
import { getEntityTypes } from '@semiont/ontology';
|
|
10
9
|
import { getResourceIcon } from '../../../lib/resource-utils';
|
|
11
|
-
import {
|
|
12
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
10
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
13
11
|
import { renderAgentLabel } from './agent-label';
|
|
14
|
-
import { useObservableExternalNavigation } from '../../../hooks/useObservableBrowse';
|
|
15
12
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
16
13
|
|
|
17
14
|
// Extended annotation type with runtime properties added by backend enrichment
|
|
@@ -21,29 +18,31 @@ interface EnrichedAnnotation extends Annotation {
|
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
interface ReferenceEntryProps {
|
|
21
|
+
/** Session for interaction routing (browse.click etc.); the panel threads it. */
|
|
22
|
+
session: SemiontSession | null;
|
|
24
23
|
reference: Annotation;
|
|
25
24
|
isFocused: boolean;
|
|
26
25
|
isHovered?: boolean;
|
|
27
|
-
|
|
26
|
+
/** Host-owned navigation: called with the resolved resource id when the entry's link opens. */
|
|
27
|
+
onOpenResource?: (resourceId: string) => void;
|
|
28
28
|
annotateMode?: boolean;
|
|
29
29
|
isGenerating?: boolean;
|
|
30
30
|
ref?: Ref<HTMLDivElement>;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function ReferenceEntry({
|
|
34
|
+
session,
|
|
34
35
|
reference,
|
|
35
36
|
isFocused,
|
|
36
37
|
isHovered = false,
|
|
37
|
-
|
|
38
|
+
onOpenResource,
|
|
38
39
|
annotateMode = true,
|
|
39
40
|
isGenerating = false,
|
|
40
41
|
ref,
|
|
41
42
|
}: ReferenceEntryProps) {
|
|
42
43
|
const t = useTranslations('ReferencesPanel');
|
|
43
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
44
44
|
const semiont = session?.client;
|
|
45
|
-
const
|
|
46
|
-
const hoverProps = useHoverEmitter(reference.id);
|
|
45
|
+
const hoverProps = useHoverEmitter(session, reference.id);
|
|
47
46
|
|
|
48
47
|
const selectedText = getAnnotationExactText(reference) || '';
|
|
49
48
|
const isResolved = isBodyResolved(reference.body);
|
|
@@ -65,7 +64,7 @@ export function ReferenceEntry({
|
|
|
65
64
|
const handleOpen = () => {
|
|
66
65
|
if (resolvedResourceUri) {
|
|
67
66
|
// resolvedResourceUri is already a bare resource ID
|
|
68
|
-
|
|
67
|
+
onOpenResource?.(resolvedResourceUri);
|
|
69
68
|
}
|
|
70
69
|
};
|
|
71
70
|
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
9
8
|
import { AnnotateReferencesProgressWidget } from '../../AnnotateReferencesProgressWidget';
|
|
10
9
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
@@ -43,6 +42,10 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
interface Props {
|
|
45
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
46
|
+
session: SemiontSession | null;
|
|
47
|
+
/** Host-owned navigation: called with the resolved resource id when a reference entry opens. */
|
|
48
|
+
onOpenResource?: (resourceId: string) => void;
|
|
46
49
|
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
47
50
|
resourceId: string;
|
|
48
51
|
// Generic panel props
|
|
@@ -78,6 +81,8 @@ interface Props {
|
|
|
78
81
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
79
82
|
*/
|
|
80
83
|
export function ReferencesPanel({
|
|
84
|
+
session,
|
|
85
|
+
onOpenResource,
|
|
81
86
|
resourceId,
|
|
82
87
|
annotations = [],
|
|
83
88
|
isAssisting,
|
|
@@ -97,7 +102,6 @@ export function ReferencesPanel({
|
|
|
97
102
|
sourceLanguage,
|
|
98
103
|
}: Props) {
|
|
99
104
|
const t = useTranslations('ReferencesPanel');
|
|
100
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
101
105
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
102
106
|
const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
|
|
103
107
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -206,7 +210,7 @@ export function ReferencesPanel({
|
|
|
206
210
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
207
211
|
}, []);
|
|
208
212
|
|
|
209
|
-
|
|
213
|
+
useSessionEventSubscriptions(session, {
|
|
210
214
|
'browse:click': handleAnnotationClick,
|
|
211
215
|
});
|
|
212
216
|
|
|
@@ -492,11 +496,12 @@ export function ReferencesPanel({
|
|
|
492
496
|
) : (
|
|
493
497
|
sortedAnnotations.map((reference) => (
|
|
494
498
|
<ReferenceEntry
|
|
499
|
+
session={session}
|
|
495
500
|
key={reference.id}
|
|
496
501
|
reference={reference}
|
|
497
502
|
isFocused={reference.id === focusedAnnotationId}
|
|
498
503
|
isHovered={reference.id === hoveredAnnotationId}
|
|
499
|
-
|
|
504
|
+
onOpenResource={onOpenResource}
|
|
500
505
|
annotateMode={annotateMode}
|
|
501
506
|
isGenerating={reference.id === generatingReferenceId}
|
|
502
507
|
ref={(el) => setEntryRef(reference.id, el)}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
4
|
-
import {
|
|
5
|
-
import { useObservable } from '../../../hooks/useObservable';
|
|
4
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
5
|
import { formatLocaleDisplay } from '@semiont/core';
|
|
7
6
|
import { resourceId as makeResourceId, type components } from '@semiont/core';
|
|
8
7
|
import { renderAgentLabel } from './agent-label';
|
|
@@ -11,6 +10,8 @@ import './ResourceInfoPanel.css';
|
|
|
11
10
|
type Agent = components['schemas']['Agent'];
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
13
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
14
|
+
session: SemiontSession | null;
|
|
14
15
|
resourceId: string;
|
|
15
16
|
documentEntityTypes: string[];
|
|
16
17
|
documentLocale?: string | undefined;
|
|
@@ -39,6 +40,7 @@ interface Props {
|
|
|
39
40
|
* @emits mark:archive - Archive this resource
|
|
40
41
|
*/
|
|
41
42
|
export function ResourceInfoPanel({
|
|
43
|
+
session,
|
|
42
44
|
resourceId,
|
|
43
45
|
documentEntityTypes,
|
|
44
46
|
documentLocale,
|
|
@@ -54,7 +56,6 @@ export function ResourceInfoPanel({
|
|
|
54
56
|
onGenerate,
|
|
55
57
|
}: Props) {
|
|
56
58
|
const t = useTranslations('ResourceInfoPanel');
|
|
57
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
58
59
|
|
|
59
60
|
// Single attribution surface. `wasAttributedTo` is the canonical list
|
|
60
61
|
// of responsible parties; if a producer set only `generator` we
|
|
@@ -5,12 +5,14 @@ import type { Ref } from 'react';
|
|
|
5
5
|
import type { Annotation } from '@semiont/core';
|
|
6
6
|
import { getAnnotationExactText } from '@semiont/core';
|
|
7
7
|
import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
|
|
8
|
-
import { useSemiont } from '../../../session/SemiontProvider';
|
|
9
8
|
import { useObservable } from '../../../hooks/useObservable';
|
|
9
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
10
10
|
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
11
11
|
import { renderAgentLabel } from './agent-label';
|
|
12
12
|
|
|
13
13
|
interface TagEntryProps {
|
|
14
|
+
/** Session for interaction routing (browse.click, tag schemas); the panel threads it. */
|
|
15
|
+
session: SemiontSession | null;
|
|
14
16
|
tag: Annotation;
|
|
15
17
|
isFocused: boolean;
|
|
16
18
|
isHovered?: boolean;
|
|
@@ -18,13 +20,13 @@ interface TagEntryProps {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function TagEntry({
|
|
23
|
+
session,
|
|
21
24
|
tag,
|
|
22
25
|
isFocused,
|
|
23
26
|
isHovered = false,
|
|
24
27
|
ref,
|
|
25
28
|
}: TagEntryProps) {
|
|
26
|
-
const
|
|
27
|
-
const hoverProps = useHoverEmitter(tag.id);
|
|
29
|
+
const hoverProps = useHoverEmitter(session, tag.id);
|
|
28
30
|
|
|
29
31
|
const selectedText = getAnnotationExactText(tag);
|
|
30
32
|
const category = getTagCategory(tag);
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import { useSemiont } from '../../../session/SemiontProvider';
|
|
6
5
|
import { useObservable } from '../../../hooks/useObservable';
|
|
7
|
-
import {
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
7
|
+
import { useSessionEventSubscriptions } from '../../../hooks/useSessionEventSubscriptions';
|
|
8
8
|
import type { components, Selector } from '@semiont/core';
|
|
9
9
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/core';
|
|
10
10
|
import { TagEntry } from './TagEntry';
|
|
@@ -39,6 +39,8 @@ function getSelectorDisplayText(selector: Selector | Selector[]): string | null
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
interface TaggingPanelProps {
|
|
42
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
43
|
+
session: SemiontSession | null;
|
|
42
44
|
/** The '@id' of the panel's resource — stamped as `source` on mark:submit (multi-viewer routing). */
|
|
43
45
|
resourceId: string;
|
|
44
46
|
annotations: Annotation[];
|
|
@@ -64,6 +66,7 @@ interface TaggingPanelProps {
|
|
|
64
66
|
* @subscribes browse:click - Annotation clicked. Payload: { annotationId: string }
|
|
65
67
|
*/
|
|
66
68
|
export function TaggingPanel({
|
|
69
|
+
session,
|
|
67
70
|
resourceId,
|
|
68
71
|
annotations,
|
|
69
72
|
annotateMode = true,
|
|
@@ -77,7 +80,6 @@ export function TaggingPanel({
|
|
|
77
80
|
sourceLanguage,
|
|
78
81
|
}: TaggingPanelProps) {
|
|
79
82
|
const t = useTranslations('TaggingPanel');
|
|
80
|
-
const session = useObservable(useSemiont().activeSession$);
|
|
81
83
|
|
|
82
84
|
// Subscribe to the per-KB tag-schema registry. Schemas are runtime-
|
|
83
85
|
// registered by the KB at session start (see frame.addTagSchema).
|
|
@@ -129,7 +131,7 @@ export function TaggingPanel({
|
|
|
129
131
|
setTimeout(() => setFocusedAnnotationId(null), 3000);
|
|
130
132
|
}, []);
|
|
131
133
|
|
|
132
|
-
|
|
134
|
+
useSessionEventSubscriptions(session, {
|
|
133
135
|
'browse:click': handleAnnotationClick,
|
|
134
136
|
});
|
|
135
137
|
|
|
@@ -528,6 +530,7 @@ export function TaggingPanel({
|
|
|
528
530
|
) : (
|
|
529
531
|
sortedAnnotations.map((tag) => (
|
|
530
532
|
<TagEntry
|
|
533
|
+
session={session}
|
|
531
534
|
key={tag.id}
|
|
532
535
|
tag={tag}
|
|
533
536
|
isFocused={tag.id === focusedAnnotationId}
|
|
@@ -5,6 +5,7 @@ import { useTranslations } from '../../../contexts/TranslationContext';
|
|
|
5
5
|
import type { components, Selector } from '@semiont/core';
|
|
6
6
|
type JobProgress = components['schemas']['JobProgress'];
|
|
7
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
9
|
import type { Annotator } from '../../../lib/annotation-registry';
|
|
9
10
|
import { StatisticsPanel } from './StatisticsPanel';
|
|
10
11
|
import { HighlightPanel } from './HighlightPanel';
|
|
@@ -37,6 +38,12 @@ const TAB_ORDER: TabKey[] = ['statistics', 'reference', 'highlight', 'assessment
|
|
|
37
38
|
* - All operations managed via event bus (no callback props)
|
|
38
39
|
*/
|
|
39
40
|
interface UnifiedAnnotationsPanelProps {
|
|
41
|
+
/** Session carrying the client and event bus; null renders inert. */
|
|
42
|
+
session: SemiontSession | null;
|
|
43
|
+
|
|
44
|
+
/** Host-owned navigation: called with the resolved resource id when a reference entry opens. */
|
|
45
|
+
onOpenResource?: (resourceId: string) => void;
|
|
46
|
+
|
|
40
47
|
// All annotations (grouped internally by motivation)
|
|
41
48
|
annotations: Annotation[];
|
|
42
49
|
|
|
@@ -245,6 +252,7 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
245
252
|
|
|
246
253
|
// Common props for all annotation panels
|
|
247
254
|
const commonProps = {
|
|
255
|
+
session: props.session,
|
|
248
256
|
resourceId: props.resourceId,
|
|
249
257
|
annotations,
|
|
250
258
|
pendingAnnotation: props.pendingAnnotation,
|
|
@@ -270,6 +278,8 @@ export function UnifiedAnnotationsPanel(props: UnifiedAnnotationsPanelProps) {
|
|
|
270
278
|
if (activeTab === 'reference') {
|
|
271
279
|
return (
|
|
272
280
|
<ReferencesPanel
|
|
281
|
+
session={commonProps.session}
|
|
282
|
+
onOpenResource={props.onOpenResource}
|
|
273
283
|
resourceId={commonProps.resourceId}
|
|
274
284
|
annotations={commonProps.annotations}
|
|
275
285
|
pendingAnnotation={commonProps.pendingAnnotation}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { screen } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
|
-
import { renderWithProviders } from '../../../../test-utils';
|
|
4
|
+
import { renderWithProviders, createTestSemiontWrapper } from '../../../../test-utils';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
|
|
7
|
-
import type { Annotation, AnnotationId } from '@semiont/core';
|
|
7
|
+
import type { Annotation, AnnotationId, EventBus } from '@semiont/core';
|
|
8
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
9
|
|
|
9
10
|
// Mock @semiont/http-transport
|
|
10
11
|
vi.mock('@semiont/core', async () => {
|
|
@@ -53,20 +54,28 @@ describe('AssessmentEntry', () => {
|
|
|
53
54
|
isFocused: false,
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
// Per-test session/bus — created in beforeEach (a module-scope factory
|
|
58
|
+
// call would hand tests a client that test-utils disposes after the
|
|
59
|
+
// first test). The `session` prop and the `eventBus` the interaction
|
|
60
|
+
// test subscribes come from the SAME factory call.
|
|
61
|
+
let session: SemiontSession;
|
|
62
|
+
let eventBus: EventBus;
|
|
63
|
+
|
|
56
64
|
beforeEach(() => {
|
|
57
65
|
vi.clearAllMocks();
|
|
66
|
+
({ session, eventBus } = createTestSemiontWrapper());
|
|
58
67
|
mockGetAnnotationExactText.mockReturnValue('Selected passage text');
|
|
59
68
|
});
|
|
60
69
|
|
|
61
70
|
describe('Rendering', () => {
|
|
62
71
|
it('should render the selected text in quotes', () => {
|
|
63
|
-
renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
72
|
+
renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
64
73
|
|
|
65
74
|
expect(screen.getByText(/Selected passage text/)).toBeInTheDocument();
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
it('should render the assessment body text', () => {
|
|
69
|
-
renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
78
|
+
renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
70
79
|
|
|
71
80
|
expect(screen.getByText('This passage needs clarification')).toBeInTheDocument();
|
|
72
81
|
});
|
|
@@ -80,7 +89,7 @@ describe('AssessmentEntry', () => {
|
|
|
80
89
|
});
|
|
81
90
|
|
|
82
91
|
renderWithProviders(
|
|
83
|
-
<AssessmentEntry assessment={assessment} isFocused={false} />
|
|
92
|
+
<AssessmentEntry assessment={assessment} isFocused={false} session={session} />
|
|
84
93
|
);
|
|
85
94
|
|
|
86
95
|
expect(screen.getByText('Direct body assessment')).toBeInTheDocument();
|
|
@@ -95,7 +104,7 @@ describe('AssessmentEntry', () => {
|
|
|
95
104
|
});
|
|
96
105
|
|
|
97
106
|
renderWithProviders(
|
|
98
|
-
<AssessmentEntry assessment={assessment} isFocused={false} />
|
|
107
|
+
<AssessmentEntry assessment={assessment} isFocused={false} session={session} />
|
|
99
108
|
);
|
|
100
109
|
|
|
101
110
|
expect(screen.getByText('Array body assessment')).toBeInTheDocument();
|
|
@@ -105,14 +114,14 @@ describe('AssessmentEntry', () => {
|
|
|
105
114
|
const longText = 'X'.repeat(150);
|
|
106
115
|
mockGetAnnotationExactText.mockReturnValue(longText);
|
|
107
116
|
|
|
108
|
-
renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
117
|
+
renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
109
118
|
|
|
110
119
|
expect(screen.getByText(new RegExp(`"${'X'.repeat(100)}`))).toBeInTheDocument();
|
|
111
120
|
expect(screen.getByText(/\.\.\./)).toBeInTheDocument();
|
|
112
121
|
});
|
|
113
122
|
|
|
114
123
|
it('should show creator name', () => {
|
|
115
|
-
renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
124
|
+
renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
116
125
|
|
|
117
126
|
expect(screen.getByText(/reviewer@example.com/)).toBeInTheDocument();
|
|
118
127
|
});
|
|
@@ -122,7 +131,7 @@ describe('AssessmentEntry', () => {
|
|
|
122
131
|
delete (assessment as Record<string, unknown>).creator;
|
|
123
132
|
|
|
124
133
|
renderWithProviders(
|
|
125
|
-
<AssessmentEntry assessment={assessment} isFocused={false} />
|
|
134
|
+
<AssessmentEntry assessment={assessment} isFocused={false} session={session} />
|
|
126
135
|
);
|
|
127
136
|
|
|
128
137
|
expect(screen.getByText(/Unknown/)).toBeInTheDocument();
|
|
@@ -133,7 +142,7 @@ describe('AssessmentEntry', () => {
|
|
|
133
142
|
delete (assessment as Record<string, unknown>).body;
|
|
134
143
|
|
|
135
144
|
const { container } = renderWithProviders(
|
|
136
|
-
<AssessmentEntry assessment={assessment} isFocused={false} />
|
|
145
|
+
<AssessmentEntry assessment={assessment} isFocused={false} session={session} />
|
|
137
146
|
);
|
|
138
147
|
|
|
139
148
|
// Body section should not render
|
|
@@ -143,7 +152,7 @@ describe('AssessmentEntry', () => {
|
|
|
143
152
|
it('should not render quote section when selectedText is empty', () => {
|
|
144
153
|
mockGetAnnotationExactText.mockReturnValue('');
|
|
145
154
|
|
|
146
|
-
const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
155
|
+
const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
147
156
|
|
|
148
157
|
expect(container.querySelector('.semiont-annotation-entry__quote')).not.toBeInTheDocument();
|
|
149
158
|
});
|
|
@@ -154,7 +163,7 @@ describe('AssessmentEntry', () => {
|
|
|
154
163
|
});
|
|
155
164
|
|
|
156
165
|
renderWithProviders(
|
|
157
|
-
<AssessmentEntry assessment={recentAssessment} isFocused={false} />
|
|
166
|
+
<AssessmentEntry assessment={recentAssessment} isFocused={false} session={session} />
|
|
158
167
|
);
|
|
159
168
|
|
|
160
169
|
expect(screen.getByText(/just now/)).toBeInTheDocument();
|
|
@@ -165,12 +174,11 @@ describe('AssessmentEntry', () => {
|
|
|
165
174
|
it('should emit browse:click on click', async () => {
|
|
166
175
|
const clickHandler = vi.fn();
|
|
167
176
|
|
|
168
|
-
const { container
|
|
169
|
-
<AssessmentEntry {...defaultProps}
|
|
170
|
-
{ returnEventBus: true }
|
|
177
|
+
const { container } = renderWithProviders(
|
|
178
|
+
<AssessmentEntry {...defaultProps} session={session} />
|
|
171
179
|
);
|
|
172
180
|
|
|
173
|
-
const subscription = eventBus
|
|
181
|
+
const subscription = eventBus.get('browse:click').subscribe(clickHandler);
|
|
174
182
|
|
|
175
183
|
const entry = container.firstChild as HTMLElement;
|
|
176
184
|
await userEvent.click(entry);
|
|
@@ -187,7 +195,7 @@ describe('AssessmentEntry', () => {
|
|
|
187
195
|
describe('Hover state', () => {
|
|
188
196
|
it('should apply pulse class when isHovered is true', () => {
|
|
189
197
|
const { container } = renderWithProviders(
|
|
190
|
-
<AssessmentEntry {...defaultProps} isHovered={true} />
|
|
198
|
+
<AssessmentEntry {...defaultProps} session={session} isHovered={true} />
|
|
191
199
|
);
|
|
192
200
|
|
|
193
201
|
const entry = container.firstChild as HTMLElement;
|
|
@@ -196,7 +204,7 @@ describe('AssessmentEntry', () => {
|
|
|
196
204
|
|
|
197
205
|
it('should not apply pulse class when isHovered is false', () => {
|
|
198
206
|
const { container } = renderWithProviders(
|
|
199
|
-
<AssessmentEntry {...defaultProps} isHovered={false} />
|
|
207
|
+
<AssessmentEntry {...defaultProps} session={session} isHovered={false} />
|
|
200
208
|
);
|
|
201
209
|
|
|
202
210
|
const entry = container.firstChild as HTMLElement;
|
|
@@ -207,7 +215,7 @@ describe('AssessmentEntry', () => {
|
|
|
207
215
|
describe('Focus state', () => {
|
|
208
216
|
it('should set data-focused to true when focused', () => {
|
|
209
217
|
const { container } = renderWithProviders(
|
|
210
|
-
<AssessmentEntry {...defaultProps} isFocused={true} />
|
|
218
|
+
<AssessmentEntry {...defaultProps} session={session} isFocused={true} />
|
|
211
219
|
);
|
|
212
220
|
|
|
213
221
|
const entry = container.firstChild as HTMLElement;
|
|
@@ -215,7 +223,7 @@ describe('AssessmentEntry', () => {
|
|
|
215
223
|
});
|
|
216
224
|
|
|
217
225
|
it('should set data-type to assessment', () => {
|
|
218
|
-
const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} />);
|
|
226
|
+
const { container } = renderWithProviders(<AssessmentEntry {...defaultProps} session={session} />);
|
|
219
227
|
|
|
220
228
|
const entry = container.firstChild as HTMLElement;
|
|
221
229
|
expect(entry).toHaveAttribute('data-type', 'assessment');
|