@semiont/react-ui 0.2.45 → 0.3.0

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 (220) hide show
  1. package/dist/{PdfAnnotationCanvas.client-COQREPXU.mjs → PdfAnnotationCanvas.client-PVTVPDBQ.mjs} +3 -4
  2. package/dist/PdfAnnotationCanvas.client-PVTVPDBQ.mjs.map +1 -0
  3. package/dist/{ar-7SUXNE34.mjs → ar-APUOG2AP.mjs} +46 -6
  4. package/dist/ar-APUOG2AP.mjs.map +1 -0
  5. package/dist/{bn-XOET3DOI.mjs → bn-EFK2LJGK.mjs} +46 -6
  6. package/dist/bn-EFK2LJGK.mjs.map +1 -0
  7. package/dist/{chunk-JH7BXE2P.mjs → chunk-7DW2P4UE.mjs} +45 -5
  8. package/dist/chunk-7DW2P4UE.mjs.map +1 -0
  9. package/dist/{chunk-Q2KV6Y2J.mjs → chunk-7GEYABC6.mjs} +32 -32
  10. package/dist/{chunk-3JTO27MH.mjs → chunk-D4GAAQMM.mjs} +2 -9
  11. package/dist/{cs-X63DXX7L.mjs → cs-A26MEEQE.mjs} +46 -6
  12. package/dist/cs-A26MEEQE.mjs.map +1 -0
  13. package/dist/{da-OWTCV57A.mjs → da-U3L2FHSZ.mjs} +46 -6
  14. package/dist/da-U3L2FHSZ.mjs.map +1 -0
  15. package/dist/{de-77BMFDVF.mjs → de-Y5BHEBT7.mjs} +46 -6
  16. package/dist/de-Y5BHEBT7.mjs.map +1 -0
  17. package/dist/dist-YLEIY3JJ.mjs +547 -0
  18. package/dist/dist-YLEIY3JJ.mjs.map +1 -0
  19. package/dist/{el-FIBNLH2V.mjs → el-HU7LAWQY.mjs} +46 -6
  20. package/dist/el-HU7LAWQY.mjs.map +1 -0
  21. package/dist/{en-XWEPVTB4.mjs → en-HAKDCFKL.mjs} +5 -3
  22. package/dist/{es-726NTS53.mjs → es-4BN64QH5.mjs} +46 -6
  23. package/dist/es-4BN64QH5.mjs.map +1 -0
  24. package/dist/{fa-3N4CIWE6.mjs → fa-6ELTBARU.mjs} +46 -6
  25. package/dist/fa-6ELTBARU.mjs.map +1 -0
  26. package/dist/{fi-JOM3M7Z4.mjs → fi-DJ4WGIFW.mjs} +46 -6
  27. package/dist/fi-DJ4WGIFW.mjs.map +1 -0
  28. package/dist/{fr-56QSXS7E.mjs → fr-23XM6H6H.mjs} +46 -6
  29. package/dist/fr-23XM6H6H.mjs.map +1 -0
  30. package/dist/{he-SNAXPJEK.mjs → he-JSWJC2XU.mjs} +46 -6
  31. package/dist/he-JSWJC2XU.mjs.map +1 -0
  32. package/dist/{hi-CRBRD5TB.mjs → hi-BENHG3OJ.mjs} +46 -6
  33. package/dist/hi-BENHG3OJ.mjs.map +1 -0
  34. package/dist/{id-BRCVLICF.mjs → id-4HHQJQNF.mjs} +46 -6
  35. package/dist/id-4HHQJQNF.mjs.map +1 -0
  36. package/dist/index.css +108 -12
  37. package/dist/index.css.map +1 -1
  38. package/dist/index.d.mts +399 -147
  39. package/dist/index.mjs +3573 -2226
  40. package/dist/index.mjs.map +1 -1
  41. package/dist/{it-M2Z27BNB.mjs → it-U6I5PDKU.mjs} +46 -6
  42. package/dist/it-U6I5PDKU.mjs.map +1 -0
  43. package/dist/{ja-TZUKW7HD.mjs → ja-K3YBDWDP.mjs} +46 -6
  44. package/dist/ja-K3YBDWDP.mjs.map +1 -0
  45. package/dist/{ko-NKBGGOL6.mjs → ko-KC2HXRXG.mjs} +46 -6
  46. package/dist/ko-KC2HXRXG.mjs.map +1 -0
  47. package/dist/{magic-string.es-7FJ3LUGB.mjs → magic-string.es-K77I4ZQN.mjs} +2 -2
  48. package/dist/{ms-XFXPN6RX.mjs → ms-KY5QGBNN.mjs} +46 -6
  49. package/dist/ms-KY5QGBNN.mjs.map +1 -0
  50. package/dist/{nl-MVYXAS5C.mjs → nl-6PZFLGY2.mjs} +46 -6
  51. package/dist/nl-6PZFLGY2.mjs.map +1 -0
  52. package/dist/{no-XOLO4JPV.mjs → no-5QR7PLVJ.mjs} +46 -6
  53. package/dist/no-5QR7PLVJ.mjs.map +1 -0
  54. package/dist/{pl-TRWLMMC4.mjs → pl-4GV2NQXE.mjs} +46 -6
  55. package/dist/pl-4GV2NQXE.mjs.map +1 -0
  56. package/dist/{pt-M3TE24UI.mjs → pt-F3F5QD2P.mjs} +46 -6
  57. package/dist/pt-F3F5QD2P.mjs.map +1 -0
  58. package/dist/{ro-QBFG2T64.mjs → ro-TFYL2IQB.mjs} +46 -6
  59. package/dist/ro-TFYL2IQB.mjs.map +1 -0
  60. package/dist/{sv-IUECBXWX.mjs → sv-PRVF2QLR.mjs} +46 -6
  61. package/dist/sv-PRVF2QLR.mjs.map +1 -0
  62. package/dist/test-utils.mjs +16994 -22140
  63. package/dist/test-utils.mjs.map +1 -1
  64. package/dist/{th-US7KIN5Q.mjs → th-SUQOQFUZ.mjs} +46 -6
  65. package/dist/th-SUQOQFUZ.mjs.map +1 -0
  66. package/dist/{tr-DWJ2FFUK.mjs → tr-AYUJZOFJ.mjs} +46 -6
  67. package/dist/tr-AYUJZOFJ.mjs.map +1 -0
  68. package/dist/{uk-M4ZE4DPZ.mjs → uk-YY5WGLBM.mjs} +46 -6
  69. package/dist/uk-YY5WGLBM.mjs.map +1 -0
  70. package/dist/{vi-FERZNPSH.mjs → vi-6RO77ITD.mjs} +46 -6
  71. package/dist/{vi-FERZNPSH.mjs.map → vi-6RO77ITD.mjs.map} +1 -1
  72. package/dist/{zh-3J2I3WYK.mjs → zh-L6GA65H6.mjs} +46 -6
  73. package/dist/zh-L6GA65H6.mjs.map +1 -0
  74. package/package.json +18 -14
  75. package/src/components/Button/Button.tsx +23 -25
  76. package/src/components/annotation/AnnotateToolbar.tsx +1 -1
  77. package/src/components/annotation-popups/SharedPopupElements.tsx +5 -7
  78. package/src/components/image-annotation/SvgDrawingCanvas.tsx +3 -4
  79. package/src/components/modals/ConfigureGenerationStep.tsx +190 -0
  80. package/src/components/modals/ConfigureSearchStep.tsx +105 -0
  81. package/src/components/modals/ContextSummary.tsx +183 -0
  82. package/src/components/modals/GatherContextStep.tsx +89 -0
  83. package/src/components/modals/KeyboardShortcutsHelpModal.tsx +4 -6
  84. package/src/components/modals/ProposeEntitiesModal.tsx +4 -6
  85. package/src/components/modals/ReferenceWizardModal.tsx +326 -0
  86. package/src/components/modals/ResourceSearchModal.tsx +4 -6
  87. package/src/components/modals/SearchModal.css +43 -0
  88. package/src/components/modals/SearchModal.tsx +4 -6
  89. package/src/components/modals/SearchResultsStep.tsx +126 -0
  90. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +3 -4
  91. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +36 -14
  92. package/src/components/resource/AnnotateView.tsx +4 -4
  93. package/src/components/resource/AnnotationHistory.tsx +2 -2
  94. package/src/components/resource/BrowseView.tsx +4 -4
  95. package/src/components/resource/ResourceViewer.tsx +5 -8
  96. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +16 -16
  97. package/src/components/resource/__tests__/BrowseView.test.tsx +2 -2
  98. package/src/components/resource/__tests__/HistoryEvent.test.tsx +1 -1
  99. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +1 -1
  100. package/src/components/resource/panels/AssessmentEntry.tsx +9 -11
  101. package/src/components/resource/panels/AssessmentPanel.tsx +1 -1
  102. package/src/components/resource/panels/CommentEntry.tsx +10 -12
  103. package/src/components/resource/panels/CommentsPanel.tsx +1 -1
  104. package/src/components/resource/panels/HighlightEntry.tsx +9 -11
  105. package/src/components/resource/panels/HighlightPanel.tsx +2 -2
  106. package/src/components/resource/panels/ReferenceEntry.tsx +57 -104
  107. package/src/components/resource/panels/ReferencesPanel.css +85 -13
  108. package/src/components/resource/panels/ReferencesPanel.tsx +2 -3
  109. package/src/components/resource/panels/TagEntry.tsx +9 -11
  110. package/src/components/resource/panels/TaggingPanel.tsx +1 -1
  111. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +4 -4
  112. package/src/components/resource/panels/__tests__/AssistSection.test.tsx +7 -7
  113. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +3 -3
  114. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +2 -2
  115. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +64 -101
  116. package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +1 -1
  117. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +7 -7
  118. package/src/components/viewers/ImageViewer.tsx +3 -6
  119. package/src/components/viewers/__tests__/ImageViewer.test.tsx +3 -3
  120. package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +5 -5
  121. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +141 -0
  122. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +41 -0
  123. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +148 -0
  124. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +106 -0
  125. package/src/features/admin-exchange/components/AdminExchangePage.tsx +120 -0
  126. package/src/features/admin-exchange/components/ExportCard.tsx +35 -0
  127. package/src/features/admin-exchange/components/ImportCard.tsx +188 -0
  128. package/src/features/admin-exchange/components/ImportProgress.tsx +86 -0
  129. package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +3 -3
  130. package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +2 -2
  131. package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +4 -4
  132. package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -3
  133. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +117 -0
  134. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +121 -0
  135. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -5
  136. package/src/features/resource-compose/components/ResourceComposePage.tsx +56 -1
  137. package/src/features/resource-discovery/__tests__/ResourceCard.test.tsx +1 -1
  138. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +2 -2
  139. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +14 -14
  140. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +12 -11
  141. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +2 -2
  142. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +22 -115
  143. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +3 -3
  144. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +20 -20
  145. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +7 -7
  146. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +43 -20
  147. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +2 -2
  148. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +45 -82
  149. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +4 -4
  150. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +151 -74
  151. package/src/integrations/tailwind-plugin.js +3 -3
  152. package/src/styles/core/buttons.css +31 -0
  153. package/src/styles/features/exchange.css +404 -0
  154. package/src/styles/index.css +1 -0
  155. package/translations/ar.json +42 -4
  156. package/translations/bn.json +42 -4
  157. package/translations/cs.json +42 -4
  158. package/translations/da.json +128 -90
  159. package/translations/de.json +122 -84
  160. package/translations/el.json +42 -4
  161. package/translations/en.json +42 -4
  162. package/translations/es.json +42 -4
  163. package/translations/fa.json +42 -4
  164. package/translations/fi.json +68 -30
  165. package/translations/fr.json +42 -4
  166. package/translations/he.json +42 -4
  167. package/translations/hi.json +42 -4
  168. package/translations/id.json +43 -5
  169. package/translations/it.json +62 -24
  170. package/translations/ja.json +43 -5
  171. package/translations/ko.json +42 -4
  172. package/translations/ms.json +43 -5
  173. package/translations/nl.json +41 -3
  174. package/translations/no.json +104 -66
  175. package/translations/pl.json +42 -4
  176. package/translations/pt.json +43 -5
  177. package/translations/ro.json +42 -4
  178. package/translations/sv.json +42 -4
  179. package/translations/th.json +42 -4
  180. package/translations/tr.json +42 -4
  181. package/translations/uk.json +42 -4
  182. package/translations/vi.json +42 -4
  183. package/translations/zh.json +42 -4
  184. package/dist/PdfAnnotationCanvas.client-COQREPXU.mjs.map +0 -1
  185. package/dist/ar-7SUXNE34.mjs.map +0 -1
  186. package/dist/bn-XOET3DOI.mjs.map +0 -1
  187. package/dist/chunk-JH7BXE2P.mjs.map +0 -1
  188. package/dist/cs-X63DXX7L.mjs.map +0 -1
  189. package/dist/da-OWTCV57A.mjs.map +0 -1
  190. package/dist/de-77BMFDVF.mjs.map +0 -1
  191. package/dist/el-FIBNLH2V.mjs.map +0 -1
  192. package/dist/es-726NTS53.mjs.map +0 -1
  193. package/dist/fa-3N4CIWE6.mjs.map +0 -1
  194. package/dist/fi-JOM3M7Z4.mjs.map +0 -1
  195. package/dist/fr-56QSXS7E.mjs.map +0 -1
  196. package/dist/he-SNAXPJEK.mjs.map +0 -1
  197. package/dist/hi-CRBRD5TB.mjs.map +0 -1
  198. package/dist/id-BRCVLICF.mjs.map +0 -1
  199. package/dist/it-M2Z27BNB.mjs.map +0 -1
  200. package/dist/ja-TZUKW7HD.mjs.map +0 -1
  201. package/dist/ko-NKBGGOL6.mjs.map +0 -1
  202. package/dist/ms-XFXPN6RX.mjs.map +0 -1
  203. package/dist/nl-MVYXAS5C.mjs.map +0 -1
  204. package/dist/no-XOLO4JPV.mjs.map +0 -1
  205. package/dist/pl-TRWLMMC4.mjs.map +0 -1
  206. package/dist/pt-M3TE24UI.mjs.map +0 -1
  207. package/dist/ro-QBFG2T64.mjs.map +0 -1
  208. package/dist/sv-IUECBXWX.mjs.map +0 -1
  209. package/dist/th-US7KIN5Q.mjs.map +0 -1
  210. package/dist/tr-DWJ2FFUK.mjs.map +0 -1
  211. package/dist/uk-M4ZE4DPZ.mjs.map +0 -1
  212. package/dist/zh-3J2I3WYK.mjs.map +0 -1
  213. package/src/examples/ButtonUsageExample.tsx +0 -242
  214. package/src/examples/button-css-modules.module.css +0 -164
  215. package/src/examples/button-styled-components.tsx +0 -215
  216. package/src/examples/button-tailwind.css +0 -51
  217. /package/dist/{chunk-Q2KV6Y2J.mjs.map → chunk-7GEYABC6.mjs.map} +0 -0
  218. /package/dist/{chunk-3JTO27MH.mjs.map → chunk-D4GAAQMM.mjs.map} +0 -0
  219. /package/dist/{en-XWEPVTB4.mjs.map → en-HAKDCFKL.mjs.map} +0 -0
  220. /package/dist/{magic-string.es-7FJ3LUGB.mjs.map → magic-string.es-K77I4ZQN.mjs.map} +0 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Tests for ImportCard component
