@semiont/react-ui 0.4.20 → 0.4.22
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-5QESNO5H.mjs} +13 -16
- package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-4NOUO3W6.mjs +7788 -0
- package/dist/chunk-4NOUO3W6.mjs.map +1 -0
- package/dist/index.d.mts +212 -1206
- package/dist/index.mjs +3332 -13712
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +48 -21
- package/dist/test-utils.mjs +2505 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +12 -12
- package/src/components/LiveRegion.tsx +1 -2
- 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 +50 -65
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -9
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/JsonLdView.tsx +1 -2
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
- package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +12 -8
- package/src/components/modals/SearchModal.tsx +11 -6
- package/src/components/modals/SearchResultsStep.tsx +1 -3
- 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.accessibility.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
- 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 +15 -18
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
- package/src/components/resource/AnnotateView.tsx +8 -10
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +11 -8
- package/src/components/resource/ResourceViewer.tsx +22 -34
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
- package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
- package/src/components/resource/event-formatting.ts +2 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
- package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
- package/src/components/resource/panels/AssistSection.tsx +15 -21
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +7 -8
- package/src/components/resource/panels/CommentsPanel.tsx +11 -13
- package/src/components/resource/panels/HighlightEntry.tsx +7 -8
- package/src/components/resource/panels/HighlightPanel.tsx +12 -13
- package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
- package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
- package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
- package/src/components/resource/panels/TagEntry.tsx +7 -8
- package/src/components/resource/panels/TaggingPanel.tsx +14 -23
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
- 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 +28 -53
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
- package/src/components/settings/SettingsPanel.tsx +9 -9
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
- 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 +6 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
- 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
|
@@ -1,42 +1,37 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { screen } from '@testing-library/react';
|
|
3
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
5
|
import '@testing-library/jest-dom';
|
|
5
6
|
import { renderWithProviders } from '../../test-utils';
|
|
6
|
-
|
|
7
|
-
// Mock api-hooks
|
|
8
|
-
vi.mock('../../lib/api-hooks', () => ({
|
|
9
|
-
useHealth: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
import { useHealth } from '../../lib/api-hooks';
|
|
13
|
-
import type { MockedFunction } from 'vitest';
|
|
14
7
|
import { StatusDisplay } from '../StatusDisplay';
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
let mockGetStatus: ReturnType<typeof vi.fn>;
|
|
10
|
+
const stableMockClient = {
|
|
11
|
+
admin: {
|
|
12
|
+
get status() { return mockGetStatus; },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const stableMockSession = { client: stableMockClient };
|
|
16
|
+
const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
|
|
17
|
+
const stableMockBrowser = { activeSession$: stableActiveSession$ };
|
|
18
|
+
|
|
19
|
+
vi.mock('../../session/SemiontProvider', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import('../../session/SemiontProvider')>();
|
|
19
21
|
return {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
status: {
|
|
24
|
-
useQuery: vi.fn().mockReturnValue({
|
|
25
|
-
data: queryResult.data ?? undefined,
|
|
26
|
-
isLoading: queryResult.isLoading ?? false,
|
|
27
|
-
error: queryResult.error ?? undefined,
|
|
28
|
-
}),
|
|
29
|
-
},
|
|
22
|
+
...actual,
|
|
23
|
+
useSemiont: () => stableMockBrowser,
|
|
30
24
|
};
|
|
31
|
-
}
|
|
25
|
+
});
|
|
32
26
|
|
|
33
27
|
describe('StatusDisplay', () => {
|
|
34
28
|
beforeEach(() => {
|
|
35
29
|
vi.clearAllMocks();
|
|
30
|
+
mockGetStatus = vi.fn();
|
|
36
31
|
});
|
|
37
32
|
|
|
38
33
|
it('should show "Authentication required" when not fully authenticated', () => {
|
|
39
|
-
|
|
34
|
+
mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.0.0' });
|
|
40
35
|
|
|
41
36
|
renderWithProviders(
|
|
42
37
|
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
@@ -45,55 +40,45 @@ describe('StatusDisplay', () => {
|
|
|
45
40
|
expect(screen.getByText(/Authentication required/)).toBeInTheDocument();
|
|
46
41
|
});
|
|
47
42
|
|
|
48
|
-
it('should show
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
renderWithProviders(
|
|
52
|
-
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
expect(screen.getByText(/Connecting\.\.\./)).toBeInTheDocument();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should show status when data is available', () => {
|
|
59
|
-
mockUseHealth.mockReturnValue(createMockHealth({
|
|
60
|
-
data: { status: 'healthy', version: '1.2.3' },
|
|
61
|
-
}) as ReturnType<typeof useHealth>);
|
|
43
|
+
it('should show status when data is available', async () => {
|
|
44
|
+
mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.2.3' });
|
|
62
45
|
|
|
63
46
|
renderWithProviders(
|
|
64
47
|
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
65
48
|
);
|
|
66
49
|
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
await waitFor(() => {
|
|
51
|
+
expect(screen.getByText(/healthy/)).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText(/v1\.2\.3/)).toBeInTheDocument();
|
|
53
|
+
});
|
|
69
54
|
});
|
|
70
55
|
|
|
71
|
-
it('should show "Connection failed" on error', () => {
|
|
72
|
-
|
|
73
|
-
error: new Error('Network error'),
|
|
74
|
-
}) as ReturnType<typeof useHealth>);
|
|
56
|
+
it('should show "Connection failed" on error', async () => {
|
|
57
|
+
mockGetStatus.mockRejectedValue(new Error('Network error'));
|
|
75
58
|
|
|
76
59
|
renderWithProviders(
|
|
77
60
|
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
78
61
|
);
|
|
79
62
|
|
|
80
|
-
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(screen.getByText(/Connection failed/)).toBeInTheDocument();
|
|
65
|
+
});
|
|
81
66
|
});
|
|
82
67
|
|
|
83
|
-
it('should show re-login message for 401 errors', () => {
|
|
84
|
-
|
|
85
|
-
error: new Error('401 Unauthorized'),
|
|
86
|
-
}) as ReturnType<typeof useHealth>);
|
|
68
|
+
it('should show re-login message for 401 errors', async () => {
|
|
69
|
+
mockGetStatus.mockRejectedValue(new Error('401 Unauthorized'));
|
|
87
70
|
|
|
88
71
|
renderWithProviders(
|
|
89
72
|
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
90
73
|
);
|
|
91
74
|
|
|
92
|
-
|
|
75
|
+
await waitFor(() => {
|
|
76
|
+
expect(screen.getByText(/sign out and sign in again/i)).toBeInTheDocument();
|
|
77
|
+
});
|
|
93
78
|
});
|
|
94
79
|
|
|
95
80
|
it('should show warning for authenticated users missing backend token', () => {
|
|
96
|
-
|
|
81
|
+
mockGetStatus.mockResolvedValue(undefined);
|
|
97
82
|
|
|
98
83
|
renderWithProviders(
|
|
99
84
|
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={true} hasValidBackendToken={false} />
|
|
@@ -103,7 +88,7 @@ describe('StatusDisplay', () => {
|
|
|
103
88
|
});
|
|
104
89
|
|
|
105
90
|
it('should have role="status"', () => {
|
|
106
|
-
|
|
91
|
+
mockGetStatus.mockResolvedValue(undefined);
|
|
107
92
|
|
|
108
93
|
renderWithProviders(
|
|
109
94
|
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
@@ -113,7 +98,7 @@ describe('StatusDisplay', () => {
|
|
|
113
98
|
});
|
|
114
99
|
|
|
115
100
|
it('should have aria-live="polite"', () => {
|
|
116
|
-
|
|
101
|
+
mockGetStatus.mockResolvedValue(undefined);
|
|
117
102
|
|
|
118
103
|
renderWithProviders(
|
|
119
104
|
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
@@ -124,7 +109,7 @@ describe('StatusDisplay', () => {
|
|
|
124
109
|
});
|
|
125
110
|
|
|
126
111
|
it('should show sign-in hint when not authenticated', () => {
|
|
127
|
-
|
|
112
|
+
mockGetStatus.mockResolvedValue(undefined);
|
|
128
113
|
|
|
129
114
|
renderWithProviders(
|
|
130
115
|
<StatusDisplay isFullyAuthenticated={false} isAuthenticated={false} />
|
|
@@ -133,28 +118,28 @@ describe('StatusDisplay', () => {
|
|
|
133
118
|
expect(screen.getByText('Sign in to view backend status')).toBeInTheDocument();
|
|
134
119
|
});
|
|
135
120
|
|
|
136
|
-
it('should show error hint when there is an error', () => {
|
|
137
|
-
|
|
138
|
-
error: new Error('Connection refused'),
|
|
139
|
-
}) as ReturnType<typeof useHealth>);
|
|
121
|
+
it('should show error hint when there is an error', async () => {
|
|
122
|
+
mockGetStatus.mockRejectedValue(new Error('Connection refused'));
|
|
140
123
|
|
|
141
124
|
renderWithProviders(
|
|
142
125
|
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
143
126
|
);
|
|
144
127
|
|
|
145
|
-
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(screen.getByText(/Check that the backend server is running/)).toBeInTheDocument();
|
|
130
|
+
});
|
|
146
131
|
});
|
|
147
132
|
|
|
148
|
-
it('should set data-status attribute based on state', () => {
|
|
149
|
-
|
|
150
|
-
data: { status: 'healthy', version: '1.0.0' },
|
|
151
|
-
}) as ReturnType<typeof useHealth>);
|
|
133
|
+
it('should set data-status attribute based on state', async () => {
|
|
134
|
+
mockGetStatus.mockResolvedValue({ status: 'healthy', version: '1.0.0' });
|
|
152
135
|
|
|
153
136
|
renderWithProviders(
|
|
154
137
|
<StatusDisplay isFullyAuthenticated={true} isAuthenticated={true} hasValidBackendToken={true} />
|
|
155
138
|
);
|
|
156
139
|
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
await waitFor(() => {
|
|
141
|
+
const status = screen.getByRole('status');
|
|
142
|
+
expect(status).toHaveAttribute('data-status', 'success');
|
|
143
|
+
});
|
|
159
144
|
});
|
|
160
145
|
});
|
|
@@ -66,15 +66,15 @@ describe('Toolbar', () => {
|
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
describe('event emission', () => {
|
|
69
|
-
it('emits
|
|
69
|
+
it('emits panel:toggle with panel name on click', () => {
|
|
70
70
|
const handler = vi.fn();
|
|
71
71
|
|
|
72
|
-
const {
|
|
72
|
+
const { shellBus } = renderWithProviders(
|
|
73
73
|
<Toolbar context="document" activePanel={null} />,
|
|
74
|
-
{
|
|
74
|
+
{ returnShellBus: true }
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
-
const subscription =
|
|
77
|
+
const subscription = shellBus!.get('panel:toggle').subscribe(handler);
|
|
78
78
|
|
|
79
79
|
fireEvent.click(screen.getByLabelText('Toolbar.resourceInfo'));
|
|
80
80
|
expect(handler).toHaveBeenCalledWith({ panel: 'info' });
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useRef, useEffect } 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 { getSupportedShapes } from '../../lib/media-shapes';
|
|
7
8
|
import type { Annotator } from '../../lib/annotation-registry';
|
|
8
9
|
import './annotations.css';
|
|
@@ -119,7 +120,7 @@ export function AnnotateToolbar({
|
|
|
119
120
|
annotators
|
|
120
121
|
}: AnnotateToolbarProps) {
|
|
121
122
|
const t = useTranslations('AnnotateToolbar');
|
|
122
|
-
const
|
|
123
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
123
124
|
|
|
124
125
|
// Helper to get emoji from annotators by motivation (with fallback for safety)
|
|
125
126
|
const getMotivationEmoji = (motivation: SelectionMotivation): string => {
|
|
@@ -188,11 +189,9 @@ export function AnnotateToolbar({
|
|
|
188
189
|
const handleSelectionClick = (motivation: SelectionMotivation | null) => {
|
|
189
190
|
// If null is clicked, always deselect. Otherwise toggle.
|
|
190
191
|
if (motivation === null) {
|
|
191
|
-
|
|
192
|
+
session?.client.mark.changeSelection(null);
|
|
192
193
|
} else {
|
|
193
|
-
|
|
194
|
-
motivation: selectedMotivation === motivation ? null : motivation
|
|
195
|
-
});
|
|
194
|
+
session?.client.mark.changeSelection(selectedMotivation === motivation ? null : motivation);
|
|
196
195
|
}
|
|
197
196
|
// Close dropdown after selection
|
|
198
197
|
setSelectionPinned(false);
|
|
@@ -200,21 +199,21 @@ export function AnnotateToolbar({
|
|
|
200
199
|
};
|
|
201
200
|
|
|
202
201
|
const handleClickClick = (action: ClickAction) => {
|
|
203
|
-
|
|
202
|
+
session?.client.mark.changeClick(action);
|
|
204
203
|
// Close dropdown after selection
|
|
205
204
|
setClickPinned(false);
|
|
206
205
|
setClickHovered(false);
|
|
207
206
|
};
|
|
208
207
|
|
|
209
208
|
const handleShapeClick = (shape: ShapeType) => {
|
|
210
|
-
|
|
209
|
+
session?.client.mark.changeShape(shape);
|
|
211
210
|
// Close dropdown after selection
|
|
212
211
|
setShapePinned(false);
|
|
213
212
|
setShapeHovered(false);
|
|
214
213
|
};
|
|
215
214
|
|
|
216
215
|
const handleModeToggle = () => {
|
|
217
|
-
|
|
216
|
+
session?.client.mark.toggleMode();
|
|
218
217
|
setModePinned(false);
|
|
219
218
|
setModeHovered(false);
|
|
220
219
|
};
|
|
@@ -3,9 +3,10 @@ import { render, screen, fireEvent, waitFor, within } from '@testing-library/rea
|
|
|
3
3
|
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
|
4
4
|
import { AnnotateToolbar, type SelectionMotivation, type ClickAction } from '../AnnotateToolbar';
|
|
5
5
|
import { ANNOTATORS } from '../../../lib/annotation-registry';
|
|
6
|
-
import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
|
|
7
6
|
import { TranslationProvider } from '../../../contexts/TranslationContext';
|
|
7
|
+
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
8
8
|
import type { TranslationManager } from '../../../types/TranslationManager';
|
|
9
|
+
import type { EventBus } from '@semiont/core';
|
|
9
10
|
|
|
10
11
|
// Mock translations
|
|
11
12
|
const messages: Record<string, Record<string, string>> = {
|
|
@@ -43,68 +44,37 @@ interface TrackedEvent {
|
|
|
43
44
|
|
|
44
45
|
function createEventTracker() {
|
|
45
46
|
const events: TrackedEvent[] = [];
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const handlers: Array<() => void> = [];
|
|
52
|
-
|
|
53
|
-
// Track toolbar-related events
|
|
54
|
-
const trackEvent = (eventName: string) => (payload: any) => {
|
|
55
|
-
events.push({ event: eventName, payload });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const toolbarEvents = [
|
|
47
|
+
return {
|
|
48
|
+
events,
|
|
49
|
+
clear: () => { events.length = 0; },
|
|
50
|
+
_attach(eventBus: EventBus) {
|
|
51
|
+
const trackedEvents = [
|
|
59
52
|
'mark:mode-toggled',
|
|
60
53
|
'mark:click-changed',
|
|
61
54
|
'mark:selection-changed',
|
|
62
55
|
'mark:shape-changed',
|
|
63
56
|
] as const;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
handlers.push(subscription);
|
|
57
|
+
trackedEvents.forEach((eventName) => {
|
|
58
|
+
eventBus.get(eventName).subscribe((payload: any) => {
|
|
59
|
+
events.push({ event: eventName, payload });
|
|
60
|
+
});
|
|
69
61
|
});
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
handlers.forEach(sub => sub.unsubscribe());
|
|
73
|
-
};
|
|
74
|
-
}, [eventBus]);
|
|
75
|
-
|
|
76
|
-
return <>{children}</>;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
EventTrackingWrapper,
|
|
81
|
-
events,
|
|
82
|
-
clear: () => {
|
|
83
|
-
events.length = 0;
|
|
84
62
|
},
|
|
85
63
|
};
|
|
86
64
|
}
|
|
87
65
|
|
|
88
66
|
const renderWithIntl = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<tracker.EventTrackingWrapper>
|
|
94
|
-
{component}
|
|
95
|
-
</tracker.EventTrackingWrapper>
|
|
96
|
-
</TranslationProvider>
|
|
97
|
-
</EventBusProvider>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return render(
|
|
102
|
-
<EventBusProvider>
|
|
67
|
+
const { SemiontWrapper, eventBus } = createTestSemiontWrapper();
|
|
68
|
+
if (tracker) tracker._attach(eventBus);
|
|
69
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
70
|
+
<SemiontWrapper>
|
|
103
71
|
<TranslationProvider translationManager={translationManager}>
|
|
104
|
-
{
|
|
72
|
+
{children}
|
|
105
73
|
</TranslationProvider>
|
|
106
|
-
</
|
|
74
|
+
</SemiontWrapper>
|
|
107
75
|
);
|
|
76
|
+
const result = render(component, { wrapper: Wrapper });
|
|
77
|
+
return { ...result, eventBus };
|
|
108
78
|
};
|
|
109
79
|
|
|
110
80
|
describe('AnnotateToolbar', () => {
|
|
@@ -187,14 +157,10 @@ describe('AnnotateToolbar', () => {
|
|
|
187
157
|
expect(screen.getByText('Browse')).toBeInTheDocument();
|
|
188
158
|
|
|
189
159
|
rerender(
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
annotateMode={true}
|
|
195
|
-
/>
|
|
196
|
-
</TranslationProvider>
|
|
197
|
-
</EventBusProvider>
|
|
160
|
+
<AnnotateToolbar
|
|
161
|
+
{...defaultProps}
|
|
162
|
+
annotateMode={true}
|
|
163
|
+
/>
|
|
198
164
|
);
|
|
199
165
|
expect(screen.getByText('Annotate')).toBeInTheDocument();
|
|
200
166
|
});
|
|
@@ -292,16 +258,10 @@ describe('AnnotateToolbar', () => {
|
|
|
292
258
|
|
|
293
259
|
// Simulate mode change by rerendering with new mode
|
|
294
260
|
rerender(
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
{...defaultProps}
|
|
300
|
-
annotateMode={true}
|
|
301
|
-
/>
|
|
302
|
-
</tracker.EventTrackingWrapper>
|
|
303
|
-
</TranslationProvider>
|
|
304
|
-
</EventBusProvider>
|
|
261
|
+
<AnnotateToolbar
|
|
262
|
+
{...defaultProps}
|
|
263
|
+
annotateMode={true}
|
|
264
|
+
/>
|
|
305
265
|
);
|
|
306
266
|
|
|
307
267
|
// After mode change, the collapsed content should show "Annotate"
|
|
@@ -399,16 +359,10 @@ describe('AnnotateToolbar', () => {
|
|
|
399
359
|
|
|
400
360
|
// Simulate selection
|
|
401
361
|
rerender(
|
|
402
|
-
<
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
{...defaultProps}
|
|
407
|
-
selectedMotivation="highlighting"
|
|
408
|
-
/>
|
|
409
|
-
</tracker.EventTrackingWrapper>
|
|
410
|
-
</TranslationProvider>
|
|
411
|
-
</EventBusProvider>
|
|
362
|
+
<AnnotateToolbar
|
|
363
|
+
{...defaultProps}
|
|
364
|
+
selectedMotivation="highlighting"
|
|
365
|
+
/>
|
|
412
366
|
);
|
|
413
367
|
|
|
414
368
|
// Click again to deselect
|
|
@@ -8,9 +8,8 @@ import { oneDark } from '@codemirror/theme-one-dark';
|
|
|
8
8
|
import { syntaxHighlighting } from '@codemirror/language';
|
|
9
9
|
import { jsonLightTheme, jsonLightHighlightStyle } from '../../lib/codemirror-json-theme';
|
|
10
10
|
import { useLineNumbers } from '../../hooks/useLineNumbers';
|
|
11
|
-
import type { components } from '@semiont/core';
|
|
12
11
|
|
|
13
|
-
type Annotation
|
|
12
|
+
import type { Annotation } from '@semiont/core';
|
|
14
13
|
|
|
15
14
|
interface JsonLdViewProps {
|
|
16
15
|
annotation: Annotation;
|
|
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
6
|
import type { components } from '@semiont/core';
|
|
7
7
|
|
|
8
|
-
type Annotation
|
|
8
|
+
import type { Annotation } from '@semiont/core';
|
|
9
9
|
|
|
10
10
|
// Mock CodeMirror modules
|
|
11
11
|
vi.mock('@codemirror/view', () => {
|
|
@@ -70,7 +70,6 @@ const createMockAnnotation = (overrides?: Partial<Annotation>): Annotation => ({
|
|
|
70
70
|
end: 10,
|
|
71
71
|
},
|
|
72
72
|
},
|
|
73
|
-
body: [],
|
|
74
73
|
...overrides,
|
|
75
74
|
});
|
|
76
75
|
|
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react';
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { parseSvgSelector } from '@semiont/
|
|
8
|
-
import type {
|
|
9
|
-
|
|
10
|
-
type Annotation = components['schemas']['Annotation'];
|
|
11
|
-
|
|
4
|
+
import type { Annotation } from '@semiont/core';
|
|
5
|
+
import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/core';
|
|
6
|
+
import { createHoverHandlers } from '@semiont/sdk';
|
|
7
|
+
import { parseSvgSelector } from '@semiont/core';
|
|
8
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
12
9
|
interface AnnotationOverlayProps {
|
|
13
10
|
annotations: Annotation[];
|
|
14
11
|
imageWidth: number;
|
|
15
12
|
imageHeight: number;
|
|
16
13
|
displayWidth: number;
|
|
17
14
|
displayHeight: number;
|
|
18
|
-
|
|
15
|
+
session?: SemiontSession | null | undefined;
|
|
19
16
|
hoveredAnnotationId?: string | null;
|
|
20
17
|
selectedAnnotationId?: string | null;
|
|
21
18
|
hoverDelayMs: number;
|
|
@@ -72,7 +69,7 @@ export function AnnotationOverlay({
|
|
|
72
69
|
imageHeight,
|
|
73
70
|
displayWidth,
|
|
74
71
|
displayHeight,
|
|
75
|
-
|
|
72
|
+
session,
|
|
76
73
|
hoveredAnnotationId,
|
|
77
74
|
selectedAnnotationId,
|
|
78
75
|
hoverDelayMs
|
|
@@ -81,8 +78,8 @@ export function AnnotationOverlay({
|
|
|
81
78
|
const scaleY = displayHeight / imageHeight;
|
|
82
79
|
|
|
83
80
|
const { handleMouseEnter, handleMouseLeave } = useMemo(
|
|
84
|
-
() => createHoverHandlers((
|
|
85
|
-
[
|
|
81
|
+
() => createHoverHandlers((id) => session?.client.beckon.hover(id), hoverDelayMs),
|
|
82
|
+
[session, hoverDelayMs]
|
|
86
83
|
);
|
|
87
84
|
|
|
88
85
|
return (
|
|
@@ -131,7 +128,7 @@ export function AnnotationOverlay({
|
|
|
131
128
|
className="semiont-annotation-overlay__shape"
|
|
132
129
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
133
130
|
data-selected={isSelected ? 'true' : 'false'}
|
|
134
|
-
onClick={() =>
|
|
131
|
+
onClick={() => session?.client.browse.click(annotation.id, annotation.motivation)}
|
|
135
132
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
136
133
|
onMouseLeave={handleMouseLeave}
|
|
137
134
|
/>
|
|
@@ -144,7 +141,7 @@ export function AnnotationOverlay({
|
|
|
144
141
|
style={{ userSelect: 'none' }}
|
|
145
142
|
onClick={(e) => {
|
|
146
143
|
e.stopPropagation();
|
|
147
|
-
|
|
144
|
+
session?.client.browse.click(annotation.id, annotation.motivation);
|
|
148
145
|
}}
|
|
149
146
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
150
147
|
onMouseLeave={handleMouseLeave}
|
|
@@ -178,7 +175,7 @@ export function AnnotationOverlay({
|
|
|
178
175
|
className="semiont-annotation-overlay__shape"
|
|
179
176
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
180
177
|
data-selected={isSelected ? 'true' : 'false'}
|
|
181
|
-
onClick={() =>
|
|
178
|
+
onClick={() => session?.client.browse.click(annotation.id, annotation.motivation)}
|
|
182
179
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
183
180
|
onMouseLeave={handleMouseLeave}
|
|
184
181
|
/>
|
|
@@ -191,7 +188,7 @@ export function AnnotationOverlay({
|
|
|
191
188
|
style={{ userSelect: 'none' }}
|
|
192
189
|
onClick={(e) => {
|
|
193
190
|
e.stopPropagation();
|
|
194
|
-
|
|
191
|
+
session?.client.browse.click(annotation.id, annotation.motivation);
|
|
195
192
|
}}
|
|
196
193
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
197
194
|
onMouseLeave={handleMouseLeave}
|
|
@@ -238,7 +235,7 @@ export function AnnotationOverlay({
|
|
|
238
235
|
className="semiont-annotation-overlay__shape"
|
|
239
236
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
240
237
|
data-selected={isSelected ? 'true' : 'false'}
|
|
241
|
-
onClick={() =>
|
|
238
|
+
onClick={() => session?.client.browse.click(annotation.id, annotation.motivation)}
|
|
242
239
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
243
240
|
onMouseLeave={handleMouseLeave}
|
|
244
241
|
/>
|
|
@@ -251,7 +248,7 @@ export function AnnotationOverlay({
|
|
|
251
248
|
style={{ userSelect: 'none' }}
|
|
252
249
|
onClick={(e) => {
|
|
253
250
|
e.stopPropagation();
|
|
254
|
-
|
|
251
|
+
session?.client.browse.click(annotation.id, annotation.motivation);
|
|
255
252
|
}}
|
|
256
253
|
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
257
254
|
onMouseLeave={handleMouseLeave}
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
4
|
-
import type {
|
|
5
|
-
import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative, parseSvgSelector, Point } from '@semiont/
|
|
4
|
+
import type { Annotation } from '@semiont/core';
|
|
5
|
+
import { createRectangleSvg, createCircleSvg, createPolygonSvg, scaleSvgToNative, parseSvgSelector, Point } from '@semiont/core';
|
|
6
6
|
import { AnnotationOverlay } from './AnnotationOverlay';
|
|
7
7
|
import type { SelectionMotivation } from '../annotation/AnnotateToolbar';
|
|
8
|
-
import type {
|
|
8
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
9
9
|
import { useHoverDelay } from '../../hooks/useHoverDelay';
|
|
10
10
|
|
|
11
|
-
type Annotation = components['schemas']['Annotation'];
|
|
12
|
-
|
|
13
11
|
export type DrawingMode = 'rectangle' | 'polygon' | 'circle' | 'freeform' | null;
|
|
14
12
|
|
|
15
13
|
/**
|
|
@@ -40,7 +38,7 @@ interface SvgDrawingCanvasProps {
|
|
|
40
38
|
existingAnnotations?: Annotation[];
|
|
41
39
|
drawingMode: DrawingMode;
|
|
42
40
|
selectedMotivation?: SelectionMotivation | null;
|
|
43
|
-
|
|
41
|
+
session?: SemiontSession | null | undefined;
|
|
44
42
|
hoveredAnnotationId?: string | null;
|
|
45
43
|
selectedAnnotationId?: string | null;
|
|
46
44
|
hoverDelayMs?: number;
|
|
@@ -57,7 +55,7 @@ export function SvgDrawingCanvas({
|
|
|
57
55
|
existingAnnotations = [],
|
|
58
56
|
drawingMode,
|
|
59
57
|
selectedMotivation,
|
|
60
|
-
|
|
58
|
+
session,
|
|
61
59
|
hoveredAnnotationId,
|
|
62
60
|
selectedAnnotationId
|
|
63
61
|
}: SvgDrawingCanvasProps) {
|
|
@@ -212,7 +210,7 @@ export function SvgDrawingCanvas({
|
|
|
212
210
|
});
|
|
213
211
|
|
|
214
212
|
if (clickedAnnotation) {
|
|
215
|
-
|
|
213
|
+
session?.client.browse.click(clickedAnnotation.id, clickedAnnotation.motivation);
|
|
216
214
|
setIsDrawing(false);
|
|
217
215
|
setStartPoint(null);
|
|
218
216
|
setCurrentPoint(null);
|
|
@@ -274,14 +272,11 @@ export function SvgDrawingCanvas({
|
|
|
274
272
|
);
|
|
275
273
|
|
|
276
274
|
// Emit annotation:requested event with SvgSelector
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
},
|
|
283
|
-
motivation: selectedMotivation
|
|
284
|
-
});
|
|
275
|
+
if (session && selectedMotivation) {
|
|
276
|
+
session.client.mark.request(
|
|
277
|
+
{ type: 'SvgSelector', value: nativeSvg },
|
|
278
|
+
selectedMotivation,
|
|
279
|
+
);
|
|
285
280
|
}
|
|
286
281
|
|
|
287
282
|
// Reset drawing state
|
|
@@ -338,7 +333,7 @@ export function SvgDrawingCanvas({
|
|
|
338
333
|
displayWidth={displayDimensions.width}
|
|
339
334
|
displayHeight={displayDimensions.height}
|
|
340
335
|
hoverDelayMs={hoverDelayMs}
|
|
341
|
-
{...(
|
|
336
|
+
{...(session && { session })}
|
|
342
337
|
{...(hoveredAnnotationId !== undefined && { hoveredAnnotationId })}
|
|
343
338
|
{...(selectedAnnotationId !== undefined && { selectedAnnotationId })}
|
|
344
339
|
/>
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
4
|
import type { GatheredContext } from '@semiont/core';
|
|
5
|
-
import { LOCALES } from '@semiont/
|
|
6
|
-
|
|
5
|
+
import { LOCALES } from '@semiont/core';
|
|
7
6
|
export interface GenerationConfig {
|
|
8
7
|
title: string;
|
|
9
8
|
storagePath: string;
|