@semiont/react-ui 0.5.28 → 0.5.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/{PdfAnnotationCanvas.client-C2IGG4SW.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +4 -4
- package/dist/{ar-FABPTJVB.js → ar-AZBRADEB.js} +5 -1
- package/dist/{bn-LKP6BDMH.js → bn-CCEW6AOS.js} +5 -1
- package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
- package/dist/{chunk-2YHEE63S.js → chunk-G4VHL4UA.js} +31 -31
- package/dist/{chunk-2GOHTTHW.js → chunk-SDKFZLEX.js} +5 -1
- package/dist/{cs-YQYNWHRG.js → cs-HXPF2CTE.js} +5 -1
- package/dist/{da-QHZOYKRY.js → da-K5NTK7LH.js} +5 -1
- package/dist/{de-GKA7RD7J.js → de-GMRQEDZI.js} +5 -1
- package/dist/{el-45T257AL.js → el-EUB4CI4G.js} +5 -1
- package/dist/{en-RVW5EQ66.js → en-R5XA26NF.js} +2 -2
- package/dist/{es-AVER7RU4.js → es-Z32DAZ6W.js} +5 -1
- package/dist/{fa-K3VPK3O2.js → fa-G7HN6B32.js} +5 -1
- package/dist/{fi-5NEKF7EB.js → fi-3YBLZO7I.js} +5 -1
- package/dist/{fr-FUMNDLBZ.js → fr-VUBH7BBS.js} +5 -1
- package/dist/{he-3JGNA3PR.js → he-WQQEY26B.js} +5 -1
- package/dist/{hi-IJY375DF.js → hi-HJZ6W4JB.js} +5 -1
- package/dist/{id-LWHKJ7ZD.js → id-ZOGZ37MI.js} +5 -1
- package/dist/index.d.ts +157 -257
- package/dist/index.js +763 -1209
- package/dist/{it-EBVOV4RW.js → it-DT6UW66F.js} +5 -1
- package/dist/{ja-KWRIX2NF.js → ja-RUKQCOLO.js} +5 -1
- package/dist/{ko-L5CJMFDS.js → ko-ZH5ADKZM.js} +5 -1
- package/dist/{ms-LSNMJDR2.js → ms-Y2AFPZTU.js} +5 -1
- package/dist/{nl-ZB32MB7U.js → nl-OQEWIXNA.js} +5 -1
- package/dist/{no-4GTNY7M5.js → no-KPAYJZWY.js} +5 -1
- package/dist/{pl-LUHBLTHB.js → pl-WSXK2UUD.js} +5 -1
- package/dist/{pt-C4H3UNQ6.js → pt-NWRJRUCK.js} +5 -1
- package/dist/{ro-GGITS5V4.js → ro-3CGVMOC6.js} +5 -1
- package/dist/{sv-T56RTLBP.js → sv-HGSEBA7S.js} +5 -1
- package/dist/test-utils.js +16 -7
- package/dist/{th-22X2IWCB.js → th-NZ4G352Z.js} +5 -1
- package/dist/{tr-77TFG62H.js → tr-LGGS4DLW.js} +5 -1
- package/dist/{uk-MIIL6NXT.js → uk-AIX536L4.js} +5 -1
- package/dist/{vi-7JYOIGNV.js → vi-V6CVQZLQ.js} +5 -1
- package/dist/{zh-ID6DRYU4.js → zh-AGKPGI3L.js} +5 -1
- package/package.json +13 -13
- package/src/components/StatusDisplay.tsx +13 -13
- package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
- package/src/components/modals/ComposeStep.tsx +19 -4
- package/src/components/modals/ConfigureGenerationStep.tsx +143 -15
- package/src/components/modals/ReferenceWizardModal.tsx +14 -9
- package/src/components/modals/ResourceGenerateModal.tsx +10 -5
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +171 -8
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +25 -1
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +24 -1
- package/src/components/modals/__tests__/WizardFooter.test.tsx +3 -5
- package/src/components/resource/BrowseView.tsx +3 -3
- package/src/components/resource/DownloadFileLink.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +2 -2
- package/src/components/resource/__tests__/download-link.test.tsx +6 -6
- package/src/components/resource/panels/ReferenceEntry.tsx +2 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +5 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -2
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -1
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
- package/src/features/auth/components/SignInForm.tsx +53 -53
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +2 -2
- package/src/features/resource-compose/components/ResourceComposePage.tsx +18 -3
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
- package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
- package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +16 -25
- package/src/features/resource-viewer/generation-options.ts +29 -0
- package/src/styles/index.css +0 -1
- package/translations/ar.json +4 -0
- package/translations/bn.json +4 -0
- package/translations/cs.json +4 -0
- package/translations/da.json +4 -0
- package/translations/de.json +4 -0
- package/translations/el.json +4 -0
- package/translations/en.json +4 -0
- package/translations/es.json +4 -0
- package/translations/fa.json +4 -0
- package/translations/fi.json +4 -0
- package/translations/fr.json +4 -0
- package/translations/he.json +4 -0
- package/translations/hi.json +4 -0
- package/translations/id.json +4 -0
- package/translations/it.json +4 -0
- package/translations/ja.json +4 -0
- package/translations/ko.json +4 -0
- package/translations/ms.json +4 -0
- package/translations/nl.json +4 -0
- package/translations/no.json +4 -0
- package/translations/pl.json +4 -0
- package/translations/pt.json +4 -0
- package/translations/ro.json +4 -0
- package/translations/sv.json +4 -0
- package/translations/th.json +4 -0
- package/translations/tr.json +4 -0
- package/translations/uk.json +4 -0
- package/translations/vi.json +4 -0
- package/translations/zh.json +4 -0
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
- package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
- package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
- package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
- package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
- package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
- package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
- package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
- package/src/styles/features/exchange.css +0 -374
|
@@ -1,183 +0,0 @@
|
|
|
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
|
-
import type { ImportPreview } from '../state/exchange-state-unit';
|
|
9
|
-
export type { ImportPreview };
|
|
10
|
-
|
|
11
|
-
export interface ImportCardTranslations {
|
|
12
|
-
title: string;
|
|
13
|
-
description: string;
|
|
14
|
-
dropzoneLabel: string;
|
|
15
|
-
dropzoneActive: string;
|
|
16
|
-
detectedFormat: string;
|
|
17
|
-
statsPreview: string;
|
|
18
|
-
importButton: string;
|
|
19
|
-
importing: string;
|
|
20
|
-
importConfirmTitle: string;
|
|
21
|
-
importConfirmMessage: string;
|
|
22
|
-
confirmImport: string;
|
|
23
|
-
cancelImport: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ImportCardProps {
|
|
27
|
-
onFileSelected: (file: File) => void;
|
|
28
|
-
onImport: () => void;
|
|
29
|
-
onCancel: () => void;
|
|
30
|
-
selectedFile: File | null;
|
|
31
|
-
preview: ImportPreview | null;
|
|
32
|
-
isImporting: boolean;
|
|
33
|
-
translations: ImportCardTranslations;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function ImportCard({
|
|
37
|
-
onFileSelected,
|
|
38
|
-
onImport,
|
|
39
|
-
onCancel,
|
|
40
|
-
selectedFile,
|
|
41
|
-
preview,
|
|
42
|
-
isImporting,
|
|
43
|
-
translations: t,
|
|
44
|
-
}: ImportCardProps) {
|
|
45
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
46
|
-
const [isDragActive, setIsDragActive] = useState(false);
|
|
47
|
-
const [showConfirm, setShowConfirm] = useState(false);
|
|
48
|
-
|
|
49
|
-
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
50
|
-
e.preventDefault();
|
|
51
|
-
setIsDragActive(true);
|
|
52
|
-
}, []);
|
|
53
|
-
|
|
54
|
-
const handleDragLeave = useCallback(() => {
|
|
55
|
-
setIsDragActive(false);
|
|
56
|
-
}, []);
|
|
57
|
-
|
|
58
|
-
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
setIsDragActive(false);
|
|
61
|
-
setShowConfirm(false);
|
|
62
|
-
const file = e.dataTransfer.files[0];
|
|
63
|
-
if (file) onFileSelected(file);
|
|
64
|
-
}, [onFileSelected]);
|
|
65
|
-
|
|
66
|
-
const handleFileChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
67
|
-
setShowConfirm(false);
|
|
68
|
-
const file = e.target.files?.[0];
|
|
69
|
-
if (file) onFileSelected(file);
|
|
70
|
-
}, [onFileSelected]);
|
|
71
|
-
|
|
72
|
-
const handleImportClick = () => {
|
|
73
|
-
setShowConfirm(true);
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const handleConfirm = () => {
|
|
77
|
-
setShowConfirm(false);
|
|
78
|
-
onImport();
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const handleCancelConfirm = () => {
|
|
82
|
-
setShowConfirm(false);
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<div className="semiont-admin__card">
|
|
87
|
-
<h2 className="semiont-admin__section-title">{t.title}</h2>
|
|
88
|
-
<p className="semiont-admin__section-description">{t.description}</p>
|
|
89
|
-
|
|
90
|
-
<div
|
|
91
|
-
className={`semiont-exchange__dropzone${isDragActive ? ' semiont-exchange__dropzone--active' : ''}`}
|
|
92
|
-
onDragOver={handleDragOver}
|
|
93
|
-
onDragLeave={handleDragLeave}
|
|
94
|
-
onDrop={handleDrop}
|
|
95
|
-
onClick={() => fileInputRef.current?.click()}
|
|
96
|
-
>
|
|
97
|
-
<p className="semiont-exchange__dropzone-text">
|
|
98
|
-
{isDragActive ? t.dropzoneActive : t.dropzoneLabel}
|
|
99
|
-
</p>
|
|
100
|
-
{selectedFile && (
|
|
101
|
-
<p className="semiont-exchange__file-info">
|
|
102
|
-
{selectedFile.name} ({(selectedFile.size / 1024).toFixed(1)} KB)
|
|
103
|
-
</p>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<input
|
|
108
|
-
ref={fileInputRef}
|
|
109
|
-
type="file"
|
|
110
|
-
accept=".tar.gz,.jsonl,.gz"
|
|
111
|
-
onChange={handleFileChange}
|
|
112
|
-
style={{ display: 'none' }}
|
|
113
|
-
/>
|
|
114
|
-
|
|
115
|
-
{preview && (
|
|
116
|
-
<div className="semiont-exchange__preview">
|
|
117
|
-
<div className="semiont-exchange__preview-row">
|
|
118
|
-
<span className="semiont-exchange__preview-label">{t.detectedFormat}</span>
|
|
119
|
-
<span className="semiont-exchange__preview-value">
|
|
120
|
-
{preview.format} v{preview.version}
|
|
121
|
-
</span>
|
|
122
|
-
</div>
|
|
123
|
-
{preview.sourceUrl && (
|
|
124
|
-
<div className="semiont-exchange__preview-row">
|
|
125
|
-
<span className="semiont-exchange__preview-label">Source</span>
|
|
126
|
-
<span className="semiont-exchange__preview-value">{preview.sourceUrl}</span>
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
{Object.keys(preview.stats).length > 0 && (
|
|
130
|
-
<div className="semiont-exchange__preview-row">
|
|
131
|
-
<span className="semiont-exchange__preview-label">{t.statsPreview}</span>
|
|
132
|
-
<span className="semiont-exchange__preview-value">
|
|
133
|
-
{Object.entries(preview.stats).map(([k, v]) => `${v} ${k}`).join(', ')}
|
|
134
|
-
</span>
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
{showConfirm && (
|
|
141
|
-
<div className="semiont-exchange__confirm">
|
|
142
|
-
<p className="semiont-exchange__confirm-title">{t.importConfirmTitle}</p>
|
|
143
|
-
<p className="semiont-exchange__confirm-message">{t.importConfirmMessage}</p>
|
|
144
|
-
<div className="semiont-exchange__confirm-actions">
|
|
145
|
-
<button
|
|
146
|
-
className="semiont-exchange__button semiont-exchange__button--primary"
|
|
147
|
-
onClick={handleConfirm}
|
|
148
|
-
>
|
|
149
|
-
{t.confirmImport}
|
|
150
|
-
</button>
|
|
151
|
-
<button
|
|
152
|
-
className="semiont-exchange__button semiont-exchange__button--secondary"
|
|
153
|
-
onClick={handleCancelConfirm}
|
|
154
|
-
>
|
|
155
|
-
{t.cancelImport}
|
|
156
|
-
</button>
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
)}
|
|
160
|
-
|
|
161
|
-
{!showConfirm && (
|
|
162
|
-
<div className="semiont-exchange__actions">
|
|
163
|
-
<button
|
|
164
|
-
className="semiont-exchange__button semiont-exchange__button--primary"
|
|
165
|
-
onClick={handleImportClick}
|
|
166
|
-
disabled={!preview || isImporting}
|
|
167
|
-
>
|
|
168
|
-
{isImporting ? t.importing : t.importButton}
|
|
169
|
-
</button>
|
|
170
|
-
{selectedFile && (
|
|
171
|
-
<button
|
|
172
|
-
className="semiont-exchange__button semiont-exchange__button--secondary"
|
|
173
|
-
onClick={onCancel}
|
|
174
|
-
disabled={isImporting}
|
|
175
|
-
>
|
|
176
|
-
{t.cancelImport}
|
|
177
|
-
</button>
|
|
178
|
-
)}
|
|
179
|
-
</div>
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
phaseComplete: string;
|
|
10
|
-
phaseError: string;
|
|
11
|
-
statsEventsReplayed: string;
|
|
12
|
-
statsResourcesCreated: string;
|
|
13
|
-
statsAnnotationsCreated: string;
|
|
14
|
-
statsEntityTypesAdded: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ImportProgressProps {
|
|
18
|
-
phase: string;
|
|
19
|
-
message?: string;
|
|
20
|
-
result?: Record<string, unknown>;
|
|
21
|
-
translations: ImportProgressTranslations;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const PHASE_LABELS: Record<string, keyof ImportProgressTranslations> = {
|
|
25
|
-
started: 'phaseStarted',
|
|
26
|
-
complete: 'phaseComplete',
|
|
27
|
-
error: 'phaseError',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const STAT_LABELS: Record<string, keyof ImportProgressTranslations> = {
|
|
31
|
-
eventsReplayed: 'statsEventsReplayed',
|
|
32
|
-
resourcesCreated: 'statsResourcesCreated',
|
|
33
|
-
annotationsCreated: 'statsAnnotationsCreated',
|
|
34
|
-
entityTypesAdded: 'statsEntityTypesAdded',
|
|
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
|
-
const stats = result?.stats != null && typeof result.stats === 'object'
|
|
45
|
-
? (result.stats as Record<string, unknown>)
|
|
46
|
-
: undefined;
|
|
47
|
-
const statEntries = stats
|
|
48
|
-
? Object.entries(stats).filter((entry): entry is [string, number] => typeof entry[1] === 'number')
|
|
49
|
-
: [];
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<div className="semiont-exchange__progress">
|
|
53
|
-
<div className={`semiont-exchange__phase-label${isError ? ' semiont-exchange__phase-label--error' : isComplete ? ' semiont-exchange__phase-label--complete' : ''}`}>
|
|
54
|
-
{phaseLabel}
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
{message && !isComplete && !isError && (
|
|
58
|
-
<p className="semiont-exchange__progress-message">{message}</p>
|
|
59
|
-
)}
|
|
60
|
-
|
|
61
|
-
{isComplete && statEntries.length > 0 && (
|
|
62
|
-
<div className="semiont-exchange__result">
|
|
63
|
-
{statEntries.map(([key, value]) => {
|
|
64
|
-
const statKey = STAT_LABELS[key];
|
|
65
|
-
return (
|
|
66
|
-
<div key={key} className="semiont-exchange__result-stat">
|
|
67
|
-
<span className="semiont-exchange__result-value">{value}</span>
|
|
68
|
-
<span className="semiont-exchange__result-label">{statKey ? t[statKey] : key}</span>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
{isError && message && (
|
|
76
|
-
<p className="semiont-exchange__error-message">{message}</p>
|
|
77
|
-
)}
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
@@ -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
|
-
}
|