3
+ */
4
+
5
+ import { describe, it, expect, vi } from 'vitest';
6
+ import { render, screen, fireEvent } from '@testing-library/react';
7
+ import { ImportCard } from '../components/ImportCard';
8
+ import type { ImportCardProps } from '../components/ImportCard';
9
+
10
+ const createProps = (overrides?: Partial<ImportCardProps>): ImportCardProps => ({
11
+ onFileSelected: vi.fn(),
12
+ onImport: vi.fn(),
13
+ onCancel: vi.fn(),
14
+ selectedFile: null,
15
+ preview: null,
16
+ isImporting: false,
17
+ translations: {
18
+ title: 'Import',
19
+ description: 'Restore from a file',
20
+ dropzoneLabel: 'Drop a file here',
21
+ dropzoneActive: 'Drop to upload',
22
+ detectedFormat: 'Format',
23
+ statsPreview: 'Preview',
24
+ importButton: 'Import',
25
+ importing: 'Importing…',
26
+ importConfirmTitle: 'Confirm Import',
27
+ importConfirmMessage: 'This cannot be undone.',
28
+ confirmImport: 'Proceed',
29
+ cancelImport: 'Cancel',
30
+ },
31
+ ...overrides,
32
+ });
33
+
34
+ describe('ImportCard', () => {
35
+ it('renders title and description', () => {
36
+ render(<ImportCard {...createProps()} />);
37
+ expect(screen.getByRole('heading', { name: 'Import' })).toBeInTheDocument();
38
+ expect(screen.getByText('Restore from a file')).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders dropzone with label', () => {
42
+ render(<ImportCard {...createProps()} />);
43
+ expect(screen.getByText('Drop a file here')).toBeInTheDocument();
44
+ });
45
+
46
+ it('disables import button when no preview', () => {
47
+ render(<ImportCard {...createProps()} />);
48
+ expect(screen.getByRole('button', { name: 'Import' })).toBeDisabled();
49
+ });
50
+
51
+ it('enables import button when preview is available', () => {
52
+ render(<ImportCard {...createProps({
53
+ selectedFile: new File([''], 'test.jsonl'),
54
+ preview: { format: 'semiont-backup', version: 1, sourceUrl: '', stats: {} },
55
+ })} />);
56
+ expect(screen.getByRole('button', { name: 'Import' })).not.toBeDisabled();
57
+ });
58
+
59
+ it('shows file info when a file is selected', () => {
60
+ const file = new File(['test'], 'backup.tar.gz');
61
+ render(<ImportCard {...createProps({ selectedFile: file })} />);
62
+ expect(screen.getByText(/backup\.tar\.gz/)).toBeInTheDocument();
63
+ });
64
+
65
+ it('shows preview details', () => {
66
+ render(<ImportCard {...createProps({
67
+ selectedFile: new File([''], 'test.jsonl'),
68
+ preview: {
69
+ format: 'semiont-backup',
70
+ version: 1,
71
+ sourceUrl: 'http://example.com',
72
+ stats: { streams: 5 },
73
+ },
74
+ })} />);
75
+ expect(screen.getByText('semiont-backup v1')).toBeInTheDocument();
76
+ expect(screen.getByText('http://example.com')).toBeInTheDocument();
77
+ expect(screen.getByText('5 streams')).toBeInTheDocument();
78
+ });
79
+
80
+ it('shows confirmation dialog on import click', () => {
81
+ render(<ImportCard {...createProps({
82
+ selectedFile: new File([''], 'test.jsonl'),
83
+ preview: { format: 'semiont-backup', version: 1, sourceUrl: '', stats: {} },
84
+ })} />);
85
+
86
+ fireEvent.click(screen.getByRole('button', { name: 'Import' }));
87
+ expect(screen.getByText('Confirm Import')).toBeInTheDocument();
88
+ expect(screen.getByText('This cannot be undone.')).toBeInTheDocument();
89
+ });
90
+
91
+ it('calls onImport when confirmation is confirmed', () => {
92
+ const onImport = vi.fn();
93
+ render(<ImportCard {...createProps({
94
+ onImport,
95
+ selectedFile: new File([''], 'test.jsonl'),
96
+ preview: { format: 'semiont-backup', version: 1, sourceUrl: '', stats: {} },
97
+ })} />);
98
+
99
+ fireEvent.click(screen.getByRole('button', { name: 'Import' }));
100
+ fireEvent.click(screen.getByRole('button', { name: 'Proceed' }));
101
+ expect(onImport).toHaveBeenCalled();
102
+ });
103
+
104
+ it('hides confirmation when cancel is clicked in confirm dialog', () => {
105
+ render(<ImportCard {...createProps({
106
+ selectedFile: new File([''], 'test.jsonl'),
107
+ preview: { format: 'semiont-backup', version: 1, sourceUrl: '', stats: {} },
108
+ })} />);
109
+
110
+ fireEvent.click(screen.getByRole('button', { name: 'Import' }));
111
+ expect(screen.getByText('Confirm Import')).toBeInTheDocument();
112
+
113
+ // In confirm dialog, the Cancel button
114
+ const cancelButtons = screen.getAllByRole('button', { name: 'Cancel' });
115
+ fireEvent.click(cancelButtons[0]);
116
+ expect(screen.queryByText('Confirm Import')).not.toBeInTheDocument();
117
+ });
118
+
119
+ it('shows cancel button when file is selected', () => {
120
+ render(<ImportCard {...createProps({
121
+ selectedFile: new File([''], 'test.jsonl'),
122
+ })} />);
123
+ expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
124
+ });
125
+
126
+ it('disables import button when importing', () => {
127
+ render(<ImportCard {...createProps({
128
+ isImporting: true,
129
+ selectedFile: new File([''], 'test.jsonl'),
130
+ preview: { format: 'semiont-backup', version: 1, sourceUrl: '', stats: {} },
131
+ })} />);
132
+ expect(screen.getByRole('button', { name: 'Importing…' })).toBeDisabled();
133
+ });
134
+
135
+ it('handles file drop', () => {
136
+ const onFileSelected = vi.fn();
137
+ render(<ImportCard {...createProps({ onFileSelected })} />);
138
+
139
+ const dropzone = screen.getByText('Drop a file here').closest('.semiont-exchange__dropzone')!;
140
+ const file = new File(['content'], 'test.tar.gz');
141
+
142
+ fireEvent.drop(dropzone, {
143
+ dataTransfer: { files: [file] },
144
+ });
145
+
146
+ expect(onFileSelected).toHaveBeenCalledWith(file);
147
+ });
148
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tests for ImportProgress component
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { render, screen } from '@testing-library/react';
7
+ import { ImportProgress } from '../components/ImportProgress';
8
+ import type { ImportProgressProps } from '../components/ImportProgress';
9
+
10
+ const translations: ImportProgressProps['translations'] = {
11
+ phaseStarted: 'Starting restore…',
12
+ phaseEntityTypes: 'Adding entity types…',
13
+ phaseResources: 'Creating resources…',
14
+ phaseAnnotations: 'Creating annotations…',
15
+ phaseComplete: 'Restore complete',
16
+ phaseError: 'Restore failed',
17
+ hashChainValid: 'Hash chain valid',
18
+ hashChainInvalid: 'Hash chain invalid',
19
+ streams: 'Event streams',
20
+ events: 'Events',
21
+ blobs: 'Content blobs',
22
+ };
23
+
24
+ describe('ImportProgress', () => {
25
+ it('renders phase label for known phases', () => {
26
+ render(<ImportProgress phase="started" translations={translations} />);
27
+ expect(screen.getByText('Starting restore…')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders raw phase string for unknown phases', () => {
31
+ render(<ImportProgress phase="custom-phase" translations={translations} />);
32
+ expect(screen.getByText('custom-phase')).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders message during active phases', () => {
36
+ render(<ImportProgress phase="resources" message="Processing resource 3/10" translations={translations} />);
37
+ expect(screen.getByText('Processing resource 3/10')).toBeInTheDocument();
38
+ });
39
+
40
+ it('does not render message during complete phase', () => {
41
+ render(<ImportProgress phase="complete" message="some message" translations={translations} />);
42
+ expect(screen.queryByText('some message')).not.toBeInTheDocument();
43
+ });
44
+
45
+ it('renders complete phase with correct class', () => {
46
+ const { container } = render(<ImportProgress phase="complete" translations={translations} />);
47
+ const label = container.querySelector('.semiont-exchange__phase-label--complete');
48
+ expect(label).toBeInTheDocument();
49
+ expect(label).toHaveTextContent('Restore complete');
50
+ });
51
+
52
+ it('renders error phase with correct class', () => {
53
+ const { container } = render(<ImportProgress phase="error" translations={translations} />);
54
+ const label = container.querySelector('.semiont-exchange__phase-label--error');
55
+ expect(label).toBeInTheDocument();
56
+ expect(label).toHaveTextContent('Restore failed');
57
+ });
58
+
59
+ it('renders error message', () => {
60
+ render(<ImportProgress phase="error" message="Connection failed" translations={translations} />);
61
+ expect(screen.getByText('Connection failed')).toBeInTheDocument();
62
+ });
63
+
64
+ it('renders backup result stats', () => {
65
+ render(<ImportProgress
66
+ phase="complete"
67
+ result={{ stats: { streams: 5, events: 42, blobs: 3 } }}
68
+ translations={translations}
69
+ />);
70
+ expect(screen.getByText('5')).toBeInTheDocument();
71
+ expect(screen.getByText('Event streams')).toBeInTheDocument();
72
+ expect(screen.getByText('42')).toBeInTheDocument();
73
+ expect(screen.getByText('Events')).toBeInTheDocument();
74
+ expect(screen.getByText('3')).toBeInTheDocument();
75
+ expect(screen.getByText('Content blobs')).toBeInTheDocument();
76
+ });
77
+
78
+ it('renders valid hash chain badge', () => {
79
+ const { container } = render(<ImportProgress
80
+ phase="complete"
81
+ result={{ hashChainValid: true }}
82
+ translations={translations}
83
+ />);
84
+ expect(screen.getByText('Hash chain valid')).toBeInTheDocument();
85
+ expect(container.querySelector('.semiont-exchange__hash-badge--valid')).toBeInTheDocument();
86
+ });
87
+
88
+ it('renders invalid hash chain badge', () => {
89
+ const { container } = render(<ImportProgress
90
+ phase="complete"
91
+ result={{ hashChainValid: false }}
92
+ translations={translations}
93
+ />);
94
+ expect(screen.getByText('Hash chain invalid')).toBeInTheDocument();
95
+ expect(container.querySelector('.semiont-exchange__hash-badge--invalid')).toBeInTheDocument();
96
+ });
97
+
98
+ it('does not render result section during non-complete phases', () => {
99
+ const { container } = render(<ImportProgress
100
+ phase="resources"
101
+ result={{ stats: { streams: 1, events: 5, blobs: 0 } }}
102
+ translations={translations}
103
+ />);
104
+ expect(container.querySelector('.semiont-exchange__result')).not.toBeInTheDocument();
105
+ });
106
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * AdminExchangePage — Backup/Restore admin page
3
+ *
4
+ * Pure React component. All state and handlers passed as props.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { COMMON_PANELS, type ToolbarPanelType } from '../../../hooks/usePanelBrowse';
9
+ import { ExportCard, type ExportCardTranslations } from './ExportCard';
10
+ import { ImportCard, type ImportCardProps, type ImportCardTranslations } from './ImportCard';
11
+ import { ImportProgress, type ImportProgressTranslations } from './ImportProgress';
12
+
13
+ export interface AdminExchangePageTranslations {
14
+ title: string;
15
+ subtitle: string;
16
+ export: ExportCardTranslations;
17
+ import: ImportCardTranslations;
18
+ progress: ImportProgressTranslations;
19
+ }
20
+
21
+ export interface AdminExchangePageProps {
22
+ // Backup
23
+ onExport: () => void;
24
+ isExporting: boolean;
25
+
26
+ // Import
27
+ onFileSelected: (file: File) => void;
28
+ onImport: () => void;
29
+ onCancelImport: () => void;
30
+ selectedFile: File | null;
31
+ preview: ImportCardProps['preview'];
32
+ isImporting: boolean;
33
+
34
+ // Progress
35
+ importPhase: string | null;
36
+ importMessage?: string | undefined;
37
+ importResult?: Record<string, unknown> | undefined;
38
+
39
+ // UI state
40
+ theme: 'light' | 'dark' | 'system';
41
+ showLineNumbers: boolean;
42
+ activePanel: string | null;
43
+
44
+ // Translations
45
+ translations: AdminExchangePageTranslations;
46
+
47
+ // Component dependencies
48
+ ToolbarPanels: React.ComponentType<any>;
49
+ Toolbar: React.ComponentType<any>;
50
+ }
51
+
52
+ export function AdminExchangePage({
53
+ onExport,
54
+ isExporting,
55
+ onFileSelected,
56
+ onImport,
57
+ onCancelImport,
58
+ selectedFile,
59
+ preview,
60
+ isImporting,
61
+ importPhase,
62
+ importMessage,
63
+ importResult,
64
+ theme,
65
+ showLineNumbers,
66
+ activePanel,
67
+ translations: t,
68
+ ToolbarPanels,
69
+ Toolbar,
70
+ }: AdminExchangePageProps) {
71
+ return (
72
+ <div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
73
+ <div className="semiont-page__content">
74
+ <div className="semiont-page__header">
75
+ <h1 className="semiont-page__title">{t.title}</h1>
76
+ <p className="semiont-page__subtitle">{t.subtitle}</p>
77
+ </div>
78
+
79
+ <div className="semiont-exchange__cards">
80
+ <ExportCard
81
+ onExport={onExport}
82
+ isExporting={isExporting}
83
+ translations={t.export}
84
+ />
85
+
86
+ <ImportCard
87
+ onFileSelected={onFileSelected}
88
+ onImport={onImport}
89
+ onCancel={onCancelImport}
90
+ selectedFile={selectedFile}
91
+ preview={preview}
92
+ isImporting={isImporting}
93
+ translations={t.import}
94
+ />
95
+ </div>
96
+
97
+ {importPhase && (
98
+ <ImportProgress
99
+ phase={importPhase}
100
+ message={importMessage}
101
+ result={importResult}
102
+ translations={t.progress}
103
+ />
104
+ )}
105
+ </div>
106
+
107
+ <div className="semiont-page__sidebar">
108
+ <ToolbarPanels
109
+ activePanel={activePanel}
110
+ theme={theme}
111
+ showLineNumbers={showLineNumbers}
112
+ />
113
+ <Toolbar
114
+ context="simple"
115
+ activePanel={activePanel}
116
+ />
117
+ </div>
118
+ </div>
119
+ );
120
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * ExportCard — Backup trigger
3
+ *
4
+ * Pure React component. All state and handlers passed as props.
5
+ */
6
+
7
+ export interface ExportCardTranslations {
8
+ title: string;
9
+ description: string;
10
+ exportButton: string;
11
+ exporting: string;
12
+ }
13
+
14
+ export interface ExportCardProps {
15
+ onExport: () => void;
16
+ isExporting: boolean;
17
+ translations: ExportCardTranslations;
18
+ }
19
+
20
+ export function ExportCard({ onExport, isExporting, translations: t }: ExportCardProps) {
21
+ return (
22
+ <div className="semiont-admin__card">
23
+ <h2 className="semiont-admin__section-title">{t.title}</h2>
24
+ <p className="semiont-admin__section-description">{t.description}</p>
25
+
26
+ <button
27
+ className="semiont-exchange__button semiont-exchange__button--primary"
28
+ onClick={onExport}
29
+ disabled={isExporting}
30
+ >
31
+ {isExporting ? t.exporting : t.exportButton}
32
+ </button>
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * ImportCard — File drop zone, preview, and import trigger
3
+ *
4
+ * Pure React component. All state and handlers passed as props.
5
+ */
6
+
7
+ import React, { useRef, useState, useCallback } from 'react';
8
+
9
+ export interface ImportPreview {
10
+ format: string;
11
+ version: number;
12
+ sourceUrl: string;
13
+ stats: Record<string, number>;
14
+ }
15
+
16
+ export interface ImportCardTranslations {
17
+ title: string;
18
+ description: string;
19
+ dropzoneLabel: string;
20
+ dropzoneActive: string;
21
+ detectedFormat: string;
22
+ statsPreview: string;
23
+ importButton: string;
24
+ importing: string;
25
+ importConfirmTitle: string;
26
+ importConfirmMessage: string;
27
+ confirmImport: string;
28
+ cancelImport: string;
29
+ }
30
+
31
+ export interface ImportCardProps {
32
+ onFileSelected: (file: File) => void;
33
+ onImport: () => void;
34
+ onCancel: () => void;
35
+ selectedFile: File | null;
36
+ preview: ImportPreview | null;
37
+ isImporting: boolean;
38
+ translations: ImportCardTranslations;
39
+ }
40
+
41
+ export function ImportCard({
42
+ onFileSelected,
43
+ onImport,
44
+ onCancel,
45
+ selectedFile,
46
+ preview,
47
+ isImporting,
48
+ translations: t,
49
+ }: ImportCardProps) {
50
+ const fileInputRef = useRef<HTMLInputElement>(null);
51
+ const [isDragActive, setIsDragActive] = useState(false);
52
+ const [showConfirm, setShowConfirm] = useState(false);
53
+
54
+ const handleDragOver = useCallback((e: React.DragEvent) => {
55
+ e.preventDefault();
56
+ setIsDragActive(true);
57
+ }, []);
58
+
59
+ const handleDragLeave = useCallback(() => {
60
+ setIsDragActive(false);
61
+ }, []);
62
+
63
+ const handleDrop = useCallback((e: React.DragEvent) => {
64
+ e.preventDefault();
65
+ setIsDragActive(false);
66
+ setShowConfirm(false);
67
+ const file = e.dataTransfer.files[0];
68
+ if (file) onFileSelected(file);
69
+ }, [onFileSelected]);
70
+
71
+ const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
72
+ setShowConfirm(false);
73
+ const file = e.target.files?.[0];
74
+ if (file) onFileSelected(file);
75
+ }, [onFileSelected]);
76
+
77
+ const handleImportClick = () => {
78
+ setShowConfirm(true);
79
+ };
80
+
81
+ const handleConfirm = () => {
82
+ setShowConfirm(false);
83
+ onImport();
84
+ };
85
+
86
+ const handleCancelConfirm = () => {
87
+ setShowConfirm(false);
88
+ };
89
+
90
+ return (
91
+ <div className="semiont-admin__card">
92
+ <h2 className="semiont-admin__section-title">{t.title}</h2>
93
+ <p className="semiont-admin__section-description">{t.description}</p>
94
+
95
+ <div
96
+ className={`semiont-exchange__dropzone${isDragActive ? ' semiont-exchange__dropzone--active' : ''}`}
97
+ onDragOver={handleDragOver}
98
+ onDragLeave={handleDragLeave}
99
+ onDrop={handleDrop}
100
+ onClick={() => fileInputRef.current?.click()}
101
+ >
102
+ <p className="semiont-exchange__dropzone-text">
103
+ {isDragActive ? t.dropzoneActive : t.dropzoneLabel}
104
+ </p>
105
+ {selectedFile && (
106
+ <p className="semiont-exchange__file-info">
107
+ {selectedFile.name} ({(selectedFile.size / 1024).toFixed(1)} KB)
108
+ </p>
109
+ )}
110
+ </div>
111
+
112
+ <input
113
+ ref={fileInputRef}
114
+ type="file"
115
+ accept=".tar.gz,.jsonl,.gz"
116
+ onChange={handleFileChange}
117
+ style={{ display: 'none' }}
118
+ />
119
+
120
+ {preview && (
121
+ <div className="semiont-exchange__preview">
122
+ <div className="semiont-exchange__preview-row">
123
+ <span className="semiont-exchange__preview-label">{t.detectedFormat}</span>
124
+ <span className="semiont-exchange__preview-value">
125
+ {preview.format} v{preview.version}
126
+ </span>
127
+ </div>
128
+ {preview.sourceUrl && (
129
+ <div className="semiont-exchange__preview-row">
130
+ <span className="semiont-exchange__preview-label">Source</span>
131
+ <span className="semiont-exchange__preview-value">{preview.sourceUrl}</span>
132
+ </div>
133
+ )}
134
+ {Object.keys(preview.stats).length > 0 && (
135
+ <div className="semiont-exchange__preview-row">
136
+ <span className="semiont-exchange__preview-label">{t.statsPreview}</span>
137
+ <span className="semiont-exchange__preview-value">
138
+ {Object.entries(preview.stats).map(([k, v]) => `${v} ${k}`).join(', ')}
139
+ </span>
140
+ </div>
141
+ )}
142
+ </div>
143
+ )}
144
+
145
+ {showConfirm && (
146
+ <div className="semiont-exchange__confirm">
147
+ <p className="semiont-exchange__confirm-title">{t.importConfirmTitle}</p>
148
+ <p className="semiont-exchange__confirm-message">{t.importConfirmMessage}</p>
149
+ <div className="semiont-exchange__confirm-actions">
150
+ <button
151
+ className="semiont-exchange__button semiont-exchange__button--primary"
152
+ onClick={handleConfirm}
153
+ >
154
+ {t.confirmImport}
155
+ </button>
156
+ <button
157
+ className="semiont-exchange__button semiont-exchange__button--secondary"
158
+ onClick={handleCancelConfirm}
159
+ >
160
+ {t.cancelImport}
161
+ </button>
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ {!showConfirm && (
167
+ <div className="semiont-exchange__actions">
168
+ <button
169
+ className="semiont-exchange__button semiont-exchange__button--primary"
170
+ onClick={handleImportClick}
171
+ disabled={!preview || isImporting}
172
+ >
173
+ {isImporting ? t.importing : t.importButton}
174
+ </button>
175
+ {selectedFile && (
176
+ <button
177
+ className="semiont-exchange__button semiont-exchange__button--secondary"
178
+ onClick={onCancel}
179
+ disabled={isImporting}
180
+ >
181
+ {t.cancelImport}
182
+ </button>
183
+ )}
184
+ </div>
185
+ )}
186
+ </div>
187
+ );
188
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ImportProgress — Shows SSE-driven progress during restore
3
+ *
4
+ * Pure React component. All state passed as props.
5
+ */
6
+
7
+ export interface ImportProgressTranslations {
8
+ phaseStarted: string;
9
+ phaseEntityTypes: string;
10
+ phaseResources: string;
11
+ phaseAnnotations: string;
12
+ phaseComplete: string;
13
+ phaseError: string;
14
+ hashChainValid: string;
15
+ hashChainInvalid: string;
16
+ streams: string;
17
+ events: string;
18
+ blobs: string;
19
+ }
20
+
21
+ export interface ImportProgressProps {
22
+ phase: string;
23
+ message?: string;
24
+ result?: Record<string, unknown>;
25
+ translations: ImportProgressTranslations;
26
+ }
27
+
28
+ const PHASE_LABELS: Record<string, keyof ImportProgressTranslations> = {
29
+ started: 'phaseStarted',
30
+ 'entity-types': 'phaseEntityTypes',
31
+ resources: 'phaseResources',
32
+ annotations: 'phaseAnnotations',
33
+ complete: 'phaseComplete',
34
+ error: 'phaseError',
35
+ };
36
+
37
+ export function ImportProgress({ phase, message, result, translations: t }: ImportProgressProps) {
38
+ const labelKey = PHASE_LABELS[phase];
39
+ const phaseLabel = labelKey ? t[labelKey] : phase;
40
+
41
+ const isComplete = phase === 'complete';
42
+ const isError = phase === 'error';
43
+
44
+ return (
45
+ <div className="semiont-exchange__progress">
46
+ <div className={`semiont-exchange__phase-label${isError ? ' semiont-exchange__phase-label--error' : isComplete ? ' semiont-exchange__phase-label--complete' : ''}`}>
47
+ {phaseLabel}
48
+ </div>
49
+
50
+ {message && !isComplete && !isError && (
51
+ <p className="semiont-exchange__progress-message">{message}</p>
52
+ )}
53
+
54
+ {isComplete && result && (
55
+ <div className="semiont-exchange__result">
56
+ {result.stats != null && typeof result.stats === 'object' && (
57
+ <>
58
+ {Object.entries(result.stats as Record<string, number>).map(([key, value]) => {
59
+ const label = key === 'streams' ? t.streams
60
+ : key === 'events' ? t.events
61
+ : key === 'blobs' ? t.blobs
62
+ : key;
63
+ return (
64
+ <div key={key} className="semiont-exchange__result-stat">
65
+ <span className="semiont-exchange__result-value">{value}</span>
66
+ <span className="semiont-exchange__result-label">{label}</span>
67
+ </div>
68
+ );
69
+ })}
70
+ </>
71
+ )}
72
+
73
+ {result.hashChainValid !== undefined && (
74
+ <div className={`semiont-exchange__hash-badge${result.hashChainValid ? ' semiont-exchange__hash-badge--valid' : ' semiont-exchange__hash-badge--invalid'}`}>
75
+ {result.hashChainValid ? t.hashChainValid : t.hashChainInvalid}
76
+ </div>
77
+ )}
78
+ </div>
79
+ )}
80
+
81
+ {isError && message && (
82
+ <p className="semiont-exchange__error-message">{message}</p>
83
+ )}
84
+ </div>
85
+ );
86
+ }