@semiont/react-ui 0.4.14 → 0.4.16

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.
Files changed (169) hide show
  1. package/README.md +18 -12
  2. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +175 -0
  3. package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
  4. package/dist/{ar-R4CRNXEF.mjs → ar-3W37O3R3.mjs} +9 -3
  5. package/dist/ar-3W37O3R3.mjs.map +1 -0
  6. package/dist/{bn-CZKGRHTA.mjs → bn-JZTJLMVE.mjs} +9 -3
  7. package/dist/bn-JZTJLMVE.mjs.map +1 -0
  8. package/dist/chunk-FAI3S4BM.mjs +865 -0
  9. package/dist/chunk-FAI3S4BM.mjs.map +1 -0
  10. package/dist/{chunk-HVMAGUFA.mjs → chunk-NOD3NCXE.mjs} +3 -1
  11. package/dist/chunk-NOD3NCXE.mjs.map +1 -0
  12. package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
  13. package/dist/chunk-OZICDVH7.mjs.map +1 -0
  14. package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
  15. package/dist/chunk-VN5NY4SN.mjs.map +1 -0
  16. package/dist/{cs-4WIB2IHH.mjs → cs-XYHH7HNE.mjs} +9 -3
  17. package/dist/cs-XYHH7HNE.mjs.map +1 -0
  18. package/dist/{da-JWYEUYPX.mjs → da-MZKIECVT.mjs} +9 -3
  19. package/dist/da-MZKIECVT.mjs.map +1 -0
  20. package/dist/{de-GWUQZGER.mjs → de-AYXTMRQW.mjs} +9 -3
  21. package/dist/de-AYXTMRQW.mjs.map +1 -0
  22. package/dist/{el-DM2GT7P5.mjs → el-A6CVQWAW.mjs} +9 -3
  23. package/dist/el-A6CVQWAW.mjs.map +1 -0
  24. package/dist/{en-IUV4ZXKH.mjs → en-YPQQBI4T.mjs} +2 -2
  25. package/dist/{es-6LVQIM3D.mjs → es-M2HXLJGT.mjs} +9 -3
  26. package/dist/es-M2HXLJGT.mjs.map +1 -0
  27. package/dist/{fa-IRUJY3QI.mjs → fa-V6JZJDYP.mjs} +9 -3
  28. package/dist/fa-V6JZJDYP.mjs.map +1 -0
  29. package/dist/{fi-53FBOEVT.mjs → fi-ONDTZ5H7.mjs} +9 -3
  30. package/dist/fi-ONDTZ5H7.mjs.map +1 -0
  31. package/dist/{fr-Q5KY7QL6.mjs → fr-PAPV4H4G.mjs} +9 -3
  32. package/dist/fr-PAPV4H4G.mjs.map +1 -0
  33. package/dist/{he-HJNKULBY.mjs → he-F6VTLJLW.mjs} +9 -3
  34. package/dist/he-F6VTLJLW.mjs.map +1 -0
  35. package/dist/{hi-UYZ4X6CR.mjs → hi-CFUAV4BF.mjs} +9 -3
  36. package/dist/hi-CFUAV4BF.mjs.map +1 -0
  37. package/dist/{id-UAQMH6U2.mjs → id-NBKLCCI7.mjs} +9 -3
  38. package/dist/id-NBKLCCI7.mjs.map +1 -0
  39. package/dist/index.d.mts +141 -169
  40. package/dist/index.mjs +2394 -2116
  41. package/dist/index.mjs.map +1 -1
  42. package/dist/{it-C7QEBNFA.mjs → it-SLSOWVVU.mjs} +9 -3
  43. package/dist/it-SLSOWVVU.mjs.map +1 -0
  44. package/dist/{ja-THS6AOSJ.mjs → ja-L5IG4ECE.mjs} +9 -3
  45. package/dist/ja-L5IG4ECE.mjs.map +1 -0
  46. package/dist/{ko-XKK3TWQG.mjs → ko-QYMTULKK.mjs} +9 -3
  47. package/dist/ko-QYMTULKK.mjs.map +1 -0
  48. package/dist/{ms-GSK7LIF7.mjs → ms-5DGSFKM2.mjs} +9 -3
  49. package/dist/ms-5DGSFKM2.mjs.map +1 -0
  50. package/dist/{nl-KUBWITGY.mjs → nl-VZPCGONO.mjs} +9 -3
  51. package/dist/nl-VZPCGONO.mjs.map +1 -0
  52. package/dist/{no-ECWZUHT6.mjs → no-MF6F352I.mjs} +9 -3
  53. package/dist/no-MF6F352I.mjs.map +1 -0
  54. package/dist/{pl-PLVWSZWS.mjs → pl-WIK72JUO.mjs} +9 -3
  55. package/dist/pl-WIK72JUO.mjs.map +1 -0
  56. package/dist/{pt-AL74ZTKB.mjs → pt-RRP5ZF6A.mjs} +9 -3
  57. package/dist/pt-RRP5ZF6A.mjs.map +1 -0
  58. package/dist/{ro-WTPHLHGS.mjs → ro-XHQLC3T7.mjs} +9 -3
  59. package/dist/ro-XHQLC3T7.mjs.map +1 -0
  60. package/dist/{sv-QCLI7SG4.mjs → sv-EWULDN6E.mjs} +9 -3
  61. package/dist/sv-EWULDN6E.mjs.map +1 -0
  62. package/dist/test-utils.d.mts +13 -62
  63. package/dist/test-utils.mjs +41 -22
  64. package/dist/test-utils.mjs.map +1 -1
  65. package/dist/{th-WCKVZU6U.mjs → th-TGOBHFG4.mjs} +9 -3
  66. package/dist/th-TGOBHFG4.mjs.map +1 -0
  67. package/dist/{tr-2CAFS2XS.mjs → tr-LMMPBMV7.mjs} +9 -3
  68. package/dist/tr-LMMPBMV7.mjs.map +1 -0
  69. package/dist/{uk-TDE4JLCY.mjs → uk-IPGRRJY6.mjs} +9 -3
  70. package/dist/uk-IPGRRJY6.mjs.map +1 -0
  71. package/dist/{vi-KKXZ4PCX.mjs → vi-Q676OJQS.mjs} +9 -3
  72. package/dist/vi-Q676OJQS.mjs.map +1 -0
  73. package/dist/{zh-VH4XN5PV.mjs → zh-F3MTWQDX.mjs} +9 -3
  74. package/dist/zh-F3MTWQDX.mjs.map +1 -0
  75. package/package.json +5 -3
  76. package/src/components/ProtectedErrorBoundary.tsx +95 -0
  77. package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
  78. package/src/components/modals/PermissionDeniedModal.tsx +140 -0
  79. package/src/components/modals/ReferenceWizardModal.tsx +3 -2
  80. package/src/components/modals/SessionExpiredModal.tsx +101 -0
  81. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
  82. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
  83. package/src/components/resource/AnnotationHistory.tsx +5 -6
  84. package/src/components/resource/HistoryEvent.tsx +9 -8
  85. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
  86. package/src/components/resource/__tests__/HistoryEvent.test.tsx +18 -19
  87. package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
  88. package/src/components/resource/event-formatting.ts +92 -56
  89. package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
  90. package/src/components/resource/panels/ResourceInfoPanel.tsx +18 -6
  91. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
  92. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +24 -0
  93. package/src/features/resource-compose/components/ResourceComposePage.tsx +10 -1
  94. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
  95. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
  96. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
  97. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
  98. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
  99. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
  100. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
  101. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
  102. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
  103. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +26 -26
  104. package/src/styles/features/compose.css +63 -0
  105. package/translations/ar.json +6 -2
  106. package/translations/bn.json +6 -2
  107. package/translations/cs.json +6 -2
  108. package/translations/da.json +6 -2
  109. package/translations/de.json +6 -2
  110. package/translations/el.json +6 -2
  111. package/translations/en.json +2 -0
  112. package/translations/es.json +6 -2
  113. package/translations/fa.json +6 -2
  114. package/translations/fi.json +6 -2
  115. package/translations/fr.json +6 -2
  116. package/translations/he.json +6 -2
  117. package/translations/hi.json +6 -2
  118. package/translations/id.json +6 -2
  119. package/translations/it.json +6 -2
  120. package/translations/ja.json +6 -2
  121. package/translations/ko.json +6 -2
  122. package/translations/ms.json +6 -2
  123. package/translations/nl.json +6 -2
  124. package/translations/no.json +6 -2
  125. package/translations/pl.json +6 -2
  126. package/translations/pt.json +6 -2
  127. package/translations/ro.json +6 -2
  128. package/translations/sv.json +6 -2
  129. package/translations/th.json +6 -2
  130. package/translations/tr.json +6 -2
  131. package/translations/uk.json +6 -2
  132. package/translations/vi.json +6 -2
  133. package/translations/zh.json +6 -2
  134. package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
  135. package/dist/ar-R4CRNXEF.mjs.map +0 -1
  136. package/dist/bn-CZKGRHTA.mjs.map +0 -1
  137. package/dist/chunk-BQJWOK4C.mjs.map +0 -1
  138. package/dist/chunk-HNZOXH4L.mjs.map +0 -1
  139. package/dist/chunk-HVMAGUFA.mjs.map +0 -1
  140. package/dist/chunk-OL5UST25.mjs +0 -413
  141. package/dist/chunk-OL5UST25.mjs.map +0 -1
  142. package/dist/cs-4WIB2IHH.mjs.map +0 -1
  143. package/dist/da-JWYEUYPX.mjs.map +0 -1
  144. package/dist/de-GWUQZGER.mjs.map +0 -1
  145. package/dist/el-DM2GT7P5.mjs.map +0 -1
  146. package/dist/es-6LVQIM3D.mjs.map +0 -1
  147. package/dist/fa-IRUJY3QI.mjs.map +0 -1
  148. package/dist/fi-53FBOEVT.mjs.map +0 -1
  149. package/dist/fr-Q5KY7QL6.mjs.map +0 -1
  150. package/dist/he-HJNKULBY.mjs.map +0 -1
  151. package/dist/hi-UYZ4X6CR.mjs.map +0 -1
  152. package/dist/id-UAQMH6U2.mjs.map +0 -1
  153. package/dist/it-C7QEBNFA.mjs.map +0 -1
  154. package/dist/ja-THS6AOSJ.mjs.map +0 -1
  155. package/dist/ko-XKK3TWQG.mjs.map +0 -1
  156. package/dist/ms-GSK7LIF7.mjs.map +0 -1
  157. package/dist/nl-KUBWITGY.mjs.map +0 -1
  158. package/dist/no-ECWZUHT6.mjs.map +0 -1
  159. package/dist/pl-PLVWSZWS.mjs.map +0 -1
  160. package/dist/pt-AL74ZTKB.mjs.map +0 -1
  161. package/dist/ro-WTPHLHGS.mjs.map +0 -1
  162. package/dist/sv-QCLI7SG4.mjs.map +0 -1
  163. package/dist/th-WCKVZU6U.mjs.map +0 -1
  164. package/dist/tr-2CAFS2XS.mjs.map +0 -1
  165. package/dist/uk-TDE4JLCY.mjs.map +0 -1
  166. package/dist/vi-KKXZ4PCX.mjs.map +0 -1
  167. package/dist/zh-VH4XN5PV.mjs.map +0 -1
  168. /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
  169. /package/dist/{en-IUV4ZXKH.mjs.map → en-YPQQBI4T.mjs.map} +0 -0
