@semiont/react-ui 0.5.25 → 0.5.27
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 +28 -4
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js +677 -0
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +1 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css +185 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +1 -0
- package/dist/{ar-CULRHLNG.js → ar-OU3DQGC6.js} +65 -7
- package/dist/ar-OU3DQGC6.js.map +1 -0
- package/dist/{bn-HYR7CEL2.js → bn-PHWCR6AZ.js} +65 -7
- package/dist/bn-PHWCR6AZ.js.map +1 -0
- package/dist/chunk-5VDTWRXJ.js +287 -0
- package/dist/chunk-5VDTWRXJ.js.map +1 -0
- package/dist/{chunk-TG2XQEC4.js → chunk-BTUG3VJJ.js} +18 -298
- package/dist/chunk-BTUG3VJJ.js.map +1 -0
- package/dist/{chunk-NH6L2GIL.js → chunk-PCRRBU7H.js} +65 -7
- package/dist/chunk-PCRRBU7H.js.map +1 -0
- package/dist/{cs-YBRWY7NH.js → cs-NEXYZ62I.js} +65 -7
- package/dist/cs-NEXYZ62I.js.map +1 -0
- package/dist/{da-PHDEOBV3.js → da-FAT43YCU.js} +65 -7
- package/dist/da-FAT43YCU.js.map +1 -0
- package/dist/{de-O4UCCVLJ.js → de-KRQ3WBCU.js} +65 -7
- package/dist/de-KRQ3WBCU.js.map +1 -0
- package/dist/{el-MVGO6WZV.js → el-Q4IU3EKC.js} +65 -7
- package/dist/el-Q4IU3EKC.js.map +1 -0
- package/dist/{en-2KQILFKQ.js → en-2KADE7N4.js} +6 -2
- package/dist/{es-USSLXQY7.js → es-2IO3R2MK.js} +65 -7
- package/dist/es-2IO3R2MK.js.map +1 -0
- package/dist/{fa-WMAREFWY.js → fa-Q5GOQFXX.js} +65 -7
- package/dist/fa-Q5GOQFXX.js.map +1 -0
- package/dist/{fi-B3NBNOTS.js → fi-R6NMRWBE.js} +65 -7
- package/dist/fi-R6NMRWBE.js.map +1 -0
- package/dist/{fr-TXYK5T5W.js → fr-QY4D5E6J.js} +65 -7
- package/dist/fr-QY4D5E6J.js.map +1 -0
- package/dist/{he-JCJACW5D.js → he-X6Y77XHY.js} +65 -7
- package/dist/he-X6Y77XHY.js.map +1 -0
- package/dist/{hi-VOQLZNJU.js → hi-XL7IOEQM.js} +65 -7
- package/dist/hi-XL7IOEQM.js.map +1 -0
- package/dist/{id-BFN5RZNZ.js → id-QZ4ZZS67.js} +65 -7
- package/dist/id-QZ4ZZS67.js.map +1 -0
- package/dist/index.css +241 -508
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +155 -54
- package/dist/index.js +1815 -1642
- package/dist/index.js.map +1 -1
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/integrations/css-modules-helper.js.map +1 -1
- package/dist/{it-TWWDHKKM.js → it-ME2Q7OB6.js} +65 -7
- package/dist/it-ME2Q7OB6.js.map +1 -0
- package/dist/{ja-YRQLEBFM.js → ja-HX7QASRI.js} +65 -7
- package/dist/ja-HX7QASRI.js.map +1 -0
- package/dist/{ko-PG6B3CRB.js → ko-EQO5AHAV.js} +65 -7
- package/dist/ko-EQO5AHAV.js.map +1 -0
- package/dist/{ms-7EZECVKM.js → ms-UILFSF3I.js} +65 -7
- package/dist/ms-UILFSF3I.js.map +1 -0
- package/dist/{nl-HP753YAP.js → nl-NNI45PMK.js} +65 -7
- package/dist/nl-NNI45PMK.js.map +1 -0
- package/dist/{no-JUD5WX3N.js → no-MAL5F6P6.js} +65 -7
- package/dist/no-MAL5F6P6.js.map +1 -0
- package/dist/{pl-RVUL4CBM.js → pl-4RMY6JAJ.js} +65 -7
- package/dist/pl-4RMY6JAJ.js.map +1 -0
- package/dist/{pt-RPOVD4QM.js → pt-VQH2X3QQ.js} +65 -7
- package/dist/pt-VQH2X3QQ.js.map +1 -0
- package/dist/{ro-DMYWQTRD.js → ro-XXCQO2OG.js} +65 -7
- package/dist/ro-XXCQO2OG.js.map +1 -0
- package/dist/{sv-DMZ4FGXD.js → sv-7Z6EJ6U4.js} +65 -7
- package/dist/sv-7Z6EJ6U4.js.map +1 -0
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.css.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-4HVBJBQ2.js → th-L3DXXYVQ.js} +65 -7
- package/dist/th-L3DXXYVQ.js.map +1 -0
- package/dist/{tr-6BDPU5ML.js → tr-P3DFVLFJ.js} +65 -7
- package/dist/tr-P3DFVLFJ.js.map +1 -0
- package/dist/{uk-TBZBT2TL.js → uk-7C5D3PXS.js} +65 -7
- package/dist/uk-7C5D3PXS.js.map +1 -0
- package/dist/{vi-UMKZKZL3.js → vi-ZLTLEO7P.js} +65 -7
- package/dist/vi-ZLTLEO7P.js.map +1 -0
- package/dist/{zh-YBJWJJYB.js → zh-5FHVWDIN.js} +65 -7
- package/dist/zh-5FHVWDIN.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AssistProgress.tsx +144 -134
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +35 -35
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +216 -148
- package/src/components/annotation/annotation-entries.css +0 -4
- package/src/components/annotation/annotations.css +1 -1
- package/src/components/annotation/references.css +0 -4
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
- package/src/components/layout/Header.css +0 -4
- package/src/components/layout/LeftSidebar.css +0 -4
- package/src/components/modals/ConfigureGenerationStep.tsx +109 -48
- package/src/components/modals/ConfigureSearchStep.tsx +23 -40
- package/src/components/modals/ReferenceWizardModal.tsx +55 -16
- package/src/components/modals/ResourceGenerateModal.tsx +24 -8
- package/src/components/modals/ResourceSearchModal.tsx +1 -1
- package/src/components/modals/SearchModal.css +5 -17
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +2 -20
- package/src/components/modals/WizardFooter.tsx +63 -0
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +255 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +72 -0
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +256 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +1 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/WizardFooter.test.tsx +235 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +162 -6
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +623 -232
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +764 -7
- package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
- package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
- package/src/components/resource/AnnotateView.tsx +16 -5
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +15 -4
- package/src/components/resource/ResourceViewer.tsx +23 -24
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +29 -3
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +33 -0
- package/src/components/resource/annotate-renderers.tsx +10 -1
- package/src/components/resource/browse-renderers.tsx +10 -1
- package/src/components/resource/event-formatting.ts +0 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
- package/src/components/resource/panels/AssessmentPanel.css +5 -6
- package/src/components/resource/panels/AssistSection.css +68 -271
- package/src/components/resource/panels/AssistSection.tsx +4 -4
- package/src/components/resource/panels/AssistShell.tsx +12 -4
- package/src/components/resource/panels/CollaborationPanel.css +47 -22
- package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
- package/src/components/resource/panels/CommentEntry.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.css +0 -16
- package/src/components/resource/panels/HighlightEntry.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.css +0 -7
- package/src/components/resource/panels/JsonLdPanel.css +0 -4
- package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +12 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +1 -1
- package/src/components/resource/panels/StatisticsPanel.css +0 -20
- package/src/components/resource/panels/TagEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.css +0 -4
- package/src/components/resource/panels/TaggingPanel.tsx +2 -10
- package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +34 -53
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +37 -1
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
- package/src/components/settings/SettingsPanel.css +0 -4
- package/src/features/auth/auth.css +9 -33
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +25 -1
- package/src/features/resource-compose/state/compose-page-state-unit.ts +11 -2
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
- package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +26 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +42 -21
- package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
- package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
- package/src/styles/core/buttons.css +3 -1
- package/src/styles/core/forms.css +0 -21
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +1 -5
- package/src/styles/core/progress.css +2 -34
- package/src/styles/core/sliders.css +0 -4
- package/src/styles/core/toggles.css +0 -4
- package/src/styles/features/compose.css +1 -1
- package/src/styles/features/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +1 -21
- package/src/styles/features/static-pages.css +11 -11
- package/src/styles/motivations/motivation-assessment.css +2 -2
- package/src/styles/motivations/motivation-comment.css +2 -2
- package/src/styles/motivations/motivation-reference.css +2 -2
- package/src/styles/panels/history-panel.css +0 -40
- package/src/styles/panels/user-panel.css +0 -3
- package/src/styles/patterns/panel-helpers.css +0 -27
- package/src/styles/patterns/panel-sections.css +0 -59
- package/src/styles/patterns/panels-base.css +4 -36
- package/src/styles/utilities/motion.css +15 -15
- package/src/styles/variables.css +29 -1
- package/translations/ar.json +59 -5
- package/translations/bn.json +59 -5
- package/translations/cs.json +59 -5
- package/translations/da.json +59 -5
- package/translations/de.json +59 -5
- package/translations/el.json +59 -5
- package/translations/en.json +59 -5
- package/translations/es.json +59 -5
- package/translations/fa.json +59 -5
- package/translations/fi.json +59 -5
- package/translations/fr.json +59 -5
- package/translations/he.json +59 -5
- package/translations/hi.json +59 -5
- package/translations/id.json +59 -5
- package/translations/it.json +59 -5
- package/translations/ja.json +59 -5
- package/translations/ko.json +59 -5
- package/translations/ms.json +59 -5
- package/translations/nl.json +59 -5
- package/translations/no.json +59 -5
- package/translations/pl.json +59 -5
- package/translations/pt.json +59 -5
- package/translations/ro.json +59 -5
- package/translations/sv.json +59 -5
- package/translations/th.json +59 -5
- package/translations/tr.json +59 -5
- package/translations/uk.json +59 -5
- package/translations/vi.json +59 -5
- package/translations/zh.json +59 -5
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js +0 -462
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
- package/dist/ar-CULRHLNG.js.map +0 -1
- package/dist/bn-HYR7CEL2.js.map +0 -1
- package/dist/chunk-NH6L2GIL.js.map +0 -1
- package/dist/chunk-TG2XQEC4.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-YBRWY7NH.js.map +0 -1
- package/dist/da-PHDEOBV3.js.map +0 -1
- package/dist/de-O4UCCVLJ.js.map +0 -1
- package/dist/el-MVGO6WZV.js.map +0 -1
- package/dist/es-USSLXQY7.js.map +0 -1
- package/dist/fa-WMAREFWY.js.map +0 -1
- package/dist/fi-B3NBNOTS.js.map +0 -1
- package/dist/fr-TXYK5T5W.js.map +0 -1
- package/dist/he-JCJACW5D.js.map +0 -1
- package/dist/hi-VOQLZNJU.js.map +0 -1
- package/dist/id-BFN5RZNZ.js.map +0 -1
- package/dist/it-TWWDHKKM.js.map +0 -1
- package/dist/ja-YRQLEBFM.js.map +0 -1
- package/dist/ko-PG6B3CRB.js.map +0 -1
- package/dist/ms-7EZECVKM.js.map +0 -1
- package/dist/nl-HP753YAP.js.map +0 -1
- package/dist/no-JUD5WX3N.js.map +0 -1
- package/dist/pl-RVUL4CBM.js.map +0 -1
- package/dist/pt-RPOVD4QM.js.map +0 -1
- package/dist/ro-DMYWQTRD.js.map +0 -1
- package/dist/sv-DMZ4FGXD.js.map +0 -1
- package/dist/th-4HVBJBQ2.js.map +0 -1
- package/dist/tr-6BDPU5ML.js.map +0 -1
- package/dist/uk-TBZBT2TL.js.map +0 -1
- package/dist/vi-UMKZKZL3.js.map +0 -1
- package/dist/zh-YBJWJJYB.js.map +0 -1
- package/src/integrations/tailwind-plugin.cjs +0 -224
- /package/dist/{en-2KQILFKQ.js.map → en-2KADE7N4.js.map} +0 -0
|
@@ -95,7 +95,9 @@ const renderWithEventBus = (component: React.ReactElement<{ session: SemiontSess
|
|
|
95
95
|
);
|
|
96
96
|
// The component is provider-free: inject the factory session so it is the
|
|
97
97
|
// SAME session whose client the tracker spies on / whose bus it subscribes.
|
|
98
|
-
|
|
98
|
+
// The injected session/client are returned too: a test that spies on what the
|
|
99
|
+
// panel calls has to spy on THIS client, not a second factory's.
|
|
100
|
+
return { ...render(React.cloneElement(component, { session }), { wrapper: Wrapper }), session, client };
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
describe('ResourceInfoPanel Component', () => {
|
|
@@ -359,6 +361,40 @@ describe('ResourceInfoPanel Component', () => {
|
|
|
359
361
|
expect(screen.getByText('urn:semiont:resource:abc123')).toBeInTheDocument();
|
|
360
362
|
});
|
|
361
363
|
|
|
364
|
+
it('a derived-from id is a LINK, and clicking it opens that resource', () => {
|
|
365
|
+
// Rendering the id is half the feature; the provenance chain is only
|
|
366
|
+
// walkable if the id navigates. The click goes through the session's
|
|
367
|
+
// client, so a panel handed no session must not throw either — the
|
|
368
|
+
// optional chain below is load-bearing, not defensive noise.
|
|
369
|
+
// `renderWithEventBus` injects its own per-test session via cloneElement,
|
|
370
|
+
// so the spy has to go on THAT client — one created here would never be
|
|
371
|
+
// the one the button calls.
|
|
372
|
+
const { client } = renderWithEventBus(
|
|
373
|
+
<ResourceInfoPanel
|
|
374
|
+
{...defaultProps}
|
|
375
|
+
wasDerivedFrom="urn:semiont:resource:abc123"
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
const openSpy = vi.spyOn(client.browse, 'openResource');
|
|
379
|
+
|
|
380
|
+
fireEvent.click(screen.getByText('urn:semiont:resource:abc123'));
|
|
381
|
+
expect(openSpy).toHaveBeenCalledTimes(1);
|
|
382
|
+
expect(String(openSpy.mock.calls[0]![0])).toBe('urn:semiont:resource:abc123');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('renders every id when the resource derives from several', () => {
|
|
386
|
+
renderWithEventBus(
|
|
387
|
+
<ResourceInfoPanel
|
|
388
|
+
{...defaultProps}
|
|
389
|
+
wasDerivedFrom={['urn:semiont:resource:a', 'urn:semiont:resource:b'] as never}
|
|
390
|
+
/>
|
|
391
|
+
);
|
|
392
|
+
// The separator lives inside the button (`{i > 0 && ', '}{id}`), so the
|
|
393
|
+
// second one's text node is ", urn:…:b" — matched loosely on purpose.
|
|
394
|
+
expect(screen.getByText(/urn:semiont:resource:a/)).toBeInTheDocument();
|
|
395
|
+
expect(screen.getByText(/urn:semiont:resource:b/)).toBeInTheDocument();
|
|
396
|
+
});
|
|
397
|
+
|
|
362
398
|
it('falls back to generator for attribution when wasAttributedTo is absent', () => {
|
|
363
399
|
renderWithEventBus(
|
|
364
400
|
<ResourceInfoPanel
|
|
@@ -53,19 +53,11 @@
|
|
|
53
53
|
margin-bottom: 0.5rem;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
[data-theme="dark"] .semiont-auth__welcome {
|
|
57
|
-
color: var(--semiont-text-primary);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
56
|
.semiont-auth__subtitle {
|
|
61
57
|
font-size: 1rem;
|
|
62
58
|
color: var(--semiont-text-secondary);
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
[data-theme="dark"] .semiont-auth__subtitle {
|
|
66
|
-
color: var(--semiont-text-secondary);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
/* Error display */
|
|
70
62
|
.semiont-auth__error-container {
|
|
71
63
|
margin-bottom: 1.5rem;
|
|
@@ -129,10 +121,6 @@
|
|
|
129
121
|
letter-spacing: 0.05em;
|
|
130
122
|
}
|
|
131
123
|
|
|
132
|
-
[data-theme="dark"] .semiont-auth__divider-text {
|
|
133
|
-
color: var(--semiont-text-secondary);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
124
|
/* Info text */
|
|
137
125
|
.semiont-auth__info {
|
|
138
126
|
text-align: center;
|
|
@@ -227,10 +215,6 @@
|
|
|
227
215
|
margin-bottom: 0.5rem;
|
|
228
216
|
}
|
|
229
217
|
|
|
230
|
-
[data-theme="dark"] .semiont-auth__signup-title {
|
|
231
|
-
color: var(--semiont-text-primary);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
218
|
.semiont-auth__signup-content {
|
|
235
219
|
display: flex;
|
|
236
220
|
flex-direction: column;
|
|
@@ -244,10 +228,6 @@
|
|
|
244
228
|
line-height: 1.5;
|
|
245
229
|
}
|
|
246
230
|
|
|
247
|
-
[data-theme="dark"] .semiont-auth__signup-info {
|
|
248
|
-
color: var(--semiont-text-secondary);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
231
|
.semiont-auth__signup-footer {
|
|
252
232
|
text-align: center;
|
|
253
233
|
padding-top: 1rem;
|
|
@@ -316,12 +296,12 @@
|
|
|
316
296
|
}
|
|
317
297
|
|
|
318
298
|
.semiont-form__input-row:focus-visible {
|
|
319
|
-
outline: 2px solid var(--semiont-focus-ring
|
|
299
|
+
outline: 2px solid var(--semiont-focus-ring);
|
|
320
300
|
outline-offset: 2px;
|
|
321
301
|
}
|
|
322
302
|
|
|
323
303
|
[data-theme="dark"] .semiont-form__input-row:focus-visible {
|
|
324
|
-
outline-color: var(--semiont-focus-ring
|
|
304
|
+
outline-color: var(--semiont-focus-ring);
|
|
325
305
|
}
|
|
326
306
|
|
|
327
307
|
.semiont-form__input-row .semiont-input {
|
|
@@ -329,12 +309,12 @@
|
|
|
329
309
|
}
|
|
330
310
|
|
|
331
311
|
.semiont-form__input-row .semiont-input:focus-visible {
|
|
332
|
-
outline: 2px solid var(--semiont-focus-ring
|
|
312
|
+
outline: 2px solid var(--semiont-focus-ring);
|
|
333
313
|
outline-offset: 2px;
|
|
334
314
|
}
|
|
335
315
|
|
|
336
316
|
[data-theme="dark"] .semiont-form__input-row .semiont-input:focus-visible {
|
|
337
|
-
outline-color: var(--semiont-focus-ring
|
|
317
|
+
outline-color: var(--semiont-focus-ring);
|
|
338
318
|
}
|
|
339
319
|
|
|
340
320
|
.semiont-form__url-status {
|
|
@@ -345,27 +325,23 @@
|
|
|
345
325
|
}
|
|
346
326
|
|
|
347
327
|
.semiont-form__url-status--checking {
|
|
348
|
-
color: var(--semiont-text-
|
|
328
|
+
color: var(--semiont-text-tertiary);
|
|
349
329
|
animation: spin 1s linear infinite;
|
|
350
330
|
display: inline-block;
|
|
351
331
|
}
|
|
352
332
|
|
|
353
|
-
[data-theme="dark"] .semiont-form__url-status--checking {
|
|
354
|
-
color: var(--semiont-text-muted, #888888);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
333
|
.semiont-form__url-status--ok {
|
|
358
|
-
color: var(--semiont-success
|
|
334
|
+
color: var(--semiont-color-success);
|
|
359
335
|
}
|
|
360
336
|
|
|
361
337
|
[data-theme="dark"] .semiont-form__url-status--ok {
|
|
362
|
-
color: var(--semiont-success
|
|
338
|
+
color: var(--semiont-color-success-light);
|
|
363
339
|
}
|
|
364
340
|
|
|
365
341
|
.semiont-form__url-status--error {
|
|
366
|
-
color: var(--semiont-error
|
|
342
|
+
color: var(--semiont-color-error);
|
|
367
343
|
}
|
|
368
344
|
|
|
369
345
|
[data-theme="dark"] .semiont-form__url-status--error {
|
|
370
|
-
color: var(--semiont-error
|
|
346
|
+
color: var(--semiont-color-error-light);
|
|
371
347
|
}
|
|
@@ -83,7 +83,14 @@ describe('createComposePageStateUnit', () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it('parses storedContext in reference mode', async () => {
|
|
86
|
-
|
|
86
|
+
// A real `GatheredContext` shape. The old fixture here was
|
|
87
|
+
// `{ annotation, sourceContext }` — not a context at all — and it round-tripped
|
|
88
|
+
// because nothing checked. That is the hole the guard closes.
|
|
89
|
+
const context = {
|
|
90
|
+
focus: { kind: 'annotation', annotation: { id: 'ann-1' }, sourceResource: { id: 'doc-1' } },
|
|
91
|
+
graph: { nodes: [], edges: [] },
|
|
92
|
+
metadata: {},
|
|
93
|
+
};
|
|
87
94
|
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
88
95
|
annotationUri: 'ann-1',
|
|
89
96
|
sourceDocumentId: 'doc-1',
|
|
@@ -97,6 +104,23 @@ describe('createComposePageStateUnit', () => {
|
|
|
97
104
|
stateUnit.dispose();
|
|
98
105
|
});
|
|
99
106
|
|
|
107
|
+
it('ignores a stash that parses but is not a GatheredContext', async () => {
|
|
108
|
+
// The realistic case: an entry written by an older build, read back after a
|
|
109
|
+
// deploy. It used to sail through and reach `deriveViews`, which maps
|
|
110
|
+
// `graph.nodes` during render and throws instead of degrading.
|
|
111
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
112
|
+
annotationUri: 'ann-1',
|
|
113
|
+
sourceDocumentId: 'doc-1',
|
|
114
|
+
name: 'Ref',
|
|
115
|
+
storedContext: JSON.stringify({ annotation: { id: 'ann-1' }, sourceContext: 'text' }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Treated as NO stash: the ordinary gather path owns the outcome from here.
|
|
119
|
+
expect(await firstValueFrom(stateUnit.gatheredContext$)).toBeNull();
|
|
120
|
+
|
|
121
|
+
stateUnit.dispose();
|
|
122
|
+
});
|
|
123
|
+
|
|
100
124
|
it('ignores malformed storedContext', async () => {
|
|
101
125
|
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
102
126
|
annotationUri: 'ann-1',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
|
|
3
|
-
import { resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
|
|
3
|
+
import { isGatheredContext, resourceId as makeResourceId, annotationId as makeAnnotationId } from '@semiont/core';
|
|
4
4
|
import { createDisposer } from '@semiont/sdk';
|
|
5
5
|
import type { StateUnit } from '@semiont/core';
|
|
6
6
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
@@ -102,7 +102,16 @@ export function createComposePageStateUnit(
|
|
|
102
102
|
entityTypes,
|
|
103
103
|
});
|
|
104
104
|
if (params.storedContext) {
|
|
105
|
-
|
|
105
|
+
// The stash is the one place a `GatheredContext` arrives with its type
|
|
106
|
+
// history severed (sessionStorage → JSON.parse), so it is checked here and
|
|
107
|
+
// trusted everywhere after. A stale entry — a tab held open across a deploy
|
|
108
|
+
// — used to reach `deriveViews`, which maps `graph.nodes` during render and
|
|
109
|
+
// throws rather than degrading. A failed guard is treated as NO stash: the
|
|
110
|
+
// ordinary gather path runs, with the loading and error states it already has.
|
|
111
|
+
try {
|
|
112
|
+
const parsed: unknown = JSON.parse(params.storedContext);
|
|
113
|
+
if (isGatheredContext(parsed)) gatheredContext$.next(parsed);
|
|
114
|
+
} catch { /* ignore malformed */ }
|
|
106
115
|
}
|
|
107
116
|
loading$.next(false);
|
|
108
117
|
} else if (isCloneMode) {
|
|
@@ -45,6 +45,7 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
|
|
|
45
45
|
searchPlaceholder: 'Search resources...',
|
|
46
46
|
searchButton: 'Search',
|
|
47
47
|
searching: 'Searching...',
|
|
48
|
+
semanticFallbackNotice: 'No title matches — showing related documents.',
|
|
48
49
|
filterByEntityType: 'Filter by type',
|
|
49
50
|
all: 'All',
|
|
50
51
|
recentResources: 'Recent Resources',
|
|
@@ -70,6 +71,60 @@ const renderWithProviders = (ui: React.ReactElement) => {
|
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
describe('ResourceDiscoveryPage', () => {
|
|
74
|
+
// SEMANTIC-FALLBACK P3b / S11. The distinction is asserted through a stable
|
|
75
|
+
// hook, never the sentence: decision 3 set the wording and it will be revised
|
|
76
|
+
// from use, so a copy-pinned test would rot on the first edit and teach the
|
|
77
|
+
// next reader to weaken it.
|
|
78
|
+
describe('semantic fallback notice', () => {
|
|
79
|
+
const NOTICE = 'semiont-semantic-match-notice';
|
|
80
|
+
|
|
81
|
+
it('marks a semantic result set as distinct from a lexical one', () => {
|
|
82
|
+
const props = createMockProps({
|
|
83
|
+
searchQuery: 'kitten',
|
|
84
|
+
searchDocuments: [createMockResource('r1', 'All about cats')],
|
|
85
|
+
searchMatchKind: 'semantic',
|
|
86
|
+
});
|
|
87
|
+
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
88
|
+
|
|
89
|
+
expect(screen.getByTestId(NOTICE)).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('says nothing when the answer was lexical', () => {
|
|
93
|
+
const props = createMockProps({
|
|
94
|
+
searchQuery: 'cats',
|
|
95
|
+
searchDocuments: [createMockResource('r1', 'All about cats')],
|
|
96
|
+
searchMatchKind: 'lexical',
|
|
97
|
+
});
|
|
98
|
+
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
99
|
+
|
|
100
|
+
expect(screen.queryByTestId(NOTICE)).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('does not carry a stale label onto the no-query view', () => {
|
|
104
|
+
// Clearing the box returns to "recent", which no label describes; a
|
|
105
|
+
// leftover `semantic` must not annotate that list.
|
|
106
|
+
const props = createMockProps({
|
|
107
|
+
searchQuery: '',
|
|
108
|
+
searchDocuments: [],
|
|
109
|
+
searchMatchKind: 'semantic',
|
|
110
|
+
});
|
|
111
|
+
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
112
|
+
|
|
113
|
+
expect(screen.queryByTestId(NOTICE)).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('renders exactly as today when the prop is absent', () => {
|
|
117
|
+
const props = createMockProps({
|
|
118
|
+
searchQuery: 'cats',
|
|
119
|
+
searchDocuments: [createMockResource('r1', 'All about cats')],
|
|
120
|
+
});
|
|
121
|
+
renderWithProviders(<ResourceDiscoveryPage {...props} />);
|
|
122
|
+
|
|
123
|
+
expect(screen.queryByTestId(NOTICE)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
|
|
73
128
|
beforeEach(() => {
|
|
74
129
|
});
|
|
75
130
|
|
|
@@ -28,6 +28,16 @@ export interface ResourceDiscoveryPageProps {
|
|
|
28
28
|
recentError?: Error | null;
|
|
29
29
|
onRetryRecent?: () => void;
|
|
30
30
|
isSearching: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* How the current search answer was produced — `'lexical'` (title/metadata
|
|
33
|
+
* matching) or `'semantic'` (the vector fallback that runs only when lexical
|
|
34
|
+
* matching returned nothing). Undefined before the first answer, and on the
|
|
35
|
+
* no-query view where `recent` renders and no label describes it.
|
|
36
|
+
*
|
|
37
|
+
* Optional so hosts that never wire it — including external consumers —
|
|
38
|
+
* render exactly as before (SEMANTIC-FALLBACK P3b).
|
|
39
|
+
*/
|
|
40
|
+
searchMatchKind?: 'lexical' | 'semantic';
|
|
31
41
|
|
|
32
42
|
// Controlled search state
|
|
33
43
|
searchQuery: string;
|
|
@@ -53,6 +63,7 @@ export interface ResourceDiscoveryPageProps {
|
|
|
53
63
|
searchPlaceholder: string;
|
|
54
64
|
searchButton: string;
|
|
55
65
|
searching: string;
|
|
66
|
+
semanticFallbackNotice: string;
|
|
56
67
|
filterByEntityType: string;
|
|
57
68
|
all: string;
|
|
58
69
|
recentResources: string;
|
|
@@ -80,6 +91,7 @@ export function ResourceDiscoveryPage({
|
|
|
80
91
|
recentError = null,
|
|
81
92
|
onRetryRecent,
|
|
82
93
|
isSearching,
|
|
94
|
+
searchMatchKind,
|
|
83
95
|
searchQuery,
|
|
84
96
|
onSearchQueryChange,
|
|
85
97
|
selectedEntityType,
|
|
@@ -232,6 +244,23 @@ export function ResourceDiscoveryPage({
|
|
|
232
244
|
}
|
|
233
245
|
</h3>
|
|
234
246
|
|
|
247
|
+
{/* Decision 3: label it, minimally. Someone who searches "kitten"
|
|
248
|
+
and gets documents about cats otherwise has no way to know why,
|
|
249
|
+
and an unexplained answer is a worse trust outcome than the
|
|
250
|
+
empty page this replaced. Only ever appears where the
|
|
251
|
+
alternative was that empty page (S5), so it never competes with
|
|
252
|
+
a better answer. `aria-live` because the results it explains
|
|
253
|
+
arrive without any control being activated. */}
|
|
254
|
+
{hasSearchQuery && searchMatchKind === 'semantic' && searchDocuments.length > 0 && (
|
|
255
|
+
<p
|
|
256
|
+
className="semiont-card__documents-note"
|
|
257
|
+
data-testid="semiont-semantic-match-notice"
|
|
258
|
+
aria-live="polite"
|
|
259
|
+
>
|
|
260
|
+
{t.semanticFallbackNotice}
|
|
261
|
+
</p>
|
|
262
|
+
)}
|
|
263
|
+
|
|
235
264
|
{showNoResultsWarning && (
|
|
236
265
|
<div className="semiont-card__warning">
|
|
237
266
|
<p className="semiont-card__warning-text">
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
2
2
|
import { asStates } from '../../../../__tests__/test-client';
|
|
3
3
|
import { sessionOf } from '../../../../__tests__/test-client';
|
|
4
4
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
5
|
-
import { filter, skip, take, toArray } from 'rxjs/operators';
|
|
5
|
+
import { filter, map, skip, take, toArray } from 'rxjs/operators';
|
|
6
6
|
import type { SemiontClient } from '@semiont/sdk';
|
|
7
7
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
8
8
|
import { createDiscoverStateUnit } from '../discover-state-unit';
|
|
@@ -23,6 +23,8 @@ function mockClient(overrides: {
|
|
|
23
23
|
resources$?: BehaviorSubject<unknown[] | undefined>;
|
|
24
24
|
entityTypes$?: BehaviorSubject<string[] | undefined>;
|
|
25
25
|
resourcesFn?: (filters: BrowseFilters) => BehaviorSubject<unknown[] | undefined>;
|
|
26
|
+
/** What the stubbed list envelope reports as its match kind. */
|
|
27
|
+
matchKind?: 'lexical' | 'semantic';
|
|
26
28
|
} = {}): { client: SemiontClient; resourceCalls: BrowseFilters[] } {
|
|
27
29
|
const resourceCalls: BrowseFilters[] = [];
|
|
28
30
|
const defaultResources$ =
|
|
@@ -36,7 +38,11 @@ function mockClient(overrides: {
|
|
|
36
38
|
browse: {
|
|
37
39
|
resources: (filters: BrowseFilters = {}) => {
|
|
38
40
|
resourceCalls.push(filters);
|
|
39
|
-
return asStates(resourcesFn(filters).asObservable()
|
|
41
|
+
return asStates(resourcesFn(filters).asObservable().pipe(
|
|
42
|
+
// Arrays get the list envelope `resources()` now emits; explicit
|
|
43
|
+
// status objects (B15 fixtures) and `undefined` pass through.
|
|
44
|
+
map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: overrides.matchKind ?? 'lexical' } : v)),
|
|
45
|
+
));
|
|
40
46
|
},
|
|
41
47
|
entityTypes: () => asStates(entityTypes$.asObservable()),
|
|
42
48
|
},
|
|
@@ -45,6 +51,50 @@ function mockClient(overrides: {
|
|
|
45
51
|
return { client, resourceCalls };
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
describe('createDiscoverStateUnit — search match kind (SEMANTIC-FALLBACK P3b)', () => {
|
|
55
|
+
// S10 is tier-agnostic: the label and the resources it describes must arrive
|
|
56
|
+
// as ONE value. Exposing `matchKind$` as a second observable beside
|
|
57
|
+
// `state$.results` would satisfy the component and still let a render pair
|
|
58
|
+
// this query's label with the previous query's list.
|
|
59
|
+
it('emits matchKind in the SAME value as the results it labels', async () => {
|
|
60
|
+
vi.useFakeTimers();
|
|
61
|
+
try {
|
|
62
|
+
const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'sem-1' }]);
|
|
63
|
+
const { client } = mockClient({ resourcesFn: () => results$, matchKind: 'semantic' });
|
|
64
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
65
|
+
|
|
66
|
+
// Subscribe BEFORE setQuery: `queryInput$` is a plain Subject, so a value
|
|
67
|
+
// pushed with nothing listening is gone.
|
|
68
|
+
const collected: Array<{ results: unknown[]; matchKind?: string }> = [];
|
|
69
|
+
const sub = stateUnit.search.state$.subscribe((st) => collected.push(st));
|
|
70
|
+
|
|
71
|
+
stateUnit.search.setQuery('kitten');
|
|
72
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
73
|
+
|
|
74
|
+
const last = collected.at(-1)!;
|
|
75
|
+
expect(last.results).toEqual([{ '@id': 'sem-1' }]);
|
|
76
|
+
expect(last.matchKind).toBe('semantic');
|
|
77
|
+
|
|
78
|
+
sub.unsubscribe();
|
|
79
|
+
stateUnit.dispose();
|
|
80
|
+
} finally {
|
|
81
|
+
vi.useRealTimers();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('carries no matchKind on the empty-query view', async () => {
|
|
86
|
+
// Clearing the box returns to `recent`, which no label describes.
|
|
87
|
+
const { client } = mockClient();
|
|
88
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
89
|
+
|
|
90
|
+
const first = await firstValueFrom(stateUnit.search.state$);
|
|
91
|
+
expect(first.results).toEqual([]);
|
|
92
|
+
expect(first.matchKind).toBeUndefined();
|
|
93
|
+
|
|
94
|
+
stateUnit.dispose();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
48
98
|
describe('createDiscoverStateUnit', () => {
|
|
49
99
|
it('exposes recent resources from browse namespace', async () => {
|
|
50
100
|
const { client } = mockClient();
|
|
@@ -201,7 +251,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
201
251
|
stateUnit.search.setQuery('lincoln');
|
|
202
252
|
await vi.advanceTimersByTimeAsync(300);
|
|
203
253
|
|
|
204
|
-
expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false });
|
|
254
|
+
expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false, matchKind: 'lexical' });
|
|
205
255
|
|
|
206
256
|
sub.unsubscribe();
|
|
207
257
|
stateUnit.dispose();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
|
|
2
|
-
import { readyValue } from '@semiont/sdk';
|
|
2
|
+
import { readyValue, type CacheState } from '@semiont/sdk';
|
|
3
3
|
import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
|
|
4
4
|
import type { ResourceDescriptor } from '@semiont/core';
|
|
5
5
|
import type { SemiontSession } from '@semiont/sdk';
|
|
@@ -14,7 +14,18 @@ const DEBOUNCE_MS = 250;
|
|
|
14
14
|
|
|
15
15
|
export interface DiscoverSearchPipeline {
|
|
16
16
|
query$: Observable<string>;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The page AND the label that describes it, as one value. `matchKind` is
|
|
19
|
+
* carried here rather than beside it because SEMANTIC-FALLBACK S10 is
|
|
20
|
+
* tier-agnostic: two separately-observable pieces of state let a render pair
|
|
21
|
+
* this query's label with the previous query's list. Undefined only before
|
|
22
|
+
* the first answer (and while a query is empty), never as a third kind.
|
|
23
|
+
*/
|
|
24
|
+
state$: Observable<{
|
|
25
|
+
results: ResourceDescriptor[];
|
|
26
|
+
isSearching: boolean;
|
|
27
|
+
matchKind?: 'lexical' | 'semantic';
|
|
28
|
+
}>;
|
|
18
29
|
setQuery(value: string): void;
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -58,7 +69,11 @@ export function createDiscoverStateUnit(
|
|
|
58
69
|
limit: RECENT_LIMIT,
|
|
59
70
|
archived: false,
|
|
60
71
|
...(et ? { entityType: et } : {}),
|
|
61
|
-
})
|
|
72
|
+
}).pipe(
|
|
73
|
+
// This unit renders only the page; project it out of the list
|
|
74
|
+
// envelope (`matchKind` rendering is SEMANTIC-FALLBACK P3b's).
|
|
75
|
+
map((st): CacheState<ResourceDescriptor[]> => (st.status === 'ready' ? { status: 'ready', value: st.value.resources } : st)),
|
|
76
|
+
),
|
|
62
77
|
),
|
|
63
78
|
),
|
|
64
79
|
[],
|
|
@@ -74,11 +89,13 @@ export function createDiscoverStateUnit(
|
|
|
74
89
|
distinctUntilChanged(),
|
|
75
90
|
);
|
|
76
91
|
|
|
77
|
-
const state$:
|
|
92
|
+
const state$: DiscoverSearchPipeline['state$'] =
|
|
78
93
|
combineLatest([debouncedQuery$, selectedEntityType$]).pipe(
|
|
79
94
|
switchMap(([q, et]) => {
|
|
80
95
|
const trimmed = q.trim();
|
|
81
96
|
if (!trimmed) {
|
|
97
|
+
// No query, no answer to label: `recent` is what renders, and no
|
|
98
|
+
// matchKind describes it.
|
|
82
99
|
return of({ results: [] as ResourceDescriptor[], isSearching: false });
|
|
83
100
|
}
|
|
84
101
|
return client.browse
|
|
@@ -88,10 +105,14 @@ export function createDiscoverStateUnit(
|
|
|
88
105
|
...(et ? { entityType: et } : {}),
|
|
89
106
|
})
|
|
90
107
|
.pipe(
|
|
91
|
-
map((st) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
map((st) => {
|
|
109
|
+
const ready = readyValue(st);
|
|
110
|
+
return {
|
|
111
|
+
results: ready?.resources ?? [],
|
|
112
|
+
isSearching: st.status === 'pending',
|
|
113
|
+
...(ready ? { matchKind: ready.matchKind } : {}),
|
|
114
|
+
};
|
|
115
|
+
}),
|
|
95
116
|
startWith({ results: [] as ResourceDescriptor[], isSearching: true }),
|
|
96
117
|
);
|
|
97
118
|
}),
|
|
@@ -123,6 +123,15 @@ vi.mock('../../../contexts/ResourceAnnotationsContext', () => ({
|
|
|
123
123
|
ResourceAnnotationsProvider: ({ children }: any) => children,
|
|
124
124
|
}));
|
|
125
125
|
|
|
126
|
+
// Capture what ResourceViewerPage passes to useOutcomeToasts. The real hook is a
|
|
127
|
+
// pure event subscription, so standing it in costs the other specs nothing.
|
|
128
|
+
const outcomeToastsCalls = vi.hoisted(() => [] as unknown[]);
|
|
129
|
+
vi.mock('../../../hooks/useOutcomeToasts', () => ({
|
|
130
|
+
useOutcomeToasts: (resourceId: unknown) => {
|
|
131
|
+
outcomeToastsCalls.push(resourceId);
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
|
|
126
135
|
// Mock useEventSubscription at the direct path used by ResourceViewerPage
|
|
127
136
|
// (the barrel export mock doesn't intercept direct context imports)
|
|
128
137
|
const mockUseEventSubscriptions = vi.fn();
|
|
@@ -180,6 +189,23 @@ const renderWithProviders = (ui: React.ReactElement) => {
|
|
|
180
189
|
);
|
|
181
190
|
};
|
|
182
191
|
|
|
192
|
+
describe('ResourceViewerPage — outcome toasts reach the user', () => {
|
|
193
|
+
// The decline/success toast was dropped for EVERY resource:
|
|
194
|
+
// `useOutcomeToasts(resource.id as string)` read a property
|
|
195
|
+
// `ResourceDescriptor` does not declare (it carries `@id`). The descriptor's
|
|
196
|
+
// open index signature made the access legal and the `as string` silenced its
|
|
197
|
+
// `unknown` type, so the hook's `event.resourceId !== resourceId` guard
|
|
198
|
+
// compared a real id against `undefined` and returned early every time.
|
|
199
|
+
it('subscribes with the resource id, not a property the descriptor lacks', () => {
|
|
200
|
+
outcomeToastsCalls.length = 0;
|
|
201
|
+
renderWithProviders(<ResourceViewerPage {...createMockProps()} />);
|
|
202
|
+
|
|
203
|
+
expect(outcomeToastsCalls).not.toHaveLength(0);
|
|
204
|
+
// Must be the resource's identity — `undefined` here silences every toast.
|
|
205
|
+
expect(outcomeToastsCalls[0]).toBe('test-123');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
183
209
|
describe('ResourceViewerPage', () => {
|
|
184
210
|
beforeEach(() => {
|
|
185
211
|
});
|