@semiont/react-ui 0.4.20 → 0.4.21
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/README.md +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
- package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-KEDFYI6N.mjs +7788 -0
- package/dist/chunk-KEDFYI6N.mjs.map +1 -0
- package/dist/index.d.mts +171 -1140
- package/dist/index.mjs +3263 -13644
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +46 -21
- package/dist/test-utils.mjs +2499 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +1 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +9 -11
- package/src/components/StatusDisplay.tsx +42 -16
- package/src/components/Toolbar.tsx +4 -4
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
- package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -7
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
- package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +10 -6
- package/src/components/modals/SearchModal.tsx +10 -6
- package/src/components/modals/SessionExpiredModal.tsx +11 -11
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
- package/src/components/navigation/ObservableLink.tsx +6 -6
- package/src/components/navigation/SimpleNavigation.tsx +4 -4
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
- package/src/components/resource/AnnotateView.tsx +7 -6
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +8 -7
- package/src/components/resource/ResourceViewer.tsx +17 -25
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
- package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
- package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
- package/src/components/resource/panels/AssistSection.tsx +11 -13
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +5 -4
- package/src/components/resource/panels/CommentsPanel.tsx +9 -11
- package/src/components/resource/panels/HighlightEntry.tsx +5 -4
- package/src/components/resource/panels/HighlightPanel.tsx +10 -11
- package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
- package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
- package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
- package/src/components/resource/panels/TagEntry.tsx +5 -4
- package/src/components/resource/panels/TaggingPanel.tsx +10 -16
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
- package/src/components/settings/SettingsPanel.tsx +8 -8
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
- package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
- package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
- package/dist/chunk-OZICDVH7.mjs +0 -62
- package/dist/chunk-OZICDVH7.mjs.map +0 -1
- package/dist/chunk-R4CCMFJH.mjs +0 -877
- package/dist/chunk-R4CCMFJH.mjs.map +0 -1
- package/dist/chunk-VN5NY4SN.mjs +0 -200
- package/dist/chunk-VN5NY4SN.mjs.map +0 -1
- package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
5
|
+
import { useSemiont } from '../../../session/SemiontProvider';
|
|
6
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
6
7
|
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
7
8
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
8
9
|
import { AnnotateReferencesProgressWidget } from '../../AnnotateReferencesProgressWidget';
|
|
9
10
|
import { ReferenceEntry } from './ReferenceEntry';
|
|
10
|
-
import type { components,
|
|
11
|
+
import type { components, Selector } from '@semiont/core';
|
|
11
12
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
12
13
|
import { PanelHeader } from './PanelHeader';
|
|
13
14
|
import './ReferencesPanel.css';
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
type JobProgress = components['schemas']['JobProgress'];
|
|
15
17
|
|
|
16
18
|
type Annotation = components['schemas']['Annotation'];
|
|
17
19
|
type Motivation = components['schemas']['Motivation'];
|
|
18
|
-
type
|
|
19
|
-
type ReferencedBy = ResponseContent<paths['/resources/{id}/referenced-by']['get']>['referencedBy'][number];
|
|
20
|
+
type ReferencedBy = components['schemas']['GetReferencedByResponse']['referencedBy'][number];
|
|
20
21
|
|
|
21
22
|
// Unified pending annotation type
|
|
22
23
|
interface PendingAnnotation {
|
|
@@ -45,7 +46,7 @@ interface Props {
|
|
|
45
46
|
// Generic panel props
|
|
46
47
|
annotations?: Annotation[];
|
|
47
48
|
isAssisting: boolean;
|
|
48
|
-
progress:
|
|
49
|
+
progress: JobProgress | null;
|
|
49
50
|
annotateMode?: boolean;
|
|
50
51
|
Link: React.ComponentType<LinkComponentProps>;
|
|
51
52
|
routes: RouteBuilder;
|
|
@@ -86,7 +87,7 @@ export function ReferencesPanel({
|
|
|
86
87
|
hoveredAnnotationId,
|
|
87
88
|
}: Props) {
|
|
88
89
|
const t = useTranslations('ReferencesPanel');
|
|
89
|
-
const
|
|
90
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
90
91
|
const [selectedEntityTypes, setSelectedEntityTypes] = useState<string[]>([]);
|
|
91
92
|
const [lastAnnotationLog, setLastDetectionLog] = useState<Array<{ entityType: string; foundCount: number }> | null>(null);
|
|
92
93
|
const [pendingEntityTypes, setPendingEntityTypes] = useState<string[]>([]);
|
|
@@ -202,7 +203,7 @@ export function ReferencesPanel({
|
|
|
202
203
|
// Clear log when starting new annotation
|
|
203
204
|
const handleAssist = () => {
|
|
204
205
|
setLastDetectionLog(null);
|
|
205
|
-
|
|
206
|
+
session?.client.emit('mark:assist-request', {
|
|
206
207
|
motivation: 'linking',
|
|
207
208
|
options: {
|
|
208
209
|
entityTypes: selectedEntityTypes,
|
|
@@ -245,7 +246,7 @@ export function ReferencesPanel({
|
|
|
245
246
|
const handleCreateReference = () => {
|
|
246
247
|
if (pendingAnnotation) {
|
|
247
248
|
const entityType = pendingEntityTypes.join(',') || undefined;
|
|
248
|
-
|
|
249
|
+
session?.client.emit('mark:submit', {
|
|
249
250
|
motivation: 'linking',
|
|
250
251
|
selector: pendingAnnotation.selector,
|
|
251
252
|
body: entityType ? [{ type: 'TextualBody', value: entityType, purpose: 'tagging' }] : [],
|
|
@@ -260,14 +261,14 @@ export function ReferencesPanel({
|
|
|
260
261
|
|
|
261
262
|
const handleEscape = (e: KeyboardEvent) => {
|
|
262
263
|
if (e.key === 'Escape') {
|
|
263
|
-
|
|
264
|
+
session?.client.emit('mark:cancel-pending', undefined);
|
|
264
265
|
setPendingEntityTypes([]);
|
|
265
266
|
}
|
|
266
267
|
};
|
|
267
268
|
|
|
268
269
|
document.addEventListener('keydown', handleEscape);
|
|
269
270
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
270
|
-
}, [pendingAnnotation]);
|
|
271
|
+
}, [pendingAnnotation, session]);
|
|
271
272
|
|
|
272
273
|
return (
|
|
273
274
|
<div className="semiont-panel">
|
|
@@ -312,7 +313,7 @@ export function ReferencesPanel({
|
|
|
312
313
|
<div className="semiont-annotation-prompt__actions">
|
|
313
314
|
<button
|
|
314
315
|
onClick={() => {
|
|
315
|
-
|
|
316
|
+
session?.client.emit('mark:cancel-pending', undefined);
|
|
316
317
|
setPendingEntityTypes([]);
|
|
317
318
|
}}
|
|
318
319
|
className="semiont-button semiont-button--secondary"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../../session/SemiontProvider';
|
|
5
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
5
6
|
import { formatLocaleDisplay } from '@semiont/api-client';
|
|
6
7
|
import { resourceId as makeResourceId, type components } from '@semiont/core';
|
|
7
8
|
import './ResourceInfoPanel.css';
|
|
@@ -47,7 +48,7 @@ export function ResourceInfoPanel({
|
|
|
47
48
|
generator,
|
|
48
49
|
}: Props) {
|
|
49
50
|
const t = useTranslations('ResourceInfoPanel');
|
|
50
|
-
const
|
|
51
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
54
|
<div className="semiont-resource-info-panel">
|
|
@@ -148,7 +149,7 @@ export function ResourceInfoPanel({
|
|
|
148
149
|
<button
|
|
149
150
|
key={id}
|
|
150
151
|
className="semiont-resource-info-panel__link"
|
|
151
|
-
onClick={() =>
|
|
152
|
+
onClick={() => session?.client.emit('browse:reference-navigate', { resourceId: id })}
|
|
152
153
|
>
|
|
153
154
|
{i > 0 && ', '}{id}
|
|
154
155
|
</button>
|
|
@@ -191,7 +192,7 @@ export function ResourceInfoPanel({
|
|
|
191
192
|
{/* Clone Action */}
|
|
192
193
|
<div className="semiont-resource-info-panel__action-section">
|
|
193
194
|
<button
|
|
194
|
-
onClick={() =>
|
|
195
|
+
onClick={() => session?.client.emit('yield:clone', undefined)}
|
|
195
196
|
className="semiont-resource-button semiont-resource-button--secondary"
|
|
196
197
|
>
|
|
197
198
|
🔗 {t('clone')}
|
|
@@ -206,7 +207,7 @@ export function ResourceInfoPanel({
|
|
|
206
207
|
{isArchived ? (
|
|
207
208
|
<>
|
|
208
209
|
<button
|
|
209
|
-
onClick={() =>
|
|
210
|
+
onClick={() => session?.client.emit('mark:unarchive', { resourceId: makeResourceId(resourceId) })}
|
|
210
211
|
className="semiont-resource-button semiont-resource-button--secondary"
|
|
211
212
|
>
|
|
212
213
|
📤 {t('unarchive')}
|
|
@@ -218,7 +219,7 @@ export function ResourceInfoPanel({
|
|
|
218
219
|
) : (
|
|
219
220
|
<>
|
|
220
221
|
<button
|
|
221
|
-
onClick={() =>
|
|
222
|
+
onClick={() => session?.client.emit('mark:archive', { resourceId: makeResourceId(resourceId) })}
|
|
222
223
|
className="semiont-resource-button semiont-resource-button--archive"
|
|
223
224
|
>
|
|
224
225
|
📦 {t('archive')}
|
|
@@ -5,8 +5,9 @@ import type { components } from '@semiont/core';
|
|
|
5
5
|
import { getAnnotationExactText } from '@semiont/api-client';
|
|
6
6
|
import { getTagCategory, getTagSchemaId } from '@semiont/ontology';
|
|
7
7
|
import { getTagSchema } from '../../../lib/tag-schemas';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { useSemiont } from '../../../session/SemiontProvider';
|
|
9
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
10
|
+
import { useHoverEmitter } from '../../../hooks/useHoverEmitter';
|
|
10
11
|
|
|
11
12
|
type Annotation = components['schemas']['Annotation'];
|
|
12
13
|
|
|
@@ -23,7 +24,7 @@ export function TagEntry({
|
|
|
23
24
|
isHovered = false,
|
|
24
25
|
ref,
|
|
25
26
|
}: TagEntryProps) {
|
|
26
|
-
const
|
|
27
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
27
28
|
const hoverProps = useHoverEmitter(tag.id);
|
|
28
29
|
|
|
29
30
|
const selectedText = getAnnotationExactText(tag);
|
|
@@ -35,7 +36,7 @@ export function TagEntry({
|
|
|
35
36
|
<div
|
|
36
37
|
ref={ref}
|
|
37
38
|
onClick={() => {
|
|
38
|
-
|
|
39
|
+
session?.client.emit('browse:click', { annotationId: tag.id, motivation: tag.motivation });
|
|
39
40
|
}}
|
|
40
41
|
{...hoverProps}
|
|
41
42
|
className={`semiont-annotation-entry${isHovered ? ' semiont-annotation-pulse' : ''}`}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import {
|
|
5
|
+
import { useSemiont } from '../../../session/SemiontProvider';
|
|
6
|
+
import { useObservable } from '../../../hooks/useObservable';
|
|
6
7
|
import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
|
|
7
8
|
import type { components, Selector } from '@semiont/core';
|
|
8
9
|
import { getTextPositionSelector, getTargetSelector } from '@semiont/api-client';
|
|
@@ -13,6 +14,7 @@ import './TaggingPanel.css';
|
|
|
13
14
|
|
|
14
15
|
type Annotation = components['schemas']['Annotation'];
|
|
15
16
|
type Motivation = components['schemas']['Motivation'];
|
|
17
|
+
type JobProgress = components['schemas']['JobProgress'];
|
|
16
18
|
|
|
17
19
|
// Unified pending annotation type
|
|
18
20
|
interface PendingAnnotation {
|
|
@@ -41,15 +43,7 @@ interface TaggingPanelProps {
|
|
|
41
43
|
annotations: Annotation[];
|
|
42
44
|
annotateMode?: boolean;
|
|
43
45
|
isAssisting?: boolean;
|
|
44
|
-
progress?:
|
|
45
|
-
status: string;
|
|
46
|
-
percentage?: number;
|
|
47
|
-
currentCategory?: string;
|
|
48
|
-
processedCategories?: number;
|
|
49
|
-
totalCategories?: number;
|
|
50
|
-
message?: string;
|
|
51
|
-
requestParams?: Array<{ label: string; value: string }>;
|
|
52
|
-
} | null;
|
|
46
|
+
progress?: JobProgress | null;
|
|
53
47
|
pendingAnnotation: PendingAnnotation | null;
|
|
54
48
|
scrollToAnnotationId?: string | null;
|
|
55
49
|
onScrollCompleted?: () => void;
|
|
@@ -75,7 +69,7 @@ export function TaggingPanel({
|
|
|
75
69
|
hoveredAnnotationId,
|
|
76
70
|
}: TaggingPanelProps) {
|
|
77
71
|
const t = useTranslations('TaggingPanel');
|
|
78
|
-
const
|
|
72
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
79
73
|
const [selectedSchemaId, setSelectedSchemaId] = useState<string>('legal-irac');
|
|
80
74
|
const [selectedCategories, setSelectedCategories] = useState<Set<string>>(new Set());
|
|
81
75
|
const [focusedAnnotationId, setFocusedAnnotationId] = useState<string | null>(null);
|
|
@@ -195,7 +189,7 @@ export function TaggingPanel({
|
|
|
195
189
|
|
|
196
190
|
const handleAssist = () => {
|
|
197
191
|
if (selectedCategories.size > 0) {
|
|
198
|
-
|
|
192
|
+
session?.client.emit('mark:assist-request', {
|
|
199
193
|
motivation: 'tagging',
|
|
200
194
|
options: {
|
|
201
195
|
schemaId: selectedSchemaId,
|
|
@@ -212,13 +206,13 @@ export function TaggingPanel({
|
|
|
212
206
|
|
|
213
207
|
const handleEscape = (e: KeyboardEvent) => {
|
|
214
208
|
if (e.key === 'Escape') {
|
|
215
|
-
|
|
209
|
+
session?.client.emit('mark:cancel-pending', undefined);
|
|
216
210
|
}
|
|
217
211
|
};
|
|
218
212
|
|
|
219
213
|
document.addEventListener('keydown', handleEscape);
|
|
220
214
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
221
|
-
}, [pendingAnnotation]);
|
|
215
|
+
}, [pendingAnnotation, session]);
|
|
222
216
|
|
|
223
217
|
// Color schemes are now handled via CSS data attributes
|
|
224
218
|
|
|
@@ -274,7 +268,7 @@ export function TaggingPanel({
|
|
|
274
268
|
className="semiont-select"
|
|
275
269
|
onChange={(e) => {
|
|
276
270
|
if (e.target.value && pendingAnnotation) {
|
|
277
|
-
|
|
271
|
+
session?.client.emit('mark:submit', {
|
|
278
272
|
motivation: 'tagging',
|
|
279
273
|
selector: pendingAnnotation.selector,
|
|
280
274
|
body: [
|
|
@@ -305,7 +299,7 @@ export function TaggingPanel({
|
|
|
305
299
|
{/* Cancel button */}
|
|
306
300
|
<div className="semiont-annotation-prompt__footer">
|
|
307
301
|
<button
|
|
308
|
-
onClick={() =>
|
|
302
|
+
onClick={() => session?.client.emit('mark:cancel-pending', undefined)}
|
|
309
303
|
className="semiont-button semiont-button--secondary"
|
|
310
304
|
data-type="tag"
|
|
311
305
|
>
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect } from 'react';
|
|
4
4
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
5
|
-
import type { components, Selector
|
|
5
|
+
import type { components, Selector } from '@semiont/core';
|
|
6
|
+
type JobProgress = components['schemas']['JobProgress'];
|
|
6
7
|
import type { RouteBuilder, LinkComponentProps } from '../../../contexts/RoutingContext';
|
|
7
8
|
import type { Annotator } from '../../../lib/annotation-registry';
|
|
8
9
|
import { StatisticsPanel } from './StatisticsPanel';
|
|
@@ -47,7 +48,7 @@ interface UnifiedAnnotationsPanelProps {
|
|
|
47
48
|
|
|
48
49
|
// Annotation assistance state (per motivation)
|
|
49
50
|
assistingMotivation?: Motivation | null;
|
|
50
|
-
progress?:
|
|
51
|
+
progress?: JobProgress | null;
|
|
51
52
|
|
|
52
53
|
// Unified pending annotation (for creating new annotations)
|
|
53
54
|
pendingAnnotation: PendingAnnotation | null;
|
|
@@ -5,8 +5,8 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
6
|
import '@testing-library/jest-dom';
|
|
7
7
|
import { AssessmentPanel } from '../AssessmentPanel';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
8
|
+
import type { components, EventBus } from '@semiont/core';
|
|
9
|
+
import { createTestSemiontWrapper } from '../../../../test-utils';
|
|
10
10
|
|
|
11
11
|
type Annotation = components['schemas']['Annotation'];
|
|
12
12
|
|
|
@@ -18,59 +18,27 @@ interface TrackedEvent {
|
|
|
18
18
|
|
|
19
19
|
function createEventTracker() {
|
|
20
20
|
const events: TrackedEvent[] = [];
|
|
21
|
-
|
|
22
|
-
function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
|
|
23
|
-
const eventBus = useEventBus();
|
|
24
|
-
|
|
25
|
-
React.useEffect(() => {
|
|
26
|
-
const handlers: Array<() => void> = [];
|
|
27
|
-
|
|
28
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
29
|
-
events.push({ event: eventName, payload });
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const panelEvents = ['mark:submit'] as const;
|
|
33
|
-
|
|
34
|
-
panelEvents.forEach(eventName => {
|
|
35
|
-
const handler = trackEvent(eventName);
|
|
36
|
-
const subscription = eventBus.get(eventName).subscribe(handler);
|
|
37
|
-
handlers.push(subscription);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
return () => {
|
|
41
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
42
|
-
};
|
|
43
|
-
}, [eventBus]);
|
|
44
|
-
|
|
45
|
-
return <>{children}</>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
21
|
return {
|
|
49
|
-
EventTrackingWrapper,
|
|
50
22
|
events,
|
|
51
|
-
clear: () => {
|
|
52
|
-
|
|
23
|
+
clear: () => { events.length = 0; },
|
|
24
|
+
_attach(eventBus: EventBus) {
|
|
25
|
+
const panelEvents = ['mark:submit'] as const;
|
|
26
|
+
panelEvents.forEach((eventName) => {
|
|
27
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
28
|
+
events.push({ event: eventName, payload });
|
|
29
|
+
});
|
|
30
|
+
});
|
|
53
31
|
},
|
|
54
32
|
};
|
|
55
33
|
}
|
|
56
34
|
|
|
57
|
-
// Helper to render with EventBusProvider
|
|
58
35
|
const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{component}
|
|
64
|
-
</tracker.EventTrackingWrapper>
|
|
65
|
-
</EventBusProvider>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return render(
|
|
70
|
-
<EventBusProvider>
|
|
71
|
-
{component}
|
|
72
|
-
</EventBusProvider>
|
|
36
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
37
|
+
if (tracker) tracker._attach(eventBus);
|
|
38
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
39
|
+
<SemiontWrapper>{children}</SemiontWrapper>
|
|
73
40
|
);
|
|
41
|
+
return render(component, { wrapper: Wrapper });
|
|
74
42
|
};
|
|
75
43
|
|
|
76
44
|
// Mock TranslationContext
|
|
@@ -108,8 +76,7 @@ vi.mock('../AssessmentEntry', () => ({
|
|
|
108
76
|
),
|
|
109
77
|
}));
|
|
110
78
|
|
|
111
|
-
// Mock AssistSection component
|
|
112
|
-
// Just render a simplified version
|
|
79
|
+
// Mock AssistSection component — just render a simplified version.
|
|
113
80
|
vi.mock('../AssistSection', () => ({
|
|
114
81
|
AssistSection: ({ annotationType, isAssisting }: any) => (
|
|
115
82
|
<div data-testid="detect-section">
|
|
@@ -380,7 +347,7 @@ describe('AssessmentPanel Component', () => {
|
|
|
380
347
|
expect(tracker.events.some(e =>
|
|
381
348
|
e.event === 'mark:submit' &&
|
|
382
349
|
e.payload?.motivation === 'assessing' &&
|
|
383
|
-
e.payload?.body?.
|
|
350
|
+
e.payload?.body?.value === 'My assessment'
|
|
384
351
|
)).toBe(true);
|
|
385
352
|
});
|
|
386
353
|
});
|
|
@@ -421,8 +388,7 @@ describe('AssessmentPanel Component', () => {
|
|
|
421
388
|
expect(tracker.events.some(e =>
|
|
422
389
|
e.event === 'mark:submit' &&
|
|
423
390
|
e.payload?.motivation === 'assessing' &&
|
|
424
|
-
|
|
425
|
-
e.payload.body.length === 0
|
|
391
|
+
e.payload?.body === undefined
|
|
426
392
|
)).toBe(true);
|
|
427
393
|
});
|
|
428
394
|
});
|
|
@@ -36,7 +36,10 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
|
|
|
36
36
|
|
|
37
37
|
describe('CollaborationPanel Component', () => {
|
|
38
38
|
const defaultProps = {
|
|
39
|
-
|
|
39
|
+
// `degraded` is the "sustained disconnect" state — matches the
|
|
40
|
+
// pre-state-machine `isConnected: false` default, which was always
|
|
41
|
+
// used in tests to mean "the UI should show Disconnected."
|
|
42
|
+
state: 'degraded' as const,
|
|
40
43
|
eventCount: 0,
|
|
41
44
|
};
|
|
42
45
|
|
|
@@ -72,19 +75,19 @@ describe('CollaborationPanel Component', () => {
|
|
|
72
75
|
|
|
73
76
|
describe('Connection Status', () => {
|
|
74
77
|
it('should show disconnected status when not connected', () => {
|
|
75
|
-
render(<CollaborationPanel {...defaultProps}
|
|
78
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
76
79
|
|
|
77
80
|
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
78
81
|
});
|
|
79
82
|
|
|
80
83
|
it('should show live status when connected', () => {
|
|
81
|
-
render(<CollaborationPanel {...defaultProps}
|
|
84
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
82
85
|
|
|
83
86
|
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
it('should show indicator when disconnected', () => {
|
|
87
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
90
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
88
91
|
|
|
89
92
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|
|
90
93
|
expect(indicator).toBeInTheDocument();
|
|
@@ -92,7 +95,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
it('should show indicator when connected', () => {
|
|
95
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
98
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
96
99
|
|
|
97
100
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|
|
98
101
|
expect(indicator).toBeInTheDocument();
|
|
@@ -100,7 +103,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
100
103
|
});
|
|
101
104
|
|
|
102
105
|
it('should use appropriate status text for disconnected state', () => {
|
|
103
|
-
render(<CollaborationPanel {...defaultProps}
|
|
106
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
104
107
|
|
|
105
108
|
const statusText = screen.getByText('Disconnected');
|
|
106
109
|
expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
|
|
@@ -108,35 +111,63 @@ describe('CollaborationPanel Component', () => {
|
|
|
108
111
|
});
|
|
109
112
|
|
|
110
113
|
it('should use appropriate status text for connected state', () => {
|
|
111
|
-
render(<CollaborationPanel {...defaultProps}
|
|
114
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
112
115
|
|
|
113
116
|
const statusText = screen.getByText('Live');
|
|
114
117
|
expect(statusText).toHaveClass('semiont-collaboration-panel__status-text');
|
|
115
118
|
expect(statusText).toHaveAttribute('data-connected', 'true');
|
|
116
119
|
});
|
|
120
|
+
|
|
121
|
+
// ── State-machine aware cases (post-CONNECTION-STATE) ─────────────
|
|
122
|
+
// These exercise the core reason CONNECTION-STATE exists: brief
|
|
123
|
+
// reconnect/connect cycles must NOT flash "Disconnected", or
|
|
124
|
+
// Strict-Mode mount churn makes the UI lie.
|
|
125
|
+
|
|
126
|
+
it('shows Live during brief `reconnecting` (does not alarm on churn)', () => {
|
|
127
|
+
render(<CollaborationPanel {...defaultProps} state="reconnecting" />);
|
|
128
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
129
|
+
expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('shows Live during `connecting` and `initial`', () => {
|
|
133
|
+
const { rerender } = render(<CollaborationPanel {...defaultProps} state="connecting" />);
|
|
134
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
135
|
+
rerender(<CollaborationPanel {...defaultProps} state="initial" />);
|
|
136
|
+
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('shows Disconnected on `degraded` (sustained disconnect)', () => {
|
|
140
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
141
|
+
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('shows Disconnected on `closed` (terminal)', () => {
|
|
145
|
+
render(<CollaborationPanel {...defaultProps} state="closed" />);
|
|
146
|
+
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
147
|
+
});
|
|
117
148
|
});
|
|
118
149
|
|
|
119
150
|
describe('Event Count', () => {
|
|
120
151
|
it('should not show event count when zero', () => {
|
|
121
|
-
render(<CollaborationPanel {...defaultProps}
|
|
152
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={0} />);
|
|
122
153
|
|
|
123
154
|
expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
|
|
124
155
|
});
|
|
125
156
|
|
|
126
157
|
it('should show event count when connected and greater than zero', () => {
|
|
127
|
-
render(<CollaborationPanel {...defaultProps}
|
|
158
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={5} />);
|
|
128
159
|
|
|
129
160
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
130
161
|
});
|
|
131
162
|
|
|
132
163
|
it('should not show event count when disconnected', () => {
|
|
133
|
-
render(<CollaborationPanel {...defaultProps}
|
|
164
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" eventCount={5} />);
|
|
134
165
|
|
|
135
166
|
expect(screen.queryByText(/event/i)).not.toBeInTheDocument();
|
|
136
167
|
});
|
|
137
168
|
|
|
138
169
|
it('should display correct event count', () => {
|
|
139
|
-
render(<CollaborationPanel {...defaultProps}
|
|
170
|
+
render(<CollaborationPanel {...defaultProps} state="open" eventCount={42} />);
|
|
140
171
|
|
|
141
172
|
// The translation will have ${count} in it
|
|
142
173
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
@@ -267,13 +298,13 @@ describe('CollaborationPanel Component', () => {
|
|
|
267
298
|
|
|
268
299
|
describe('Real-time Status Messages', () => {
|
|
269
300
|
it('should show "real-time active" when connected', () => {
|
|
270
|
-
render(<CollaborationPanel {...defaultProps}
|
|
301
|
+
render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
271
302
|
|
|
272
303
|
expect(screen.getByText('Real-time synchronization active')).toBeInTheDocument();
|
|
273
304
|
});
|
|
274
305
|
|
|
275
306
|
it('should show "reconnecting" when disconnected', () => {
|
|
276
|
-
render(<CollaborationPanel {...defaultProps}
|
|
307
|
+
render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
277
308
|
|
|
278
309
|
expect(screen.getByText('Reconnecting...')).toBeInTheDocument();
|
|
279
310
|
});
|
|
@@ -281,11 +312,11 @@ describe('CollaborationPanel Component', () => {
|
|
|
281
312
|
|
|
282
313
|
describe('Dynamic Updates', () => {
|
|
283
314
|
it('should update when connection status changes', () => {
|
|
284
|
-
const { rerender } = render(<CollaborationPanel {...defaultProps}
|
|
315
|
+
const { rerender } = render(<CollaborationPanel {...defaultProps} state="degraded" />);
|
|
285
316
|
|
|
286
317
|
expect(screen.getByText('Disconnected')).toBeInTheDocument();
|
|
287
318
|
|
|
288
|
-
rerender(<CollaborationPanel {...defaultProps}
|
|
319
|
+
rerender(<CollaborationPanel {...defaultProps} state="open" />);
|
|
289
320
|
|
|
290
321
|
expect(screen.getByText('Live')).toBeInTheDocument();
|
|
291
322
|
expect(screen.queryByText('Disconnected')).not.toBeInTheDocument();
|
|
@@ -293,12 +324,12 @@ describe('CollaborationPanel Component', () => {
|
|
|
293
324
|
|
|
294
325
|
it('should update when event count changes', () => {
|
|
295
326
|
const { rerender } = render(
|
|
296
|
-
<CollaborationPanel {...defaultProps}
|
|
327
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={5} />
|
|
297
328
|
);
|
|
298
329
|
|
|
299
330
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
300
331
|
|
|
301
|
-
rerender(<CollaborationPanel {...defaultProps}
|
|
332
|
+
rerender(<CollaborationPanel {...defaultProps} state="open" eventCount={10} />);
|
|
302
333
|
|
|
303
334
|
expect(screen.getByText(/event/i)).toBeInTheDocument();
|
|
304
335
|
});
|
|
@@ -371,7 +402,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
371
402
|
it('should handle very large event counts', () => {
|
|
372
403
|
expect(() => {
|
|
373
404
|
render(
|
|
374
|
-
<CollaborationPanel {...defaultProps}
|
|
405
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={999999} />
|
|
375
406
|
);
|
|
376
407
|
}).not.toThrow();
|
|
377
408
|
});
|
|
@@ -379,7 +410,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
379
410
|
it('should handle negative event counts', () => {
|
|
380
411
|
expect(() => {
|
|
381
412
|
render(
|
|
382
|
-
<CollaborationPanel {...defaultProps}
|
|
413
|
+
<CollaborationPanel {...defaultProps} state="open" eventCount={-5} />
|
|
383
414
|
);
|
|
384
415
|
}).not.toThrow();
|
|
385
416
|
});
|
|
@@ -432,7 +463,7 @@ describe('CollaborationPanel Component', () => {
|
|
|
432
463
|
});
|
|
433
464
|
|
|
434
465
|
it('should have visible status indicators', () => {
|
|
435
|
-
const { container } = render(<CollaborationPanel {...defaultProps}
|
|
466
|
+
const { container } = render(<CollaborationPanel {...defaultProps} state="open" />);
|
|
436
467
|
|
|
437
468
|
// Should have a visible status dot
|
|
438
469
|
const indicator = container.querySelector('.semiont-collaboration-panel__dot');
|