@@ -3,16 +3,18 @@
3
3
  import { useTranslations } from '../../../contexts/TranslationContext';
4
4
  import { useEventBus } from '../../../contexts/EventBusContext';
5
5
  import { formatLocaleDisplay } from '@semiont/api-client';
6
- import type { components } from '@semiont/core';
6
+ import { resourceId as makeResourceId, type components } from '@semiont/core';
7
7
  import './ResourceInfoPanel.css';
8
8
 
9
9
  type Agent = components['schemas']['Agent'];
10
10
 
11
11
  interface Props {
12
+ resourceId: string;
12
13
  documentEntityTypes: string[];
13
14
  documentLocale?: string | undefined;
14
15
  primaryMediaType?: string | undefined;
15
16
  primaryByteSize?: number | undefined;
17
+ storageUri?: string | undefined;
16
18
  isArchived?: boolean;
17
19
  dateCreated?: string | undefined;
18
20
  dateModified?: string | undefined;
@@ -25,15 +27,17 @@ interface Props {
25
27
  /**
26
28
  * Panel for displaying resource metadata and management actions
27
29
  *
28
- * @emits yield:clone - Clone this resource. Payload: undefined
29
- * @emits mark:unarchive - Unarchive this resource. Payload: undefined
30
- * @emits mark:archive - Archive this resource. Payload: undefined
30
+ * @emits yield:clone - Clone this resource
31
+ * @emits mark:unarchive - Unarchive this resource
32
+ * @emits mark:archive - Archive this resource
31
33
  */
32
34
  export function ResourceInfoPanel({
35
+ resourceId,
33
36
  documentEntityTypes,
34
37
  documentLocale,
35
38
  primaryMediaType,
36
39
  primaryByteSize,
40
+ storageUri,
37
41
  isArchived = false,
38
42
  dateCreated,
39
43
  dateModified,
@@ -87,6 +91,14 @@ export function ResourceInfoPanel({
87
91
  </span>
88
92
  </div>
89
93
  )}
94
+ {storageUri && (
95
+ <div>
96
+ <span className="semiont-resource-info-panel__label">{t('storageUri')}</span>
97
+ <span className="semiont-resource-info-panel__value">
98
+ {storageUri}
99
+ </span>
100
+ </div>
101
+ )}
90
102
  </div>
91
103
  </div>
92
104
  )}
@@ -194,7 +206,7 @@ export function ResourceInfoPanel({
194
206
  {isArchived ? (
195
207
  <>
196
208
  <button
197
- onClick={() => eventBus.get('mark:unarchive').next(undefined)}
209
+ onClick={() => eventBus.get('mark:unarchive').next({ resourceId: makeResourceId(resourceId) })}
198
210
  className="semiont-resource-button semiont-resource-button--secondary"
199
211
  >
200
212
  📤 {t('unarchive')}
@@ -206,7 +218,7 @@ export function ResourceInfoPanel({
206
218
  ) : (
207
219
  <>
208
220
  <button
209
- onClick={() => eventBus.get('mark:archive').next(undefined)}
221
+ onClick={() => eventBus.get('mark:archive').next({ resourceId: makeResourceId(resourceId) })}
210
222
  className="semiont-resource-button semiont-resource-button--archive"
211
223
  >
212
224
  📦 {t('archive')}
@@ -5,6 +5,7 @@ import '@testing-library/jest-dom';
5
5
  import { renderWithProviders } from '../../../../test-utils';
6
6
  import userEvent from '@testing-library/user-event';
7
7
  import type { components } from '@semiont/core';
8
+ import { SemiontApiClient } from '@semiont/api-client';
8
9
  import type { RouteBuilder } from '../../../../contexts/RoutingContext';
9
10
 
10
11
  type Annotation = components['schemas']['Annotation'];
@@ -303,28 +304,27 @@ describe('ReferenceEntry', () => {
303
304
  expect(unlinkButton).not.toBeInTheDocument();
304
305
  });
305
306
 
306
- it('should emit bind:update-body on unlink click', async () => {
307
+ it('should call client.bind.body on unlink click', async () => {
307
308
  mockIsBodyResolved.mockReturnValue(true);
308
309
  mockGetBodySource.mockReturnValue('linked-doc');
309
- const unlinkHandler = vi.fn();
310
310
 
311
- const { container, eventBus } = renderWithProviders(
311
+ const bindSpy = vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockResolvedValue({ correlationId: 'c1' });
312
+
313
+ const { container } = renderWithProviders(
312
314
  <ReferenceEntry {...defaultProps} annotateMode={true} />,
313
- { returnEventBus: true }
314
315
  );
315
316
 
316
- const subscription = eventBus!.get('bind:update-body').subscribe(unlinkHandler);
317
-
318
317
  const unlinkButton = container.querySelector('.semiont-reference-unlink')!;
319
318
  await userEvent.click(unlinkButton);
320
319
 
321
- expect(unlinkHandler).toHaveBeenCalledWith({
322
- annotationId: 'ref-1',
323
- resourceId: 'resource-1',
324
- operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }],
325
- });
320
+ expect(bindSpy).toHaveBeenCalledWith(
321
+ 'resource-1',
322
+ 'ref-1',
323
+ { operations: [{ op: 'remove', item: { type: 'SpecificResource', source: 'linked-doc' } }] },
324
+ expect.anything(),
325
+ );
326
326
 
327
- subscription.unsubscribe();
327
+ bindSpy.mockRestore();
328
328
  });
329
329
  });
330
330
 
@@ -15,6 +15,7 @@ vi.mock('../../../../contexts/TranslationContext', () => ({
15
15
  representation: 'Representation',
16
16
  mediaType: 'Media Type',
17
17
  byteSize: 'Size',
18
+ storageUri: 'Storage',
18
19
  clone: 'Clone',
19
20
  cloneDescription: 'Generate a shareable clone link for this resource',
20
21
  archive: 'Archive',
@@ -122,6 +123,7 @@ const renderWithEventBus = (component: React.ReactElement, tracker?: ReturnType<
122
123
 
123
124
  describe('ResourceInfoPanel Component', () => {
124
125
  const defaultProps = {
126
+ resourceId: 'test-resource-id',
125
127
  documentEntityTypes: [],
126
128
  documentLocale: undefined,
127
129
  primaryMediaType: undefined,
@@ -194,6 +196,28 @@ describe('ResourceInfoPanel Component', () => {
194
196
  expect(screen.getByText('1,024 bytes')).toBeInTheDocument();
195
197
  });
196
198
 
199
+ it('should render storageUri when provided', () => {
200
+ renderWithEventBus(
201
+ <ResourceInfoPanel
202
+ {...defaultProps}
203
+ primaryMediaType="text/markdown"
204
+ storageUri="file://docs/overview.md"
205
+ />
206
+ );
207
+ expect(screen.getByText('Storage')).toBeInTheDocument();
208
+ expect(screen.getByText('file://docs/overview.md')).toBeInTheDocument();
209
+ });
210
+
211
+ it('should not render storageUri when absent', () => {
212
+ renderWithEventBus(
213
+ <ResourceInfoPanel
214
+ {...defaultProps}
215
+ primaryMediaType="text/markdown"
216
+ />
217
+ );
218
+ expect(screen.queryByText('Storage')).not.toBeInTheDocument();
219
+ });
220
+
197
221
  it('should not render representation section when neither media type nor byte size provided', () => {
198
222
  renderWithEventBus(
199
223
  <ResourceInfoPanel
@@ -339,7 +339,9 @@ export function ResourceComposePage({
339
339
 
340
340
  {/* Create Form */}
341
341
  <div className="semiont-form">
342
- <form onSubmit={handleSaveResource} className="semiont-form__fields">
342
+ <form onSubmit={handleSaveResource} className="semiont-form__fields semiont-compose-grid">
343
+ {/* Left column: metadata */}
344
+ <div className="semiont-compose-grid__meta">
343
345
  {/* Name */}
344
346
  <div className="semiont-form__field">
345
347
  <label htmlFor="docName" className="semiont-form__label">
@@ -468,6 +470,11 @@ export function ResourceComposePage({
468
470
  </select>
469
471
  </div>
470
472
 
473
+ </div>{/* end semiont-compose-grid__meta */}
474
+
475
+ {/* Right column: content */}
476
+ <div className="semiont-compose-grid__content">
477
+
471
478
  {/* Content Source Toggle - only show for new resources */}
472
479
  {!isClone && !isReferenceCompletion && (
473
480
  <div className="semiont-form__field">
@@ -657,6 +664,8 @@ export function ResourceComposePage({
657
664
  </div>
658
665
  )}
659
666
 
667
+ </div>{/* end semiont-compose-grid__content */}
668
+
660
669
  {/* Action Buttons */}
661
670
  <div className="semiont-form__actions">
662
671
  <button
@@ -248,7 +248,7 @@ describe('Annotation creation clears pendingAnnotation', () => {
248
248
  const createdListener = vi.fn();
249
249
  // Set listener after first render so eventBus is captured
250
250
  await waitFor(() => expect(getEventBus()).toBeDefined());
251
- const subscription = getEventBus().get('mark:created').subscribe(createdListener);
251
+ const subscription = getEventBus().get('mark:create-ok').subscribe(createdListener);
252
252
 
253
253
  act(() => {
254
254
  emit('mark:requested', { selector: TEXT_SELECTOR, motivation: 'linking' });
@@ -27,6 +27,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
27
27
  import { render, waitFor } from '@testing-library/react';
28
28
  import { act } from 'react';
29
29
  import { useMarkFlow } from '../../../hooks/useMarkFlow';
30
+ import { useStoreTokenSync } from '../../../hooks/useStoreTokenSync';
30
31
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
31
32
 
32
33
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -69,8 +70,7 @@ describe('Annotation Deletion - Feature Integration', () => {
69
70
 
70
71
  function TestComponent() {
71
72
  eventBusInstance = useEventBus();
72
- // useMarkFlow is the single registration point for useBindFlow
73
- // (handles mark:delete mark:create annotate:detect-request, etc.)
73
+ useStoreTokenSync(); // Syncs auth token to namespace getToken
74
74
  useMarkFlow(testId);
75
75
  return null;
76
76
  }
@@ -138,7 +138,7 @@ describe('Annotation Deletion - Feature Integration', () => {
138
138
  const deletedListener = vi.fn();
139
139
 
140
140
  // Subscribe to success event
141
- eventBus.get('mark:deleted').subscribe(deletedListener);
141
+ eventBus.get('mark:delete-ok').subscribe(deletedListener);
142
142
 
143
143
  emitDelete('annotation-789');
144
144
 
@@ -175,7 +175,7 @@ describe('Annotation Deletion - Feature Integration', () => {
175
175
  // Verify failure event was emitted
176
176
  await waitFor(() => {
177
177
  expect(failedListener).toHaveBeenCalledWith({
178
- error: expect.any(Error),
178
+ message: expect.any(String),
179
179
  });
180
180
  });
181
181
  });
@@ -25,7 +25,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
25
25
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
26
26
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
27
27
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
28
- import { SSEClient } from '@semiont/api-client';
28
+ import { SemiontApiClient } from '@semiont/api-client';
29
29
  import { resourceId } from '@semiont/core';
30
30
 
31
31
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
@@ -39,20 +39,15 @@ vi.mock('../../../components/Toast', () => ({
39
39
  }));
40
40
 
41
41
  describe('Detection Progress Dismissal Bug', () => {
42
- let mockStream: any;
43
42
  const rUri = resourceId('test');
44
43
 
45
44
  beforeEach(() => {
46
45
  vi.clearAllMocks();
47
46
 
48
- mockStream = {
49
- close: vi.fn(),
50
- };
51
-
52
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream);
53
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream);
54
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream);
55
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream);
47
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
48
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
50
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
56
51
  });
57
52
 
58
53
  afterEach(() => {
@@ -2,14 +2,14 @@
2
2
  * Layer 3: Feature Integration Test - Bind Flow (body update)
3
3
  *
4
4
  * Tests the write side of useBindFlow:
5
- * - bind:update-body → calls bindAnnotation API
6
- * - bind:update-body → emits bind:body-updated on success
5
+ * - bind:update-body → calls http.bindAnnotation API (plain POST)
7
6
  * - bind:update-body → emits bind:body-update-failed on error
8
7
  * - auth token passed to bindAnnotation
9
8
  *
10
- * The wizard modal (ReferenceWizardModal) handles modal state, context
11
- * gathering, search configuration, and result display. This test covers
12
- * only the downstream API calls after the wizard emits bind:update-body.
9
+ * After the UNIFIED-STREAM migration, bind is a plain POST returning
10
+ * {correlationId}. The state change arrives on the events-stream as
11
+ * mark:body-updated. These tests focus on the POST call, not the
12
+ * events-stream delivery (which is tested in AnnotationStore tests).
13
13
  *
14
14
  * Uses real providers (EventBus, ApiClient, AuthToken) with mocked API boundary.
15
15
  */
@@ -21,14 +21,15 @@ import { useBindFlow } from '../../../hooks/useBindFlow';
21
21
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
22
22
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
23
23
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
24
- import { SSEClient } from '@semiont/api-client';
25
- import { resourceId, accessToken, annotationId } from '@semiont/core';
24
+ import { SemiontApiClient } from '@semiont/api-client';
25
+ import { resourceId, annotationId } from '@semiont/core';
26
+
27
+ const mockShowError = vi.fn();
26
28
 
27
- // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
28
29
  vi.mock('../../../components/Toast', () => ({
29
30
  useToast: () => ({
30
31
  showSuccess: vi.fn(),
31
- showError: vi.fn(),
32
+ showError: mockShowError,
32
33
  showInfo: vi.fn(),
33
34
  showWarning: vi.fn(),
34
35
  }),
@@ -43,11 +44,9 @@ describe('Bind Flow - Body Update Integration', () => {
43
44
  beforeEach(() => {
44
45
  vi.clearAllMocks();
45
46
 
46
- bindAnnotationSpy = vi.fn().mockImplementation((_rId: any, annId: any, _req: any, opts: any) => {
47
- queueMicrotask(() => opts.eventBus.get('bind:finished').next({ annotationId: annId }));
48
- return { close: vi.fn() };
49
- });
50
- vi.spyOn(SSEClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
47
+ // Mock the HTTP bindAnnotation method (plain POST, returns {correlationId})
48
+ bindAnnotationSpy = vi.fn().mockResolvedValue({ correlationId: 'corr-test' });
49
+ vi.spyOn(SemiontApiClient.prototype, 'bindAnnotation').mockImplementation(bindAnnotationSpy as any);
51
50
  });
52
51
 
53
52
  afterEach(() => {
@@ -82,10 +81,11 @@ describe('Bind Flow - Body Update Integration', () => {
82
81
 
83
82
  // ─── bind:update-body ──────────────────────────────────────────────────
84
83
 
85
- it('bind:update-body calls bindAnnotation API', async () => {
84
+ it('bind:update-body calls http.bindAnnotation (plain POST)', async () => {
86
85
  const { getEventBus } = renderBindFlow();
87
86
 
88
87
  act(() => { getEventBus().get('bind:update-body').next({
88
+ correlationId: 'corr-1',
89
89
  annotationId: annotationId('ann-body-1'),
90
90
  resourceId: resourceId('linked-resource-id'),
91
91
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'linked-resource-id' } }],
@@ -100,6 +100,7 @@ describe('Bind Flow - Body Update Integration', () => {
100
100
  const { getEventBus } = renderBindFlow();
101
101
 
102
102
  act(() => { getEventBus().get('bind:update-body').next({
103
+ correlationId: 'corr-2',
103
104
  annotationId: annotationId('ann-auth'),
104
105
  resourceId: resourceId('resource-id'),
105
106
  operations: [{ op: 'replace', newItem: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -111,57 +112,24 @@ describe('Bind Flow - Body Update Integration', () => {
111
112
 
112
113
  const callArgs = bindAnnotationSpy.mock.calls[0];
113
114
  expect(callArgs[3]).toHaveProperty('auth');
114
- expect(callArgs[3].auth).toBe(accessToken(testToken));
115
- });
116
-
117
- it('bind:update-body emits bind:body-updated on success', async () => {
118
- const { getEventBus } = renderBindFlow();
119
- const bodyUpdatedSpy = vi.fn();
120
-
121
- const subscription = getEventBus().get('bind:body-updated').subscribe(bodyUpdatedSpy);
122
-
123
- act(() => { getEventBus().get('bind:update-body').next({
124
- annotationId: annotationId('ann-success'),
125
- resourceId: resourceId('resource-id'),
126
- operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
127
- }); });
128
-
129
- await waitFor(() => {
130
- expect(bodyUpdatedSpy).toHaveBeenCalledTimes(1);
131
- });
132
-
133
- subscription.unsubscribe();
134
-
135
- expect(bodyUpdatedSpy).toHaveBeenCalledWith({
136
- annotationId: annotationId('ann-success'),
137
- });
138
115
  });
139
116
 
140
- it('bind:update-body emits bind:body-update-failed on API error', async () => {
141
- bindAnnotationSpy.mockImplementation((_rId: any, _annId: any, _req: any, opts: any) => {
142
- queueMicrotask(() => opts.eventBus.get('bind:failed').next({ error: new Error('Update failed') }));
143
- return { close: vi.fn() };
144
- });
117
+ it('bind:update-body shows error toast on API error', async () => {
118
+ bindAnnotationSpy.mockRejectedValueOnce(new Error('Update failed'));
145
119
 
146
120
  const { getEventBus } = renderBindFlow();
147
- const bodyUpdateFailedSpy = vi.fn();
148
-
149
- const subscription = getEventBus().get('bind:body-update-failed').subscribe(bodyUpdateFailedSpy);
150
121
 
151
122
  act(() => { getEventBus().get('bind:update-body').next({
123
+ correlationId: 'corr-3',
152
124
  annotationId: annotationId('ann-fail'),
153
125
  resourceId: resourceId('resource-id'),
154
126
  operations: [{ op: 'remove', item: { type: 'SpecificResource' as const, source: 'old-id' } }],
155
127
  }); });
156
128
 
157
129
  await waitFor(() => {
158
- expect(bodyUpdateFailedSpy).toHaveBeenCalledTimes(1);
159
- });
160
-
161
- subscription.unsubscribe();
162
-
163
- expect(bodyUpdateFailedSpy).toHaveBeenCalledWith({
164
- error: expect.any(Error),
130
+ expect(mockShowError).toHaveBeenCalledWith(
131
+ expect.stringContaining('Update failed'),
132
+ );
165
133
  });
166
134
  });
167
135
 
@@ -169,6 +137,7 @@ describe('Bind Flow - Body Update Integration', () => {
169
137
  const { getEventBus } = renderBindFlow();
170
138
 
171
139
  act(() => { getEventBus().get('bind:update-body').next({
140
+ correlationId: 'corr-4',
172
141
  annotationId: annotationId('ann-dedup'),
173
142
  resourceId: resourceId('resource-id'),
174
143
  operations: [{ op: 'add', item: { type: 'SpecificResource' as const, source: 'resource-id' } }],
@@ -17,7 +17,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
17
17
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
18
18
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
19
19
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
20
- import { SSEClient } from '@semiont/api-client';
20
+ import { SemiontApiClient } from '@semiont/api-client';
21
21
 
22
22
  // Mock Toast module to prevent "useToast must be used within a ToastProvider" errors
23
23
  vi.mock('../../../components/Toast', () => ({
@@ -33,11 +33,11 @@ describe('REPRODUCING BUG: Detection state not updating', () => {
33
33
  beforeEach(() => {
34
34
  vi.clearAllMocks();
35
35
 
36
- // Minimal mock - SSE streams not needed for this test
37
- vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
38
- vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
39
- vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
40
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue({ onProgress: vi.fn().mockReturnThis(), onComplete: vi.fn().mockReturnThis(), onError: vi.fn().mockReturnThis(), close: vi.fn() } as any);
36
+ // Minimal mock namespace methods call these HTTP methods internally
37
+ vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
38
+ vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
39
+ vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
40
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
41
41
  });
42
42
 
43
43
  afterEach(() => {
@@ -29,7 +29,7 @@ import { useMarkFlow } from '../../../hooks/useMarkFlow';
29
29
  import { EventBusProvider, useEventBus } from '../../../contexts/EventBusContext';
30
30
  import { ApiClientProvider } from '../../../contexts/ApiClientContext';
31
31
  import { AuthTokenProvider } from '../../../contexts/AuthTokenContext';
32
- import { SSEClient } from '@semiont/api-client';
32
+ import { SemiontApiClient } from '@semiont/api-client';
33
33
  import type { Motivation } from '@semiont/core';
34
34
  import { resourceId } from '@semiont/core';
35
35
  import type { Emitter } from 'mitt';
@@ -45,15 +45,7 @@ vi.mock('../../../components/Toast', () => ({
45
45
  }));
46
46
  import type { EventMap } from '@semiont/core';
47
47
 
48
- // Mock SSE stream - SSE now emits directly to EventBus, no callbacks
49
- const createMockSSEStream = () => {
50
- return {
51
- close: vi.fn(),
52
- };
53
- };
54
-
55
48
  describe('Detection Flow - Feature Integration', () => {
56
- let mockStream: ReturnType<typeof createMockSSEStream>;
57
49
  let markReferencesSpy: any;
58
50
  let markHighlightsSpy: any;
59
51
  let detectCommentsSpy: any;
@@ -61,14 +53,11 @@ describe('Detection Flow - Feature Integration', () => {
61
53
  beforeEach(() => {
62
54
  vi.clearAllMocks();
63
55
 
64
- // Create fresh mock stream for each test
65
- mockStream = createMockSSEStream();
66
-
67
- // Spy on SSEClient prototype methods
68
- markReferencesSpy = vi.spyOn(SSEClient.prototype, 'markReferences').mockReturnValue(mockStream as any);
69
- markHighlightsSpy = vi.spyOn(SSEClient.prototype, 'markHighlights').mockReturnValue(mockStream as any);
70
- detectCommentsSpy = vi.spyOn(SSEClient.prototype, 'markComments').mockReturnValue(mockStream as any);
71
- vi.spyOn(SSEClient.prototype, 'markAssessments').mockReturnValue(mockStream as any);
56
+ // Spy on SemiontApiClient prototype HTTP methods (namespace methods call these)
57
+ markReferencesSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateReferences').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
58
+ markHighlightsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateHighlights').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
59
+ detectCommentsSpy = vi.spyOn(SemiontApiClient.prototype, 'annotateComments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
60
+ vi.spyOn(SemiontApiClient.prototype, 'annotateAssessments').mockResolvedValue({ correlationId: 'c1', jobId: 'j1' });
72
61
  });
73
62
 
74
63
  afterEach(() => {
@@ -298,8 +287,7 @@ describe('Detection Flow - Feature Integration', () => {
298
287
 
299
288
  // Reset for next test
300
289
  vi.clearAllMocks();
301
- mockStream = createMockSSEStream();
302
- detectCommentsSpy.mockReturnValue(mockStream);
290
+ detectCommentsSpy.mockResolvedValue({ correlationId: 'c2', jobId: 'j2' });
303
291
 
304
292
  // Test commenting
305
293
  act(() => {
@@ -158,7 +158,7 @@ describe('Toast Notifications - Verifies Toast Integration', () => {
158
158
  // Emit generation failed event
159
159
  act(() => {
160
160
  eventBusInstance.get('yield:failed').next({
161
- error: new Error('Failed to generate document'),
161
+ error: 'Failed to generate document',
162
162
  });
163
163
  });
164
164