@semiont/react-ui 0.5.27 → 0.5.29

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 (192) hide show
  1. package/README.md +3 -3
  2. package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
  3. package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
  4. package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
  5. package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
  6. package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
  7. package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
  8. package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
  9. package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
  10. package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
  11. package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
  12. package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
  13. package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
  14. package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
  15. package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
  16. package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
  17. package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
  18. package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
  19. package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
  20. package/dist/index.css +279 -74
  21. package/dist/index.d.ts +318 -393
  22. package/dist/index.js +2619 -2687
  23. package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
  24. package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
  25. package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
  26. package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
  27. package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
  28. package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
  29. package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
  30. package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
  31. package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
  32. package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
  33. package/dist/test-utils.js +16 -7
  34. package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
  35. package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
  36. package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
  37. package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
  38. package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
  39. package/package.json +16 -15
  40. package/src/components/AssistProgress.tsx +36 -2
  41. package/src/components/CodeMirrorRenderer.tsx +8 -8
  42. package/src/components/StatusDisplay.tsx +13 -13
  43. package/src/components/__tests__/AssistProgress.test.tsx +59 -0
  44. package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
  45. package/src/components/modals/ComposeStep.tsx +233 -0
  46. package/src/components/modals/ConfigureGatherStep.tsx +10 -18
  47. package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
  48. package/src/components/modals/ConfigureSearchStep.tsx +19 -0
  49. package/src/components/modals/ContextSummary.tsx +185 -72
  50. package/src/components/modals/CorpusPane.tsx +75 -0
  51. package/src/components/modals/DiscardPrompt.tsx +38 -0
  52. package/src/components/modals/GatherContextStep.tsx +237 -107
  53. package/src/components/modals/ReferenceWizardModal.tsx +252 -59
  54. package/src/components/modals/ResourceGenerateModal.tsx +225 -112
  55. package/src/components/modals/SearchModal.css +337 -101
  56. package/src/components/modals/SearchResultsStep.tsx +61 -68
  57. package/src/components/modals/WizardFooter.tsx +7 -1
  58. package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
  59. package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
  60. package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
  61. package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
  62. package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
  63. package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
  64. package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
  65. package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
  66. package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
  67. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
  68. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
  69. package/src/components/resource/AnnotateView.tsx +3 -3
  70. package/src/components/resource/BrowseView.tsx +8 -8
  71. package/src/components/resource/DownloadFileLink.tsx +1 -1
  72. package/src/components/resource/ResourceViewer.tsx +4 -4
  73. package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
  74. package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
  75. package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
  76. package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
  77. package/src/components/resource/__tests__/download-link.test.tsx +6 -6
  78. package/src/components/resource/annotate-renderers.tsx +3 -3
  79. package/src/components/resource/panels/AssistSection.css +35 -0
  80. package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
  81. package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
  82. package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
  83. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
  84. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
  85. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
  86. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
  87. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
  88. package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
  89. package/src/features/auth/components/SignInForm.tsx +53 -53
  90. package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
  91. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
  92. package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
  93. package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
  94. package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
  95. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
  96. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
  97. package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
  98. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
  99. package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
  100. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
  101. package/src/features/resource-viewer/generation-options.ts +29 -0
  102. package/src/styles/core/forms.css +35 -8
  103. package/src/styles/features/resource-viewer.css +0 -22
  104. package/src/styles/features/resource.css +0 -4
  105. package/src/styles/index.css +0 -1
  106. package/translations/ar.json +42 -22
  107. package/translations/bn.json +42 -22
  108. package/translations/cs.json +42 -22
  109. package/translations/da.json +42 -22
  110. package/translations/de.json +42 -22
  111. package/translations/el.json +42 -22
  112. package/translations/en.json +42 -22
  113. package/translations/es.json +42 -22
  114. package/translations/fa.json +42 -22
  115. package/translations/fi.json +42 -22
  116. package/translations/fr.json +42 -22
  117. package/translations/he.json +42 -22
  118. package/translations/hi.json +42 -22
  119. package/translations/id.json +42 -22
  120. package/translations/it.json +42 -22
  121. package/translations/ja.json +42 -22
  122. package/translations/ko.json +42 -22
  123. package/translations/ms.json +42 -22
  124. package/translations/nl.json +42 -22
  125. package/translations/no.json +42 -22
  126. package/translations/pl.json +42 -22
  127. package/translations/pt.json +42 -22
  128. package/translations/ro.json +42 -22
  129. package/translations/sv.json +42 -22
  130. package/translations/th.json +42 -22
  131. package/translations/tr.json +42 -22
  132. package/translations/uk.json +42 -22
  133. package/translations/vi.json +42 -22
  134. package/translations/zh.json +42 -22
  135. package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
  136. package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
  137. package/dist/ar-OU3DQGC6.js.map +0 -1
  138. package/dist/bn-PHWCR6AZ.js.map +0 -1
  139. package/dist/chunk-5VDTWRXJ.js.map +0 -1
  140. package/dist/chunk-BTUG3VJJ.js.map +0 -1
  141. package/dist/chunk-O23TLLXW.js.map +0 -1
  142. package/dist/chunk-PCRRBU7H.js.map +0 -1
  143. package/dist/chunk-QT7LYM72.js.map +0 -1
  144. package/dist/chunk-VXASXU4K.js.map +0 -1
  145. package/dist/cs-NEXYZ62I.js.map +0 -1
  146. package/dist/da-FAT43YCU.js.map +0 -1
  147. package/dist/de-KRQ3WBCU.js.map +0 -1
  148. package/dist/el-Q4IU3EKC.js.map +0 -1
  149. package/dist/en-2KADE7N4.js.map +0 -1
  150. package/dist/es-2IO3R2MK.js.map +0 -1
  151. package/dist/fa-Q5GOQFXX.js.map +0 -1
  152. package/dist/fi-R6NMRWBE.js.map +0 -1
  153. package/dist/fr-QY4D5E6J.js.map +0 -1
  154. package/dist/he-X6Y77XHY.js.map +0 -1
  155. package/dist/hi-XL7IOEQM.js.map +0 -1
  156. package/dist/id-QZ4ZZS67.js.map +0 -1
  157. package/dist/index.css.map +0 -1
  158. package/dist/index.js.map +0 -1
  159. package/dist/integrations/css-modules-helper.js.map +0 -1
  160. package/dist/integrations/styled-components-theme.js.map +0 -1
  161. package/dist/it-ME2Q7OB6.js.map +0 -1
  162. package/dist/ja-HX7QASRI.js.map +0 -1
  163. package/dist/ko-EQO5AHAV.js.map +0 -1
  164. package/dist/ms-UILFSF3I.js.map +0 -1
  165. package/dist/nl-NNI45PMK.js.map +0 -1
  166. package/dist/no-MAL5F6P6.js.map +0 -1
  167. package/dist/pl-4RMY6JAJ.js.map +0 -1
  168. package/dist/pt-VQH2X3QQ.js.map +0 -1
  169. package/dist/ro-XXCQO2OG.js.map +0 -1
  170. package/dist/sv-7Z6EJ6U4.js.map +0 -1
  171. package/dist/test-utils.css.map +0 -1
  172. package/dist/test-utils.js.map +0 -1
  173. package/dist/th-L3DXXYVQ.js.map +0 -1
  174. package/dist/tr-P3DFVLFJ.js.map +0 -1
  175. package/dist/uk-7C5D3PXS.js.map +0 -1
  176. package/dist/vi-ZLTLEO7P.js.map +0 -1
  177. package/dist/zh-5FHVWDIN.js.map +0 -1
  178. package/src/components/modals/ResourceSearchModal.tsx +0 -224
  179. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
  180. package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
  181. package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
  182. package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
  183. package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
  184. package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
  185. package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
  186. package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
  187. package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
  188. package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
  189. package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
  190. package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
  191. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
  192. package/src/styles/features/exchange.css +0 -374
