@semiont/react-ui 0.2.33-build.79 → 0.2.33-build.81
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/EventBusContext-CJjL_cCf.d.mts +462 -0
- package/dist/{PdfAnnotationCanvas.client-ADC4FFSE.mjs → PdfAnnotationCanvas.client-RAJRPQLU.mjs} +42 -27
- package/dist/PdfAnnotationCanvas.client-RAJRPQLU.mjs.map +1 -0
- package/dist/{ar-EMHEHPCJ.mjs → ar-4ZEORRW2.mjs} +7 -4
- package/dist/ar-4ZEORRW2.mjs.map +1 -0
- package/dist/{bn-OVCI4F6X.mjs → bn-SEDE5BQJ.mjs} +7 -4
- package/dist/bn-SEDE5BQJ.mjs.map +1 -0
- package/dist/{chunk-LIHZTECW.mjs → chunk-D7NBW4RV.mjs} +7 -4
- package/dist/chunk-D7NBW4RV.mjs.map +1 -0
- package/dist/{chunk-JZIO2A3B.mjs → chunk-QB52Q7EQ.mjs} +206 -146
- package/dist/chunk-QB52Q7EQ.mjs.map +1 -0
- package/dist/{cs-FAN66Q2F.mjs → cs-7W4WF5WD.mjs} +7 -4
- package/dist/cs-7W4WF5WD.mjs.map +1 -0
- package/dist/{da-YBBIHI2O.mjs → da-75XGBCBK.mjs} +7 -4
- package/dist/da-75XGBCBK.mjs.map +1 -0
- package/dist/{de-MAYU33LB.mjs → de-ODJVFLHM.mjs} +7 -4
- package/dist/de-ODJVFLHM.mjs.map +1 -0
- package/dist/{el-MKGSWN4O.mjs → el-C4PM4WB3.mjs} +7 -4
- package/dist/el-C4PM4WB3.mjs.map +1 -0
- package/dist/{en-DDLIXJCU.mjs → en-KJCJQ4OO.mjs} +2 -2
- package/dist/{es-52LHUWJD.mjs → es-WD33R7QL.mjs} +7 -4
- package/dist/es-WD33R7QL.mjs.map +1 -0
- package/dist/{fa-FJICRANB.mjs → fa-2BP6V56P.mjs} +7 -4
- package/dist/fa-2BP6V56P.mjs.map +1 -0
- package/dist/{fi-O455XFCR.mjs → fi-USRRW24J.mjs} +7 -4
- package/dist/fi-USRRW24J.mjs.map +1 -0
- package/dist/{fr-TXIXHOOE.mjs → fr-EC5S6WVF.mjs} +7 -4
- package/dist/fr-EC5S6WVF.mjs.map +1 -0
- package/dist/{he-JBSOX5IN.mjs → he-7TBVIKAA.mjs} +7 -4
- package/dist/he-7TBVIKAA.mjs.map +1 -0
- package/dist/{hi-KGHI3XVT.mjs → hi-FO4VIZLA.mjs} +7 -4
- package/dist/hi-FO4VIZLA.mjs.map +1 -0
- package/dist/{id-5OCPPZLO.mjs → id-7U7GGVWY.mjs} +7 -4
- package/dist/id-7U7GGVWY.mjs.map +1 -0
- package/dist/index.css +123 -85
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +715 -574
- package/dist/index.mjs +3898 -3575
- package/dist/index.mjs.map +1 -1
- package/dist/{it-PNBBZSM2.mjs → it-Y4OPL6I2.mjs} +7 -4
- package/dist/it-Y4OPL6I2.mjs.map +1 -0
- package/dist/{ja-LDD7R3TJ.mjs → ja-PK7SQL55.mjs} +7 -4
- package/dist/ja-PK7SQL55.mjs.map +1 -0
- package/dist/{ko-F47ZDEY3.mjs → ko-L25PXMYD.mjs} +7 -4
- package/dist/ko-L25PXMYD.mjs.map +1 -0
- package/dist/{ms-Z7LMXJWL.mjs → ms-STH777QM.mjs} +7 -4
- package/dist/ms-STH777QM.mjs.map +1 -0
- package/dist/{nl-6SJFBPJ3.mjs → nl-Y7LECDDR.mjs} +7 -4
- package/dist/nl-Y7LECDDR.mjs.map +1 -0
- package/dist/{no-YXPBPSGF.mjs → no-KEKCEWU6.mjs} +7 -4
- package/dist/no-KEKCEWU6.mjs.map +1 -0
- package/dist/{pl-P4AZ2QME.mjs → pl-7A7OC75O.mjs} +7 -4
- package/dist/pl-7A7OC75O.mjs.map +1 -0
- package/dist/{pt-LHWUS6U6.mjs → pt-35HTM7RA.mjs} +7 -4
- package/dist/pt-35HTM7RA.mjs.map +1 -0
- package/dist/{ro-EA5J2ZON.mjs → ro-VAWL5KQA.mjs} +7 -4
- package/dist/ro-VAWL5KQA.mjs.map +1 -0
- package/dist/{sv-DATBS3UQ.mjs → sv-7ZK5EQEB.mjs} +7 -4
- package/dist/sv-7ZK5EQEB.mjs.map +1 -0
- package/dist/test-utils.d.mts +18 -8
- package/dist/test-utils.mjs +36 -14
- package/dist/test-utils.mjs.map +1 -1
- package/dist/{th-WTFJRWPT.mjs → th-UDWZ4X34.mjs} +7 -4
- package/dist/th-UDWZ4X34.mjs.map +1 -0
- package/dist/{tr-IKO3RXOX.mjs → tr-4WMPK3UX.mjs} +7 -4
- package/dist/tr-4WMPK3UX.mjs.map +1 -0
- package/dist/{uk-CF6CTTRK.mjs → uk-SSLASQYJ.mjs} +7 -4
- package/dist/uk-SSLASQYJ.mjs.map +1 -0
- package/dist/{vi-AJLTXPZQ.mjs → vi-IF42Z5PU.mjs} +7 -4
- package/dist/vi-IF42Z5PU.mjs.map +1 -0
- package/dist/{zh-U3ORHHYH.mjs → zh-HRQTNTAI.mjs} +7 -4
- package/dist/zh-HRQTNTAI.mjs.map +1 -0
- package/package.json +3 -1
- package/src/components/CodeMirrorRenderer.tsx +66 -93
- package/src/components/DetectionProgressWidget.tsx +16 -5
- package/src/components/ResizeHandle.tsx +10 -4
- package/src/components/SessionExpiryBanner.tsx +2 -3
- package/src/components/SessionTimer.tsx +3 -3
- package/src/components/Toolbar.tsx +18 -9
- package/src/components/__tests__/SessionTimer.test.tsx +33 -33
- package/src/components/annotation/AnnotateToolbar.tsx +17 -15
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +165 -63
- package/src/components/annotation/annotation-entries.css +10 -0
- package/src/components/annotation-popups/JsonLdView.tsx +8 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +42 -22
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +27 -30
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +12 -33
- package/src/components/layout/__tests__/PageLayout.test.tsx +37 -32
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +21 -40
- package/src/components/modals/ResourceSearchModal.tsx +2 -2
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +14 -9
- package/src/components/navigation/NavigationTabs.css +36 -24
- package/src/components/navigation/ObservableLink.tsx +91 -0
- package/src/components/navigation/SimpleNavigation.tsx +20 -16
- package/src/components/navigation/SortableResourceTab.tsx +11 -5
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +51 -26
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +28 -22
- package/src/components/resource/AnnotateView.tsx +64 -134
- package/src/components/resource/BrowseView.tsx +86 -166
- package/src/components/resource/HistoryEvent.tsx +13 -7
- package/src/components/resource/ResourceViewer.tsx +122 -264
- package/src/components/resource/__tests__/BrowseView.test.tsx +631 -0
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +231 -0
- package/src/components/resource/panels/AssessmentEntry.tsx +25 -33
- package/src/components/resource/panels/AssessmentPanel.tsx +106 -28
- package/src/components/resource/panels/CommentEntry.tsx +38 -32
- package/src/components/resource/panels/CommentsPanel.tsx +121 -28
- package/src/components/resource/panels/DetectSection.css +36 -1
- package/src/components/resource/panels/DetectSection.tsx +49 -15
- package/src/components/resource/panels/HighlightEntry.tsx +25 -33
- package/src/components/resource/panels/HighlightPanel.tsx +100 -25
- package/src/components/resource/panels/ReferenceEntry.tsx +61 -75
- package/src/components/resource/panels/ReferencesPanel.tsx +134 -42
- package/src/components/resource/panels/ResourceInfoPanel.tsx +47 -48
- package/src/components/resource/panels/TagEntry.tsx +25 -33
- package/src/components/resource/panels/TaggingPanel.tsx +118 -30
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +30 -92
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +129 -110
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +86 -78
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +144 -149
- package/src/components/resource/panels/__tests__/DetectSection.test.tsx +480 -0
- package/src/components/resource/panels/__tests__/HighlightPanel.detectionProgress.test.tsx +362 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +226 -111
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +117 -61
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +128 -106
- package/src/components/settings/SettingsPanel.tsx +15 -12
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +1 -46
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -9
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -3
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -9
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -3
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -9
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -3
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -9
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +0 -32
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -9
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +0 -32
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +51 -54
- package/src/features/resource-compose/components/ResourceComposePage.tsx +3 -13
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +39 -45
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +9 -13
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +234 -0
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +388 -0
- package/src/features/resource-viewer/__tests__/DetectionProgressDismissal.test.tsx +318 -0
- package/src/features/resource-viewer/__tests__/GenerationFlowIntegration.test.tsx +503 -0
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +139 -93
- package/src/features/resource-viewer/__tests__/detection-progress-flow.test.tsx +322 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +341 -524
- package/translations/ar.json +6 -3
- package/translations/bn.json +6 -3
- package/translations/cs.json +6 -3
- package/translations/da.json +6 -3
- package/translations/de.json +6 -3
- package/translations/el.json +6 -3
- package/translations/en.json +6 -3
- package/translations/es.json +6 -3
- package/translations/fa.json +6 -3
- package/translations/fi.json +6 -3
- package/translations/fr.json +6 -3
- package/translations/he.json +6 -3
- package/translations/hi.json +6 -3
- package/translations/id.json +6 -3
- package/translations/it.json +6 -3
- package/translations/ja.json +6 -3
- package/translations/ko.json +6 -3
- package/translations/ms.json +6 -3
- package/translations/nl.json +6 -3
- package/translations/no.json +6 -3
- package/translations/pl.json +6 -3
- package/translations/pt.json +6 -3
- package/translations/ro.json +6 -3
- package/translations/sv.json +6 -3
- package/translations/th.json +6 -3
- package/translations/tr.json +6 -3
- package/translations/uk.json +6 -3
- package/translations/vi.json +6 -3
- package/translations/zh.json +6 -3
- package/dist/PdfAnnotationCanvas.client-ADC4FFSE.mjs.map +0 -1
- package/dist/TranslationManager-Co_5fSxl.d.mts +0 -118
- package/dist/ar-EMHEHPCJ.mjs.map +0 -1
- package/dist/bn-OVCI4F6X.mjs.map +0 -1
- package/dist/chunk-JZIO2A3B.mjs.map +0 -1
- package/dist/chunk-LIHZTECW.mjs.map +0 -1
- package/dist/cs-FAN66Q2F.mjs.map +0 -1
- package/dist/da-YBBIHI2O.mjs.map +0 -1
- package/dist/de-MAYU33LB.mjs.map +0 -1
- package/dist/el-MKGSWN4O.mjs.map +0 -1
- package/dist/es-52LHUWJD.mjs.map +0 -1
- package/dist/fa-FJICRANB.mjs.map +0 -1
- package/dist/fi-O455XFCR.mjs.map +0 -1
- package/dist/fr-TXIXHOOE.mjs.map +0 -1
- package/dist/he-JBSOX5IN.mjs.map +0 -1
- package/dist/hi-KGHI3XVT.mjs.map +0 -1
- package/dist/id-5OCPPZLO.mjs.map +0 -1
- package/dist/it-PNBBZSM2.mjs.map +0 -1
- package/dist/ja-LDD7R3TJ.mjs.map +0 -1
- package/dist/ko-F47ZDEY3.mjs.map +0 -1
- package/dist/ms-Z7LMXJWL.mjs.map +0 -1
- package/dist/nl-6SJFBPJ3.mjs.map +0 -1
- package/dist/no-YXPBPSGF.mjs.map +0 -1
- package/dist/pl-P4AZ2QME.mjs.map +0 -1
- package/dist/pt-LHWUS6U6.mjs.map +0 -1
- package/dist/ro-EA5J2ZON.mjs.map +0 -1
- package/dist/sv-DATBS3UQ.mjs.map +0 -1
- package/dist/th-WTFJRWPT.mjs.map +0 -1
- package/dist/tr-IKO3RXOX.mjs.map +0 -1
- package/dist/uk-CF6CTTRK.mjs.map +0 -1
- package/dist/vi-AJLTXPZQ.mjs.map +0 -1
- package/dist/zh-U3ORHHYH.mjs.map +0 -1
- /package/dist/{en-DDLIXJCU.mjs.map → en-KJCJQ4OO.mjs.map} +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
|
3
|
-
import { vi } from 'vitest';
|
|
3
|
+
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
|
4
4
|
import { NextIntlClientProvider } from 'next-intl';
|
|
5
5
|
import { AnnotateToolbar, type SelectionMotivation, type ClickAction } from '../AnnotateToolbar';
|
|
6
6
|
import { ANNOTATORS } from '../../../lib/annotation-registry';
|
|
7
|
+
import { EventBusProvider, resetEventBusForTesting, useEventBus } from '../../../contexts/EventBusContext';
|
|
7
8
|
|
|
8
9
|
// Mock translations
|
|
9
10
|
const messages = {
|
|
@@ -29,11 +30,75 @@ const messages = {
|
|
|
29
30
|
}
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Composition-based event tracker
|
|
34
|
+
interface TrackedEvent {
|
|
35
|
+
event: string;
|
|
36
|
+
payload: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createEventTracker() {
|
|
40
|
+
const events: TrackedEvent[] = [];
|
|
41
|
+
|
|
42
|
+
function EventTrackingWrapper({ children }: { children: React.ReactNode }) {
|
|
43
|
+
const eventBus = useEventBus();
|
|
44
|
+
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const handlers: Array<() => void> = [];
|
|
47
|
+
|
|
48
|
+
// Track toolbar-related events
|
|
49
|
+
const trackEvent = (eventName: string) => (payload: any) => {
|
|
50
|
+
events.push({ event: eventName, payload });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const toolbarEvents = [
|
|
54
|
+
'view:mode-toggled',
|
|
55
|
+
'toolbar:click-changed',
|
|
56
|
+
'toolbar:selection-changed',
|
|
57
|
+
'toolbar:shape-changed',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
toolbarEvents.forEach(eventName => {
|
|
61
|
+
const handler = trackEvent(eventName);
|
|
62
|
+
eventBus.on(eventName, handler);
|
|
63
|
+
handlers.push(() => eventBus.off(eventName, handler));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return () => {
|
|
67
|
+
handlers.forEach(cleanup => cleanup());
|
|
68
|
+
};
|
|
69
|
+
}, [eventBus]);
|
|
70
|
+
|
|
71
|
+
return <>{children}</>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
EventTrackingWrapper,
|
|
76
|
+
events,
|
|
77
|
+
clear: () => {
|
|
78
|
+
events.length = 0;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const renderWithIntl = (component: React.ReactElement, tracker?: ReturnType<typeof createEventTracker>) => {
|
|
84
|
+
if (tracker) {
|
|
85
|
+
return render(
|
|
86
|
+
<EventBusProvider>
|
|
87
|
+
<NextIntlClientProvider locale="en" messages={messages}>
|
|
88
|
+
<tracker.EventTrackingWrapper>
|
|
89
|
+
{component}
|
|
90
|
+
</tracker.EventTrackingWrapper>
|
|
91
|
+
</NextIntlClientProvider>
|
|
92
|
+
</EventBusProvider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
33
96
|
return render(
|
|
34
|
-
<
|
|
35
|
-
{
|
|
36
|
-
|
|
97
|
+
<EventBusProvider>
|
|
98
|
+
<NextIntlClientProvider locale="en" messages={messages}>
|
|
99
|
+
{component}
|
|
100
|
+
</NextIntlClientProvider>
|
|
101
|
+
</EventBusProvider>
|
|
37
102
|
);
|
|
38
103
|
};
|
|
39
104
|
|
|
@@ -44,11 +109,11 @@ describe('AnnotateToolbar', () => {
|
|
|
44
109
|
onSelectionChange: vi.fn(),
|
|
45
110
|
onClickChange: vi.fn(),
|
|
46
111
|
annotateMode: false,
|
|
47
|
-
onAnnotateModeToggle: vi.fn(),
|
|
48
112
|
annotators: ANNOTATORS
|
|
49
113
|
};
|
|
50
114
|
|
|
51
115
|
beforeEach(() => {
|
|
116
|
+
resetEventBusForTesting();
|
|
52
117
|
vi.clearAllMocks();
|
|
53
118
|
});
|
|
54
119
|
|
|
@@ -113,19 +178,19 @@ describe('AnnotateToolbar', () => {
|
|
|
113
178
|
<AnnotateToolbar
|
|
114
179
|
{...defaultProps}
|
|
115
180
|
annotateMode={false}
|
|
116
|
-
onAnnotateModeToggle={vi.fn()}
|
|
117
181
|
/>
|
|
118
182
|
);
|
|
119
183
|
expect(screen.getByText('Browse')).toBeInTheDocument();
|
|
120
184
|
|
|
121
185
|
rerender(
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
<EventBusProvider>
|
|
187
|
+
<NextIntlClientProvider locale="en" messages={messages}>
|
|
188
|
+
<AnnotateToolbar
|
|
189
|
+
{...defaultProps}
|
|
190
|
+
annotateMode={true}
|
|
191
|
+
/>
|
|
192
|
+
</NextIntlClientProvider>
|
|
193
|
+
</EventBusProvider>
|
|
129
194
|
);
|
|
130
195
|
expect(screen.getByText('Annotate')).toBeInTheDocument();
|
|
131
196
|
});
|
|
@@ -135,7 +200,6 @@ describe('AnnotateToolbar', () => {
|
|
|
135
200
|
<AnnotateToolbar
|
|
136
201
|
{...defaultProps}
|
|
137
202
|
annotateMode={false}
|
|
138
|
-
onAnnotateModeToggle={vi.fn()}
|
|
139
203
|
/>
|
|
140
204
|
);
|
|
141
205
|
|
|
@@ -151,14 +215,14 @@ describe('AnnotateToolbar', () => {
|
|
|
151
215
|
});
|
|
152
216
|
});
|
|
153
217
|
|
|
154
|
-
it('
|
|
155
|
-
const
|
|
218
|
+
it('emits view:mode-toggled event when Browse is clicked in Annotate mode', async () => {
|
|
219
|
+
const tracker = createEventTracker();
|
|
156
220
|
renderWithIntl(
|
|
157
221
|
<AnnotateToolbar
|
|
158
222
|
{...defaultProps}
|
|
159
223
|
annotateMode={true}
|
|
160
|
-
|
|
161
|
-
|
|
224
|
+
/>,
|
|
225
|
+
tracker
|
|
162
226
|
);
|
|
163
227
|
|
|
164
228
|
const modeGroup = screen.getByLabelText('Mode');
|
|
@@ -169,17 +233,19 @@ describe('AnnotateToolbar', () => {
|
|
|
169
233
|
fireEvent.click(browseButton);
|
|
170
234
|
});
|
|
171
235
|
|
|
172
|
-
|
|
236
|
+
await waitFor(() => {
|
|
237
|
+
expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
|
|
238
|
+
});
|
|
173
239
|
});
|
|
174
240
|
|
|
175
|
-
it('
|
|
176
|
-
const
|
|
241
|
+
it('emits view:mode-toggled event when Annotate is clicked in Browse mode', async () => {
|
|
242
|
+
const tracker = createEventTracker();
|
|
177
243
|
renderWithIntl(
|
|
178
244
|
<AnnotateToolbar
|
|
179
245
|
{...defaultProps}
|
|
180
246
|
annotateMode={false}
|
|
181
|
-
|
|
182
|
-
|
|
247
|
+
/>,
|
|
248
|
+
tracker
|
|
183
249
|
);
|
|
184
250
|
|
|
185
251
|
const modeGroup = screen.getByLabelText('Mode');
|
|
@@ -190,17 +256,19 @@ describe('AnnotateToolbar', () => {
|
|
|
190
256
|
fireEvent.click(annotateButton);
|
|
191
257
|
});
|
|
192
258
|
|
|
193
|
-
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
|
|
261
|
+
});
|
|
194
262
|
});
|
|
195
263
|
|
|
196
264
|
it('closes dropdown after selection', async () => {
|
|
197
|
-
const
|
|
265
|
+
const tracker = createEventTracker();
|
|
198
266
|
const { rerender } = renderWithIntl(
|
|
199
267
|
<AnnotateToolbar
|
|
200
268
|
{...defaultProps}
|
|
201
269
|
annotateMode={false}
|
|
202
|
-
|
|
203
|
-
|
|
270
|
+
/>,
|
|
271
|
+
tracker
|
|
204
272
|
);
|
|
205
273
|
|
|
206
274
|
const modeGroup = screen.getByLabelText('Mode');
|
|
@@ -213,18 +281,23 @@ describe('AnnotateToolbar', () => {
|
|
|
213
281
|
const annotateButton = screen.getByText('Annotate');
|
|
214
282
|
fireEvent.click(annotateButton);
|
|
215
283
|
|
|
216
|
-
// Verify the
|
|
217
|
-
|
|
284
|
+
// Verify the event was emitted
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(tracker.events.some(e => e.event === 'view:mode-toggled')).toBe(true);
|
|
287
|
+
});
|
|
218
288
|
|
|
219
289
|
// Simulate mode change by rerendering with new mode
|
|
220
290
|
rerender(
|
|
221
|
-
<
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
291
|
+
<EventBusProvider>
|
|
292
|
+
<NextIntlClientProvider locale="en" messages={messages}>
|
|
293
|
+
<tracker.EventTrackingWrapper>
|
|
294
|
+
<AnnotateToolbar
|
|
295
|
+
{...defaultProps}
|
|
296
|
+
annotateMode={true}
|
|
297
|
+
/>
|
|
298
|
+
</tracker.EventTrackingWrapper>
|
|
299
|
+
</NextIntlClientProvider>
|
|
300
|
+
</EventBusProvider>
|
|
228
301
|
);
|
|
229
302
|
|
|
230
303
|
// After mode change, the collapsed content should show "Annotate"
|
|
@@ -238,10 +311,11 @@ describe('AnnotateToolbar', () => {
|
|
|
238
311
|
});
|
|
239
312
|
|
|
240
313
|
describe('CLICK Group Interactions', () => {
|
|
241
|
-
it('
|
|
242
|
-
const
|
|
314
|
+
it('emits toolbar:click-changed event when clicking an action', async () => {
|
|
315
|
+
const tracker = createEventTracker();
|
|
243
316
|
renderWithIntl(
|
|
244
|
-
<AnnotateToolbar {...defaultProps}
|
|
317
|
+
<AnnotateToolbar {...defaultProps} />,
|
|
318
|
+
tracker
|
|
245
319
|
);
|
|
246
320
|
|
|
247
321
|
const clickGroup = screen.getByLabelText('Click');
|
|
@@ -251,8 +325,13 @@ describe('AnnotateToolbar', () => {
|
|
|
251
325
|
expect(screen.getByText('Follow')).toBeInTheDocument();
|
|
252
326
|
});
|
|
253
327
|
|
|
328
|
+
tracker.clear();
|
|
254
329
|
fireEvent.click(screen.getByText('Follow'));
|
|
255
|
-
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(tracker.events.some(e =>
|
|
332
|
+
e.event === 'toolbar:click-changed' && e.payload?.action === 'follow'
|
|
333
|
+
)).toBe(true);
|
|
334
|
+
});
|
|
256
335
|
});
|
|
257
336
|
|
|
258
337
|
it('displays selected action', () => {
|
|
@@ -264,10 +343,11 @@ describe('AnnotateToolbar', () => {
|
|
|
264
343
|
});
|
|
265
344
|
|
|
266
345
|
describe('MOTIVATION Group Interactions', () => {
|
|
267
|
-
it('
|
|
268
|
-
const
|
|
346
|
+
it('emits toolbar:selection-changed event when clicking a motivation', async () => {
|
|
347
|
+
const tracker = createEventTracker();
|
|
269
348
|
renderWithIntl(
|
|
270
|
-
<AnnotateToolbar {...defaultProps}
|
|
349
|
+
<AnnotateToolbar {...defaultProps} />,
|
|
350
|
+
tracker
|
|
271
351
|
);
|
|
272
352
|
|
|
273
353
|
const motivationGroup = screen.getByLabelText('Motivation');
|
|
@@ -277,18 +357,23 @@ describe('AnnotateToolbar', () => {
|
|
|
277
357
|
expect(screen.getByText('Reference')).toBeInTheDocument();
|
|
278
358
|
});
|
|
279
359
|
|
|
360
|
+
tracker.clear();
|
|
280
361
|
fireEvent.click(screen.getByText('Reference'));
|
|
281
|
-
|
|
362
|
+
await waitFor(() => {
|
|
363
|
+
expect(tracker.events.some(e =>
|
|
364
|
+
e.event === 'toolbar:selection-changed' && e.payload?.motivation === 'linking'
|
|
365
|
+
)).toBe(true);
|
|
366
|
+
});
|
|
282
367
|
});
|
|
283
368
|
|
|
284
369
|
it('toggles motivation on/off', async () => {
|
|
285
|
-
const
|
|
370
|
+
const tracker = createEventTracker();
|
|
286
371
|
const { rerender } = renderWithIntl(
|
|
287
372
|
<AnnotateToolbar
|
|
288
373
|
{...defaultProps}
|
|
289
374
|
selectedMotivation={null}
|
|
290
|
-
|
|
291
|
-
|
|
375
|
+
/>,
|
|
376
|
+
tracker
|
|
292
377
|
);
|
|
293
378
|
|
|
294
379
|
const motivationGroup = screen.getByLabelText('Motivation');
|
|
@@ -300,18 +385,26 @@ describe('AnnotateToolbar', () => {
|
|
|
300
385
|
});
|
|
301
386
|
|
|
302
387
|
const dropdown = screen.getByRole('menu');
|
|
388
|
+
tracker.clear();
|
|
303
389
|
fireEvent.click(within(dropdown).getByText('Highlight'));
|
|
304
|
-
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(tracker.events.some(e =>
|
|
392
|
+
e.event === 'toolbar:selection-changed' && e.payload?.motivation === 'highlighting'
|
|
393
|
+
)).toBe(true);
|
|
394
|
+
});
|
|
305
395
|
|
|
306
396
|
// Simulate selection
|
|
307
397
|
rerender(
|
|
308
|
-
<
|
|
309
|
-
<
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
398
|
+
<EventBusProvider>
|
|
399
|
+
<NextIntlClientProvider locale="en" messages={messages}>
|
|
400
|
+
<tracker.EventTrackingWrapper>
|
|
401
|
+
<AnnotateToolbar
|
|
402
|
+
{...defaultProps}
|
|
403
|
+
selectedMotivation="highlighting"
|
|
404
|
+
/>
|
|
405
|
+
</tracker.EventTrackingWrapper>
|
|
406
|
+
</NextIntlClientProvider>
|
|
407
|
+
</EventBusProvider>
|
|
315
408
|
);
|
|
316
409
|
|
|
317
410
|
// Click again to deselect
|
|
@@ -321,21 +414,26 @@ describe('AnnotateToolbar', () => {
|
|
|
321
414
|
expect(within(dropdown).getByText('Highlight')).toBeInTheDocument();
|
|
322
415
|
});
|
|
323
416
|
const dropdown2 = screen.getByRole('menu');
|
|
417
|
+
tracker.clear();
|
|
324
418
|
fireEvent.click(within(dropdown2).getByText('Highlight'));
|
|
325
|
-
|
|
419
|
+
await waitFor(() => {
|
|
420
|
+
expect(tracker.events.some(e =>
|
|
421
|
+
e.event === 'toolbar:selection-changed' && e.payload?.motivation === null
|
|
422
|
+
)).toBe(true);
|
|
423
|
+
});
|
|
326
424
|
});
|
|
327
425
|
});
|
|
328
426
|
|
|
329
427
|
describe('SHAPE Group Interactions', () => {
|
|
330
|
-
it('
|
|
331
|
-
const
|
|
428
|
+
it('emits toolbar:shape-changed event when clicking a shape', async () => {
|
|
429
|
+
const tracker = createEventTracker();
|
|
332
430
|
renderWithIntl(
|
|
333
431
|
<AnnotateToolbar
|
|
334
432
|
{...defaultProps}
|
|
335
433
|
showShapeGroup={true}
|
|
336
434
|
selectedShape="rectangle"
|
|
337
|
-
|
|
338
|
-
|
|
435
|
+
/>,
|
|
436
|
+
tracker
|
|
339
437
|
);
|
|
340
438
|
|
|
341
439
|
const shapeGroup = screen.getByLabelText('Shape');
|
|
@@ -345,8 +443,13 @@ describe('AnnotateToolbar', () => {
|
|
|
345
443
|
expect(screen.getByText('Circle')).toBeInTheDocument();
|
|
346
444
|
});
|
|
347
445
|
|
|
446
|
+
tracker.clear();
|
|
348
447
|
fireEvent.click(screen.getByText('Circle'));
|
|
349
|
-
|
|
448
|
+
await waitFor(() => {
|
|
449
|
+
expect(tracker.events.some(e =>
|
|
450
|
+
e.event === 'toolbar:shape-changed' && e.payload?.shape === 'circle'
|
|
451
|
+
)).toBe(true);
|
|
452
|
+
});
|
|
350
453
|
});
|
|
351
454
|
});
|
|
352
455
|
|
|
@@ -356,7 +459,6 @@ describe('AnnotateToolbar', () => {
|
|
|
356
459
|
<AnnotateToolbar
|
|
357
460
|
{...defaultProps}
|
|
358
461
|
annotateMode={false}
|
|
359
|
-
onAnnotateModeToggle={vi.fn()}
|
|
360
462
|
/>
|
|
361
463
|
);
|
|
362
464
|
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
z-index: 10;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/* Pulse effect for hover synchronization */
|
|
32
|
+
.semiont-annotation-pulse {
|
|
33
|
+
background: var(--semiont-bg-hover) !important;
|
|
34
|
+
transition: background 0.3s ease-in-out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[data-theme="dark"] .semiont-annotation-pulse {
|
|
38
|
+
background: var(--semiont-bg-hover) !important;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/* Motivation-specific styles are handled in /styles/motivations/*.css */
|
|
32
42
|
|
|
33
43
|
/* Entry structure */
|
|
@@ -22,17 +22,23 @@ export function JsonLdView({ annotation, onBack }: JsonLdViewProps) {
|
|
|
22
22
|
const viewRef = useRef<EditorView | null>(null);
|
|
23
23
|
const { showLineNumbers } = useLineNumbers();
|
|
24
24
|
|
|
25
|
+
// Store callback in ref to avoid including in dependency arrays
|
|
26
|
+
const onBackRef = useRef(onBack);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
onBackRef.current = onBack;
|
|
29
|
+
});
|
|
30
|
+
|
|
25
31
|
// Handle escape key
|
|
26
32
|
useEffect(() => {
|
|
27
33
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
28
34
|
if (e.key === 'Escape') {
|
|
29
|
-
|
|
35
|
+
onBackRef.current();
|
|
30
36
|
}
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
window.addEventListener('keydown', handleKeyDown);
|
|
34
40
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
35
|
-
}, [
|
|
41
|
+
}, []);
|
|
36
42
|
|
|
37
43
|
// Initialize CodeMirror
|
|
38
44
|
useEffect(() => {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useRef } from 'react';
|
|
3
4
|
import type { components } from '@semiont/api-client';
|
|
4
5
|
import { getSvgSelector, isHighlight, isReference, isAssessment, isComment, isTag, isBodyResolved, isResolvedReference } from '@semiont/api-client';
|
|
5
6
|
import { parseSvgSelector } from '@semiont/api-client';
|
|
7
|
+
import type { EventBus } from '../../contexts/EventBusContext';
|
|
6
8
|
|
|
7
9
|
type Annotation = components['schemas']['Annotation'];
|
|
8
10
|
|
|
@@ -12,8 +14,7 @@ interface AnnotationOverlayProps {
|
|
|
12
14
|
imageHeight: number;
|
|
13
15
|
displayWidth: number;
|
|
14
16
|
displayHeight: number;
|
|
15
|
-
|
|
16
|
-
onAnnotationHover?: (annotationId: string | null) => void;
|
|
17
|
+
eventBus?: EventBus;
|
|
17
18
|
hoveredAnnotationId?: string | null;
|
|
18
19
|
selectedAnnotationId?: string | null;
|
|
19
20
|
}
|
|
@@ -59,6 +60,9 @@ function getAnnotationTooltip(annotation: Annotation): string {
|
|
|
59
60
|
|
|
60
61
|
/**
|
|
61
62
|
* Render annotation overlay - displays existing annotations as SVG shapes
|
|
63
|
+
*
|
|
64
|
+
* @emits annotation:hover - Annotation hovered or unhovered. Payload: { annotationId: string | null }
|
|
65
|
+
* @emits annotation:click - Annotation clicked. Payload: { annotationId: string, motivation: Motivation }
|
|
62
66
|
*/
|
|
63
67
|
export function AnnotationOverlay({
|
|
64
68
|
annotations,
|
|
@@ -66,14 +70,30 @@ export function AnnotationOverlay({
|
|
|
66
70
|
imageHeight,
|
|
67
71
|
displayWidth,
|
|
68
72
|
displayHeight,
|
|
69
|
-
|
|
70
|
-
onAnnotationHover,
|
|
73
|
+
eventBus,
|
|
71
74
|
hoveredAnnotationId,
|
|
72
75
|
selectedAnnotationId
|
|
73
76
|
}: AnnotationOverlayProps) {
|
|
74
77
|
const scaleX = displayWidth / imageWidth;
|
|
75
78
|
const scaleY = displayHeight / imageHeight;
|
|
76
79
|
|
|
80
|
+
// Track current hover state to prevent redundant emissions
|
|
81
|
+
const currentHover = useRef<string | null>(null);
|
|
82
|
+
|
|
83
|
+
const handleMouseEnter = (annotationId: string) => {
|
|
84
|
+
if (currentHover.current !== annotationId) {
|
|
85
|
+
currentHover.current = annotationId;
|
|
86
|
+
eventBus?.emit('annotation:hover', { annotationId });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleMouseLeave = () => {
|
|
91
|
+
if (currentHover.current !== null) {
|
|
92
|
+
currentHover.current = null;
|
|
93
|
+
eventBus?.emit('annotation:hover', { annotationId: null });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
77
97
|
return (
|
|
78
98
|
<svg
|
|
79
99
|
className="semiont-annotation-overlay"
|
|
@@ -120,9 +140,9 @@ export function AnnotationOverlay({
|
|
|
120
140
|
className="semiont-annotation-overlay__shape"
|
|
121
141
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
122
142
|
data-selected={isSelected ? 'true' : 'false'}
|
|
123
|
-
onClick={() =>
|
|
124
|
-
onMouseEnter={() =>
|
|
125
|
-
onMouseLeave={
|
|
143
|
+
onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
144
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
145
|
+
onMouseLeave={handleMouseLeave}
|
|
126
146
|
/>
|
|
127
147
|
{statusEmoji && (
|
|
128
148
|
<text
|
|
@@ -133,10 +153,10 @@ export function AnnotationOverlay({
|
|
|
133
153
|
style={{ userSelect: 'none' }}
|
|
134
154
|
onClick={(e) => {
|
|
135
155
|
e.stopPropagation();
|
|
136
|
-
|
|
156
|
+
eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
137
157
|
}}
|
|
138
|
-
onMouseEnter={() =>
|
|
139
|
-
onMouseLeave={
|
|
158
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
159
|
+
onMouseLeave={handleMouseLeave}
|
|
140
160
|
>
|
|
141
161
|
{statusEmoji}
|
|
142
162
|
</text>
|
|
@@ -167,9 +187,9 @@ export function AnnotationOverlay({
|
|
|
167
187
|
className="semiont-annotation-overlay__shape"
|
|
168
188
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
169
189
|
data-selected={isSelected ? 'true' : 'false'}
|
|
170
|
-
onClick={() =>
|
|
171
|
-
onMouseEnter={() =>
|
|
172
|
-
onMouseLeave={
|
|
190
|
+
onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
191
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
192
|
+
onMouseLeave={handleMouseLeave}
|
|
173
193
|
/>
|
|
174
194
|
{statusEmoji && (
|
|
175
195
|
<text
|
|
@@ -180,10 +200,10 @@ export function AnnotationOverlay({
|
|
|
180
200
|
style={{ userSelect: 'none' }}
|
|
181
201
|
onClick={(e) => {
|
|
182
202
|
e.stopPropagation();
|
|
183
|
-
|
|
203
|
+
eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
184
204
|
}}
|
|
185
|
-
onMouseEnter={() =>
|
|
186
|
-
onMouseLeave={
|
|
205
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
206
|
+
onMouseLeave={handleMouseLeave}
|
|
187
207
|
>
|
|
188
208
|
{statusEmoji}
|
|
189
209
|
</text>
|
|
@@ -227,9 +247,9 @@ export function AnnotationOverlay({
|
|
|
227
247
|
className="semiont-annotation-overlay__shape"
|
|
228
248
|
data-hovered={isHovered ? 'true' : 'false'}
|
|
229
249
|
data-selected={isSelected ? 'true' : 'false'}
|
|
230
|
-
onClick={() =>
|
|
231
|
-
onMouseEnter={() =>
|
|
232
|
-
onMouseLeave={
|
|
250
|
+
onClick={() => eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation })}
|
|
251
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
252
|
+
onMouseLeave={handleMouseLeave}
|
|
233
253
|
/>
|
|
234
254
|
{statusEmoji && (
|
|
235
255
|
<text
|
|
@@ -240,10 +260,10 @@ export function AnnotationOverlay({
|
|
|
240
260
|
style={{ userSelect: 'none' }}
|
|
241
261
|
onClick={(e) => {
|
|
242
262
|
e.stopPropagation();
|
|
243
|
-
|
|
263
|
+
eventBus?.emit('annotation:click', { annotationId: annotation.id, motivation: annotation.motivation });
|
|
244
264
|
}}
|
|
245
|
-
onMouseEnter={() =>
|
|
246
|
-
onMouseLeave={
|
|
265
|
+
onMouseEnter={() => handleMouseEnter(annotation.id)}
|
|
266
|
+
onMouseLeave={handleMouseLeave}
|
|
247
267
|
>
|
|
248
268
|
{statusEmoji}
|
|
249
269
|
</text>
|