@@ -1,185 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { firstValueFrom, of, throwError } from 'rxjs';
3
- import type { ShellStateUnit } from '../../../../state/shell-state-unit';
4
- import { createExchangeStateUnit } from '../exchange-state-unit';
5
- import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing/axioms';
6
-
7
- function mockBrowse(): ShellStateUnit {
8
- return { dispose: vi.fn() } as unknown as ShellStateUnit;
9
- }
10
-
11
- function makeMockFile(name: string): File {
12
- return new File(['content'], name, { type: 'application/gzip' });
13
- }
14
-
15
- describe('createExchangeStateUnit', () => {
16
- it('initializes with null/empty state', async () => {
17
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), vi.fn());
18
-
19
- expect(await firstValueFrom(stateUnit.selectedFile$)).toBeNull();
20
- expect(await firstValueFrom(stateUnit.preview$)).toBeNull();
21
- expect(await firstValueFrom(stateUnit.importPhase$)).toBeNull();
22
- expect(await firstValueFrom(stateUnit.isExporting$)).toBe(false);
23
- expect(await firstValueFrom(stateUnit.isImporting$)).toBe(false);
24
-
25
- stateUnit.dispose();
26
- });
27
-
28
- it('selectFile sets file and generates preview', async () => {
29
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), vi.fn());
30
-
31
- stateUnit.selectFile(makeMockFile('backup.tar.gz'));
32
-
33
- const file = await firstValueFrom(stateUnit.selectedFile$);
34
- expect(file?.name).toBe('backup.tar.gz');
35
-
36
- const preview = await firstValueFrom(stateUnit.preview$);
37
- expect(preview?.format).toBe('semiont-linked-data');
38
-
39
- stateUnit.dispose();
40
- });
41
-
42
- it('selectFile detects unknown format', async () => {
43
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), vi.fn());
44
-
45
- stateUnit.selectFile(makeMockFile('data.json'));
46
-
47
- const preview = await firstValueFrom(stateUnit.preview$);
48
- expect(preview?.format).toBe('unknown');
49
-
50
- stateUnit.dispose();
51
- });
52
-
53
- it('cancelImport resets all state', async () => {
54
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), vi.fn());
55
-
56
- stateUnit.selectFile(makeMockFile('backup.tar.gz'));
57
- stateUnit.cancelImport();
58
-
59
- expect(await firstValueFrom(stateUnit.selectedFile$)).toBeNull();
60
- expect(await firstValueFrom(stateUnit.preview$)).toBeNull();
61
- expect(await firstValueFrom(stateUnit.importPhase$)).toBeNull();
62
-
63
- stateUnit.dispose();
64
- });
65
-
66
- // jsdom doesn't implement `Blob.prototype.stream()` portably, so build
67
- // the stream literal — same shape, no environment dependency.
68
- function streamOf(text: string): ReadableStream<Uint8Array> {
69
- return new ReadableStream<Uint8Array>({
70
- start(controller) {
71
- controller.enqueue(new TextEncoder().encode(text));
72
- controller.close();
73
- },
74
- });
75
- }
76
-
77
- it('doExport calls exportFn and returns blob + filename from BackendDownload', async () => {
78
- // exportFn now returns a BackendDownload — a transport-neutral
79
- // { stream, contentType, filename? } object. The state unit converts the
80
- // stream to a Blob and threads filename through.
81
- const exportFn = vi.fn().mockResolvedValue({
82
- stream: streamOf('data'),
83
- contentType: 'application/x-tar',
84
- filename: 'export.tar.gz',
85
- });
86
-
87
- const stateUnit = createExchangeStateUnit(mockBrowse(), exportFn, vi.fn());
88
-
89
- const result = await stateUnit.doExport();
90
- expect(result.filename).toBe('export.tar.gz');
91
- expect(await result.blob.text()).toBe('data');
92
-
93
- expect(await firstValueFrom(stateUnit.isExporting$)).toBe(false);
94
-
95
- stateUnit.dispose();
96
- });
97
-
98
- it('doExport falls back to a synthesized filename when the download omits one', async () => {
99
- const exportFn = vi.fn().mockResolvedValue({
100
- stream: streamOf('data'),
101
- contentType: 'application/x-tar',
102
- // no filename
103
- });
104
-
105
- const stateUnit = createExchangeStateUnit(mockBrowse(), exportFn, vi.fn());
106
-
107
- const result = await stateUnit.doExport();
108
- expect(result.filename).toMatch(/^semiont-export-\d+\.tar\.gz$/);
109
-
110
- stateUnit.dispose();
111
- });
112
-
113
- it('doExport propagates errors from exportFn and clears isExporting$', async () => {
114
- // The state unit no longer inspects HTTP status — non-OK responses are the
115
- // transport's concern (ky throws on non-OK by default). The state unit just
116
- // propagates whatever the exportFn rejects with and resets state.
117
- const exportFn = vi.fn().mockRejectedValue(new Error('transport boom'));
118
-
119
- const stateUnit = createExchangeStateUnit(mockBrowse(), exportFn, vi.fn());
120
-
121
- await expect(stateUnit.doExport()).rejects.toThrow('transport boom');
122
- expect(await firstValueFrom(stateUnit.isExporting$)).toBe(false);
123
-
124
- stateUnit.dispose();
125
- });
126
-
127
- it('doImport subscribes to importFn Observable and mirrors each progress event', async () => {
128
- // importFn now returns Observable<ProgressEvent>. The state unit subscribes,
129
- // mirrors each emit into its state subjects, and resolves when the
130
- // observable completes.
131
- const importFn = vi.fn().mockReturnValue(
132
- of(
133
- { phase: 'uploading', message: '50%' },
134
- { phase: 'complete', result: { resources: 10 } },
135
- ),
136
- );
137
-
138
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), importFn);
139
- stateUnit.selectFile(makeMockFile('import.tar.gz'));
140
-
141
- await stateUnit.doImport();
142
-
143
- expect(importFn).toHaveBeenCalledOnce();
144
- expect(await firstValueFrom(stateUnit.importResult$)).toEqual({ resources: 10 });
145
- expect(await firstValueFrom(stateUnit.importPhase$)).toBe('complete');
146
- expect(await firstValueFrom(stateUnit.isImporting$)).toBe(false);
147
-
148
- stateUnit.dispose();
149
- });
150
-
151
- it('doImport propagates errors from the importFn Observable and clears isImporting$', async () => {
152
- const importFn = vi.fn().mockReturnValue(throwError(() => new Error('import boom')));
153
-
154
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), importFn);
155
- stateUnit.selectFile(makeMockFile('import.tar.gz'));
156
-
157
- await expect(stateUnit.doImport()).rejects.toThrow('import boom');
158
- expect(await firstValueFrom(stateUnit.isImporting$)).toBe(false);
159
-
160
- stateUnit.dispose();
161
- });
162
-
163
- it('doImport is no-op without selected file', async () => {
164
- const importFn = vi.fn();
165
- const stateUnit = createExchangeStateUnit(mockBrowse(), vi.fn(), importFn);
166
-
167
- await stateUnit.doImport();
168
- expect(importFn).not.toHaveBeenCalled();
169
-
170
- stateUnit.dispose();
171
- });
172
- });
173
-
174
- describe('ExchangeStateUnit — StateUnit axioms', () => {
175
- it('satisfies the StateUnit axioms (incl. A7-passed: never disposes the injected browse)', () => {
176
- assertStateUnitAxioms({
177
- setup: () => {
178
- const browse = disposeProbe();
179
- return { unit: createExchangeStateUnit(browse as unknown as ShellStateUnit, vi.fn(), vi.fn()), passedIn: [browse] };
180
- },
181
- surfaces: (u) => [u.selectedFile$, u.preview$, u.importPhase$, u.importMessage$, u.importResult$, u.isExporting$, u.isImporting$],
182
- invocations: (u) => [() => u.cancelImport(), () => u.doImport()],
183
- });
184
- });
185
- });
@@ -1,133 +0,0 @@
1
- import { BehaviorSubject, lastValueFrom, type Observable } from 'rxjs';
2
- import { tap } from 'rxjs/operators';
3
- import { createDisposer } from '@semiont/sdk';
4
- import type { StateUnit } from '@semiont/core';
5
- import type { ShellStateUnit } from '../../../state/shell-state-unit';
6
- import type { BackendDownload, ProgressEvent } from '@semiont/core';
7
-
8
- export interface ImportPreview {
9
- format: string;
10
- version: number;
11
- sourceUrl: string;
12
- stats: Record<string, number>;
13
- }
14
-
15
- export interface ExchangeStateUnit extends StateUnit {
16
- browse: ShellStateUnit;
17
- selectedFile$: Observable<File | null>;
18
- preview$: Observable<ImportPreview | null>;
19
- importPhase$: Observable<string | null>;
20
- importMessage$: Observable<string | undefined>;
21
- importResult$: Observable<Record<string, unknown> | undefined>;
22
- isExporting$: Observable<boolean>;
23
- isImporting$: Observable<boolean>;
24
- selectFile(file: File): void;
25
- cancelImport(): void;
26
- doExport(): Promise<{ blob: Blob; filename: string }>;
27
- doImport(): Promise<void>;
28
- }
29
-
30
- export function createExchangeStateUnit(
31
- browse: ShellStateUnit,
32
- exportFn: (params?: { includeArchived?: boolean }) => Promise<BackendDownload>,
33
- importFn: (file: File) => Observable<ProgressEvent>,
34
- ): ExchangeStateUnit {
35
- const disposer = createDisposer();
36
- // `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
37
- // (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
38
- // shared, app-scoped shell). See packages/sdk/docs/STATE-UNITS.md (composition rule).
39
-
40
- const selectedFile$ = new BehaviorSubject<File | null>(null);
41
- const preview$ = new BehaviorSubject<ImportPreview | null>(null);
42
- const importPhase$ = new BehaviorSubject<string | null>(null);
43
- const importMessage$ = new BehaviorSubject<string | undefined>(undefined);
44
- const importResult$ = new BehaviorSubject<Record<string, unknown> | undefined>(undefined);
45
- const isExporting$ = new BehaviorSubject<boolean>(false);
46
- const isImporting$ = new BehaviorSubject<boolean>(false);
47
-
48
- const selectFile = (file: File): void => {
49
- selectedFile$.next(file);
50
- importPhase$.next(null);
51
- importMessage$.next(undefined);
52
- importResult$.next(undefined);
53
- preview$.next({
54
- format: file.name.endsWith('.tar.gz') || file.name.endsWith('.gz') ? 'semiont-linked-data' : 'unknown',
55
- version: 1,
56
- sourceUrl: '',
57
- stats: {} as Record<string, number>,
58
- });
59
- };
60
-
61
- const cancelImport = (): void => {
62
- selectedFile$.next(null);
63
- preview$.next(null);
64
- importPhase$.next(null);
65
- importMessage$.next(undefined);
66
- importResult$.next(undefined);
67
- };
68
-
69
- const doExport = async (): Promise<{ blob: Blob; filename: string }> => {
70
- isExporting$.next(true);
71
- try {
72
- const download = await exportFn();
73
- // Wrap the stream in a Response purely as a Blob-collection helper —
74
- // BackendDownload itself carries no fetch dependency.
75
- const blob = await new Response(download.stream).blob();
76
- const filename = download.filename ?? `semiont-export-${Date.now()}.tar.gz`;
77
- return { blob, filename };
78
- } finally {
79
- isExporting$.next(false);
80
- }
81
- };
82
-
83
- const doImport = async (): Promise<void> => {
84
- const file = selectedFile$.getValue();
85
- if (!file) return;
86
- isImporting$.next(true);
87
- importPhase$.next('started');
88
- importMessage$.next(undefined);
89
- importResult$.next(undefined);
90
- try {
91
- // The importFn is `Observable<ProgressEvent>` — every emit is a
92
- // progress event; the final emit before complete is the outcome.
93
- // `tap` mirrors each event into our state subjects; `lastValueFrom`
94
- // awaits the last value (so callers can `await vm.doImport()`).
95
- await lastValueFrom(
96
- importFn(file).pipe(
97
- tap((event) => {
98
- importPhase$.next(event.phase);
99
- importMessage$.next(event.message);
100
- if (event.result) importResult$.next(event.result);
101
- }),
102
- ),
103
- );
104
- } finally {
105
- isImporting$.next(false);
106
- }
107
- };
108
-
109
- return {
110
- browse,
111
- selectedFile$: selectedFile$.asObservable(),
112
- preview$: preview$.asObservable(),
113
- importPhase$: importPhase$.asObservable(),
114
- importMessage$: importMessage$.asObservable(),
115
- importResult$: importResult$.asObservable(),
116
- isExporting$: isExporting$.asObservable(),
117
- isImporting$: isImporting$.asObservable(),
118
- selectFile,
119
- cancelImport,
120
- doExport,
121
- doImport,
122
- dispose: () => {
123
- selectedFile$.complete();
124
- preview$.complete();
125
- importPhase$.complete();
126
- importMessage$.complete();
127
- importResult$.complete();
128
- isExporting$.complete();
129
- isImporting$.complete();
130
- disposer.dispose();
131
- },
132
- };
133
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Tests for LinkedDataPage component
3
- */
4
-
5
- import { describe, it, expect, vi } from 'vitest';
6
- import { render, screen } from '@testing-library/react';
7
- import { LinkedDataPage } from '../components/LinkedDataPage';
8
- import type { LinkedDataPageProps } from '../components/LinkedDataPage';
9
-
10
- const createProps = (overrides?: Partial<LinkedDataPageProps>): LinkedDataPageProps => ({
11
- onExport: vi.fn(),
12
- isExporting: false,
13
- onFileSelected: vi.fn(),
14
- onImport: vi.fn(),
15
- onCancelImport: vi.fn(),
16
- selectedFile: null,
17
- preview: null,
18
- isImporting: false,
19
- importPhase: null,
20
- theme: 'light',
21
- activePanel: null,
22
- translations: {
23
- title: 'Export & Import',
24
- subtitle: 'Exchange knowledge as JSON-LD Linked Data',
25
- export: {
26
- title: 'Export',
27
- description: 'Export your knowledge base as JSON-LD',
28
- exportButton: 'Export as JSON-LD',
29
- exporting: 'Exporting…',
30
- },
31
- import: {
32
- title: 'Import',
33
- description: 'Import from JSON-LD',
34
- dropzoneLabel: 'Drop a file here',
35
- dropzoneActive: 'Drop to upload',
36
- detectedFormat: 'Format',
37
- statsPreview: 'Preview',
38
- importButton: 'Import',
39
- importing: 'Importing…',
40
- importConfirmTitle: 'Confirm',
41
- importConfirmMessage: 'This will create new resources.',
42
- confirmImport: 'Proceed',
43
- cancelImport: 'Cancel',
44
- },
45
- progress: {
46
- phaseStarted: 'Starting…',
47
- phaseComplete: 'Complete',
48
- phaseError: 'Failed',
49
- statsEventsReplayed: 'Events replayed',
50
- statsResourcesCreated: 'Resources created',
51
- statsAnnotationsCreated: 'Annotations created',
52
- statsEntityTypesAdded: 'Entity types added',
53
- },
54
- },
55
- ToolbarPanels: () => <div data-testid="toolbar-panels" />,
56
- Toolbar: () => <div data-testid="toolbar" />,
57
- ...overrides,
58
- });
59
-
60
- describe('LinkedDataPage', () => {
61
- it('renders page title and subtitle', () => {
62
- render(<LinkedDataPage {...createProps()} />);
63
- expect(screen.getByText('Export & Import')).toBeInTheDocument();
64
- expect(screen.getByText('Exchange knowledge as JSON-LD Linked Data')).toBeInTheDocument();
65
- });
66
-
67
- it('renders ExportCard', () => {
68
- render(<LinkedDataPage {...createProps()} />);
69
- expect(screen.getByRole('heading', { name: 'Export' })).toBeInTheDocument();
70
- expect(screen.getByText('Export as JSON-LD')).toBeInTheDocument();
71
- });
72
-
73
- it('renders ImportCard', () => {
74
- render(<LinkedDataPage {...createProps()} />);
75
- expect(screen.getByRole('heading', { name: 'Import' })).toBeInTheDocument();
76
- expect(screen.getByText('Import from JSON-LD')).toBeInTheDocument();
77
- });
78
-
79
- it('renders toolbar components', () => {
80
- render(<LinkedDataPage {...createProps()} />);
81
- expect(screen.getByTestId('toolbar-panels')).toBeInTheDocument();
82
- expect(screen.getByTestId('toolbar')).toBeInTheDocument();
83
- });
84
-
85
- it('does not render ImportProgress when importPhase is null', () => {
86
- const { container } = render(<LinkedDataPage {...createProps()} />);
87
- expect(container.querySelector('.semiont-exchange__progress')).not.toBeInTheDocument();
88
- });
89
-
90
- it('renders ImportProgress when importPhase is set', () => {
91
- render(<LinkedDataPage {...createProps({ importPhase: 'started' })} />);
92
- expect(screen.getByText('Starting…')).toBeInTheDocument();
93
- });
94
-
95
- it('renders ImportProgress with result on completion', () => {
96
- render(<LinkedDataPage {...createProps({
97
- importPhase: 'complete',
98
- importResult: { stats: { resourcesCreated: 5, annotationsCreated: 12, entityTypesAdded: 3 } },
99
- })} />);
100
- expect(screen.getByText('Complete')).toBeInTheDocument();
101
- expect(screen.getByText('5')).toBeInTheDocument();
102
- expect(screen.getByText('Resources created')).toBeInTheDocument();
103
- expect(screen.getByText('12')).toBeInTheDocument();
104
- expect(screen.getByText('Annotations created')).toBeInTheDocument();
105
- expect(screen.getByText('3')).toBeInTheDocument();
106
- expect(screen.getByText('Entity types added')).toBeInTheDocument();
107
- });
108
-
109
- it('applies panel-open class when common panel is active', () => {
110
- const { container } = render(<LinkedDataPage {...createProps({ activePanel: 'user' })} />);
111
- expect(container.querySelector('.semiont-page--panel-open')).toBeInTheDocument();
112
- });
113
-
114
- it('does not apply panel-open class when no panel is active', () => {
115
- const { container } = render(<LinkedDataPage {...createProps({ activePanel: null })} />);
116
- expect(container.querySelector('.semiont-page--panel-open')).not.toBeInTheDocument();
117
- });
118
- });
@@ -1,118 +0,0 @@
1
- /**
2
- * LinkedDataPage — JSON-LD Export/Import moderation page
3
- *
4
- * Pure React component. All state and handlers passed as props.
5
- * Reuses ExportCard, ImportCard, and ImportProgress from admin-exchange.
6
- */
7
-
8
- import React from 'react';
9
- import { COMMON_PANELS, type ToolbarPanelType } from '../../../state/shell-state-unit';
10
- import { ExportCard, type ExportCardTranslations } from '../../admin-exchange/components/ExportCard';
11
- import { ImportCard, type ImportCardProps, type ImportCardTranslations } from '../../admin-exchange/components/ImportCard';
12
- import { ImportProgress, type ImportProgressTranslations } from '../../admin-exchange/components/ImportProgress';
13
-
14
- export interface LinkedDataPageTranslations {
15
- title: string;
16
- subtitle: string;
17
- export: ExportCardTranslations;
18
- import: ImportCardTranslations;
19
- progress: ImportProgressTranslations;
20
- }
21
-
22
- export interface LinkedDataPageProps {
23
- // Export
24
- onExport: () => void;
25
- isExporting: boolean;
26
-
27
- // Import
28
- onFileSelected: (file: File) => void;
29
- onImport: () => void;
30
- onCancelImport: () => void;
31
- selectedFile: File | null;
32
- preview: ImportCardProps['preview'];
33
- isImporting: boolean;
34
-
35
- // Progress
36
- importPhase: string | null;
37
- importMessage?: string | undefined;
38
- importResult?: Record<string, unknown> | undefined;
39
-
40
- // UI state
41
- theme: 'light' | 'dark' | 'system';
42
- activePanel: string | null;
43
-
44
- // Translations
45
- translations: LinkedDataPageTranslations;
46
-
47
- // Component dependencies
48
- ToolbarPanels: React.ComponentType<any>;
49
- Toolbar: React.ComponentType<any>;
50
- }
51
-
52
- export function LinkedDataPage({
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
- activePanel,
66
- translations: t,
67
- ToolbarPanels,
68
- Toolbar,
69
- }: LinkedDataPageProps) {
70
- return (
71
- <div className={`semiont-page${activePanel && COMMON_PANELS.includes(activePanel as ToolbarPanelType) ? ' semiont-page--panel-open' : ''}`}>
72
- <div className="semiont-page__content">
73
- <div className="semiont-page__header">
74
- <h1 className="semiont-page__title">{t.title}</h1>
75
- <p className="semiont-page__subtitle">{t.subtitle}</p>
76
- </div>
77
-
78
- <div className="semiont-exchange__cards">
79
- <ExportCard
80
- onExport={onExport}
81
- isExporting={isExporting}
82
- translations={t.export}
83
- />
84
-
85
- <ImportCard
86
- onFileSelected={onFileSelected}
87
- onImport={onImport}
88
- onCancel={onCancelImport}
89
- selectedFile={selectedFile}
90
- preview={preview}
91
- isImporting={isImporting}
92
- translations={t.import}
93
- />
94
- </div>
95
-
96
- {importPhase && (
97
- <ImportProgress
98
- phase={importPhase}
99
- message={importMessage}
100
- result={importResult}
101
- translations={t.progress}
102
- />
103
- )}
104
- </div>
105
-
106
- <div className="semiont-page__sidebar">
107
- <ToolbarPanels
108
- activePanel={activePanel}
109
- theme={theme}
110
- />
111
- <Toolbar
112
- context="simple"
113
- activePanel={activePanel}
114
- />
115
- </div>
116
- </div>
117
- );
118
- }