@semiont/react-ui 0.5.22 → 0.5.24
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/dist/{ar-ZHKO4XDR.js → ar-JRTKCMTF.js} +11 -4
- package/dist/ar-JRTKCMTF.js.map +1 -0
- package/dist/{bn-3BUDKAK5.js → bn-LO6EDSCC.js} +11 -4
- package/dist/bn-LO6EDSCC.js.map +1 -0
- package/dist/{chunk-PFUXQPWJ.js → chunk-4QMTEPQ3.js} +92 -60
- package/dist/chunk-4QMTEPQ3.js.map +1 -0
- package/dist/{chunk-R52YAGHK.js → chunk-5Y5JDCKC.js} +11 -4
- package/dist/chunk-5Y5JDCKC.js.map +1 -0
- package/dist/{cs-2EFEUKTW.js → cs-LCHHOR4O.js} +11 -4
- package/dist/cs-LCHHOR4O.js.map +1 -0
- package/dist/{da-SUD3Q44Q.js → da-TARGPMJR.js} +11 -4
- package/dist/da-TARGPMJR.js.map +1 -0
- package/dist/{de-6IC5QRX7.js → de-SBH4QBPJ.js} +11 -4
- package/dist/de-SBH4QBPJ.js.map +1 -0
- package/dist/{el-Q4NAXFRC.js → el-2D2OLGWB.js} +11 -4
- package/dist/el-2D2OLGWB.js.map +1 -0
- package/dist/{en-P2L4X27N.js → en-QFIEG3CA.js} +2 -2
- package/dist/{es-YUKZBD4W.js → es-U6YK2LX6.js} +11 -4
- package/dist/es-U6YK2LX6.js.map +1 -0
- package/dist/{fa-OJ47LILE.js → fa-S7SZE37M.js} +11 -4
- package/dist/fa-S7SZE37M.js.map +1 -0
- package/dist/{fi-QFFL5CUB.js → fi-CBL3NIXN.js} +11 -4
- package/dist/fi-CBL3NIXN.js.map +1 -0
- package/dist/{fr-BOQZZWP4.js → fr-EABUHV6S.js} +11 -4
- package/dist/fr-EABUHV6S.js.map +1 -0
- package/dist/{he-LXDY3TFA.js → he-THWD244Y.js} +11 -4
- package/dist/he-THWD244Y.js.map +1 -0
- package/dist/{hi-HOJER77V.js → hi-GHGFZJ44.js} +11 -4
- package/dist/hi-GHGFZJ44.js.map +1 -0
- package/dist/{id-I5WQ6EWD.js → id-POZOUMDW.js} +11 -4
- package/dist/id-POZOUMDW.js.map +1 -0
- package/dist/index.d.ts +269 -69
- package/dist/index.js +2080 -1861
- package/dist/index.js.map +1 -1
- package/dist/{it-UYH642IJ.js → it-KDYJADSH.js} +11 -4
- package/dist/it-KDYJADSH.js.map +1 -0
- package/dist/{ja-JTVWAC4L.js → ja-Z5Z6ZF3P.js} +11 -4
- package/dist/ja-Z5Z6ZF3P.js.map +1 -0
- package/dist/{ko-IHXFMOIZ.js → ko-7X3XUXRN.js} +11 -4
- package/dist/ko-7X3XUXRN.js.map +1 -0
- package/dist/{ms-JH25VFLO.js → ms-BTWEIW43.js} +11 -4
- package/dist/ms-BTWEIW43.js.map +1 -0
- package/dist/{nl-SURXW7IQ.js → nl-IS2CGF5I.js} +11 -4
- package/dist/nl-IS2CGF5I.js.map +1 -0
- package/dist/{no-HXIKQFDZ.js → no-AAMEVA26.js} +11 -4
- package/dist/no-AAMEVA26.js.map +1 -0
- package/dist/{pl-7A4KU6XK.js → pl-PVXUQYFT.js} +11 -4
- package/dist/pl-PVXUQYFT.js.map +1 -0
- package/dist/{pt-6MGYP7Q5.js → pt-3ITQ76TL.js} +11 -4
- package/dist/pt-3ITQ76TL.js.map +1 -0
- package/dist/{ro-IROCHNIA.js → ro-FRB4IIVS.js} +11 -4
- package/dist/ro-FRB4IIVS.js.map +1 -0
- package/dist/{sv-WDJD75CV.js → sv-DS3RO24M.js} +11 -4
- package/dist/sv-DS3RO24M.js.map +1 -0
- package/dist/test-utils.js +9 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-RIO2E6B3.js → th-AX66MWWB.js} +11 -4
- package/dist/th-AX66MWWB.js.map +1 -0
- package/dist/{tr-TZ7ERCIN.js → tr-7KTB67TT.js} +11 -4
- package/dist/tr-7KTB67TT.js.map +1 -0
- package/dist/{uk-ZFWDKLUE.js → uk-SMQS2MAQ.js} +11 -4
- package/dist/uk-SMQS2MAQ.js.map +1 -0
- package/dist/{vi-UIOSP2JG.js → vi-D3JXL2EZ.js} +11 -4
- package/dist/vi-D3JXL2EZ.js.map +1 -0
- package/dist/{zh-6R6YUY46.js → zh-S754AWFS.js} +11 -4
- package/dist/zh-S754AWFS.js.map +1 -0
- package/package.json +4 -4
- package/src/components/annotation-popups/JsonLdView.tsx +1 -1
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +8 -2
- package/src/components/modals/ReferenceWizardModal.tsx +2 -1
- package/src/components/modals/ResourceGenerateModal.tsx +10 -4
- package/src/components/modals/ResourceSearchModal.tsx +3 -1
- package/src/components/modals/SearchModal.tsx +3 -1
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +5 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +2 -1
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +2 -1
- package/src/components/resource/AnnotateView.tsx +70 -138
- package/src/components/resource/AnnotationHistory.tsx +53 -17
- package/src/components/resource/BrowseView.tsx +3 -2
- package/src/components/resource/ResourceViewer.tsx +39 -22
- package/src/components/resource/__tests__/AnnotateView.registry.test.tsx +114 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +63 -41
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +2 -1
- package/src/components/resource/__tests__/ResourceViewer.multi-mount-invalidation.test.tsx +140 -0
- package/src/components/resource/__tests__/ResourceViewer.registry.test.tsx +89 -0
- package/src/components/resource/annotate-renderers.tsx +155 -0
- package/src/components/resource/panels/JsonLdPanel.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.tsx +38 -3
- package/src/components/resource/panels/TagEntry.tsx +5 -1
- package/src/components/resource/panels/TaggingPanel.tsx +6 -1
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +41 -0
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +8 -2
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +90 -0
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +2 -2
- package/src/components/resource/panels/__tests__/UnifiedAnnotationsPanel.failure.test.tsx +78 -0
- package/src/components/settings/SettingsPanel.tsx +6 -2
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +28 -3
- package/src/features/admin-devops/__tests__/AdminDevOpsPage.test.tsx +3 -11
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +0 -3
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -1
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -3
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +1 -1
- package/src/features/admin-security/__tests__/AdminSecurityPage.test.tsx +0 -8
- package/src/features/admin-security/components/AdminSecurityPage.tsx +0 -3
- package/src/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +10 -9
- package/src/features/admin-security/state/admin-security-state-unit.ts +3 -2
- package/src/features/admin-users/__tests__/AdminUsersPage.test.tsx +0 -1
- package/src/features/admin-users/components/AdminUsersPage.tsx +0 -3
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +7 -6
- package/src/features/admin-users/state/admin-users-state-unit.ts +3 -2
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +36 -26
- package/src/features/auth-welcome/state/welcome-state-unit.ts +7 -2
- package/src/features/moderate-entity-tags/__tests__/EntityTagsPage.test.tsx +0 -2
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +0 -3
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +37 -12
- package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +9 -10
- package/src/features/moderate-recent/__tests__/RecentDocumentsPage.test.tsx +3 -5
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +0 -3
- package/src/features/moderate-tag-schemas/__tests__/TagSchemasPage.test.tsx +3 -5
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +0 -3
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -3
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +17 -7
- package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -3
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +33 -12
- package/src/features/resource-compose/state/compose-page-state-unit.ts +9 -8
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +3 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +25 -3
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +90 -25
- package/src/features/resource-discovery/state/discover-state-unit.ts +30 -30
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +9 -5
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +70 -40
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +111 -8
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +167 -27
- package/src/features/resource-viewer/state/resource-loader-state-unit.ts +74 -9
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +29 -28
- package/translations/ar.json +10 -3
- package/translations/bn.json +10 -3
- package/translations/cs.json +10 -3
- package/translations/da.json +10 -3
- package/translations/de.json +10 -3
- package/translations/el.json +10 -3
- package/translations/en.json +10 -3
- package/translations/es.json +10 -3
- package/translations/fa.json +10 -3
- package/translations/fi.json +10 -3
- package/translations/fr.json +10 -3
- package/translations/he.json +10 -3
- package/translations/hi.json +10 -3
- package/translations/id.json +10 -3
- package/translations/it.json +10 -3
- package/translations/ja.json +10 -3
- package/translations/ko.json +10 -3
- package/translations/ms.json +10 -3
- package/translations/nl.json +10 -3
- package/translations/no.json +10 -3
- package/translations/pl.json +10 -3
- package/translations/pt.json +10 -3
- package/translations/ro.json +10 -3
- package/translations/sv.json +10 -3
- package/translations/th.json +10 -3
- package/translations/tr.json +10 -3
- package/translations/uk.json +10 -3
- package/translations/vi.json +10 -3
- package/translations/zh.json +10 -3
- package/dist/ar-ZHKO4XDR.js.map +0 -1
- package/dist/bn-3BUDKAK5.js.map +0 -1
- package/dist/chunk-PFUXQPWJ.js.map +0 -1
- package/dist/chunk-R52YAGHK.js.map +0 -1
- package/dist/cs-2EFEUKTW.js.map +0 -1
- package/dist/da-SUD3Q44Q.js.map +0 -1
- package/dist/de-6IC5QRX7.js.map +0 -1
- package/dist/el-Q4NAXFRC.js.map +0 -1
- package/dist/es-YUKZBD4W.js.map +0 -1
- package/dist/fa-OJ47LILE.js.map +0 -1
- package/dist/fi-QFFL5CUB.js.map +0 -1
- package/dist/fr-BOQZZWP4.js.map +0 -1
- package/dist/he-LXDY3TFA.js.map +0 -1
- package/dist/hi-HOJER77V.js.map +0 -1
- package/dist/id-I5WQ6EWD.js.map +0 -1
- package/dist/it-UYH642IJ.js.map +0 -1
- package/dist/ja-JTVWAC4L.js.map +0 -1
- package/dist/ko-IHXFMOIZ.js.map +0 -1
- package/dist/ms-JH25VFLO.js.map +0 -1
- package/dist/nl-SURXW7IQ.js.map +0 -1
- package/dist/no-HXIKQFDZ.js.map +0 -1
- package/dist/pl-7A4KU6XK.js.map +0 -1
- package/dist/pt-6MGYP7Q5.js.map +0 -1
- package/dist/ro-IROCHNIA.js.map +0 -1
- package/dist/sv-WDJD75CV.js.map +0 -1
- package/dist/th-RIO2E6B3.js.map +0 -1
- package/dist/tr-TZ7ERCIN.js.map +0 -1
- package/dist/uk-ZFWDKLUE.js.map +0 -1
- package/dist/vi-UIOSP2JG.js.map +0 -1
- package/dist/zh-6R6YUY46.js.map +0 -1
- /package/dist/{en-P2L4X27N.js.map → en-QFIEG3CA.js.map} +0 -0
|
@@ -20,6 +20,13 @@ export interface ResourceDiscoveryPageProps {
|
|
|
20
20
|
searchDocuments: ResourceDescriptor[];
|
|
21
21
|
entityTypes: string[];
|
|
22
22
|
isLoadingRecent: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The recent-resources load failed terminally (B15). Without it, "loading"
|
|
25
|
+
* derived from an absent value never resolves and this route freezes.
|
|
26
|
+
* See .plans/PANEL-FAILURE-STATES.md
|
|
27
|
+
*/
|
|
28
|
+
recentError?: Error | null;
|
|
29
|
+
onRetryRecent?: () => void;
|
|
23
30
|
isSearching: boolean;
|
|
24
31
|
|
|
25
32
|
// Controlled search state
|
|
@@ -33,7 +40,6 @@ export interface ResourceDiscoveryPageProps {
|
|
|
33
40
|
|
|
34
41
|
// UI state props
|
|
35
42
|
theme: 'light' | 'dark';
|
|
36
|
-
showLineNumbers: boolean;
|
|
37
43
|
activePanel: string | null;
|
|
38
44
|
|
|
39
45
|
// Navigation props
|
|
@@ -58,6 +64,8 @@ export interface ResourceDiscoveryPageProps {
|
|
|
58
64
|
archived: string;
|
|
59
65
|
created: string;
|
|
60
66
|
loadingKnowledgeBase: string;
|
|
67
|
+
recentFailed: string;
|
|
68
|
+
retry: string;
|
|
61
69
|
};
|
|
62
70
|
|
|
63
71
|
// Component dependencies
|
|
@@ -69,13 +77,14 @@ export function ResourceDiscoveryPage({
|
|
|
69
77
|
searchDocuments,
|
|
70
78
|
entityTypes,
|
|
71
79
|
isLoadingRecent,
|
|
80
|
+
recentError = null,
|
|
81
|
+
onRetryRecent,
|
|
72
82
|
isSearching,
|
|
73
83
|
searchQuery,
|
|
74
84
|
onSearchQueryChange,
|
|
75
85
|
selectedEntityType,
|
|
76
86
|
onSelectedEntityTypeChange,
|
|
77
87
|
theme,
|
|
78
|
-
showLineNumbers,
|
|
79
88
|
activePanel,
|
|
80
89
|
onNavigateToResource,
|
|
81
90
|
onNavigateToCompose,
|
|
@@ -116,6 +125,20 @@ export function ResourceDiscoveryPage({
|
|
|
116
125
|
}
|
|
117
126
|
}, []);
|
|
118
127
|
|
|
128
|
+
// A terminally failed load is not still loading — check it first.
|
|
129
|
+
if (recentError) {
|
|
130
|
+
return (
|
|
131
|
+
<div className="semiont-page__loading">
|
|
132
|
+
<p className="semiont-page__loading-text">{t.recentFailed}</p>
|
|
133
|
+
{onRetryRecent && (
|
|
134
|
+
<button type="button" onClick={onRetryRecent} className="semiont-button" data-variant="secondary">
|
|
135
|
+
{t.retry}
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
// Loading state
|
|
120
143
|
if (isLoadingRecent) {
|
|
121
144
|
return (
|
|
@@ -261,7 +284,6 @@ export function ResourceDiscoveryPage({
|
|
|
261
284
|
<ToolbarPanels
|
|
262
285
|
activePanel={activePanel}
|
|
263
286
|
theme={theme}
|
|
264
|
-
showLineNumbers={showLineNumbers}
|
|
265
287
|
/>
|
|
266
288
|
|
|
267
289
|
{/* Toolbar - Always visible on the right */}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { asStates } from '../../../../__tests__/test-client';
|
|
3
|
+
import { sessionOf } from '../../../../__tests__/test-client';
|
|
2
4
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
3
5
|
import { filter, skip, take, toArray } from 'rxjs/operators';
|
|
4
6
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
7
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
6
8
|
import { createDiscoverStateUnit } from '../discover-state-unit';
|
|
7
|
-
import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing';
|
|
9
|
+
import { assertStateUnitAxioms, disposeProbe } from '@semiont/core/testing/axioms';
|
|
8
10
|
|
|
9
11
|
function mockBrowse(): ShellStateUnit {
|
|
10
12
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
@@ -34,9 +36,9 @@ function mockClient(overrides: {
|
|
|
34
36
|
browse: {
|
|
35
37
|
resources: (filters: BrowseFilters = {}) => {
|
|
36
38
|
resourceCalls.push(filters);
|
|
37
|
-
return resourcesFn(filters).asObservable();
|
|
39
|
+
return asStates(resourcesFn(filters).asObservable());
|
|
38
40
|
},
|
|
39
|
-
entityTypes: () => entityTypes$.asObservable(),
|
|
41
|
+
entityTypes: () => asStates(entityTypes$.asObservable()),
|
|
40
42
|
},
|
|
41
43
|
} as unknown as SemiontClient;
|
|
42
44
|
|
|
@@ -46,9 +48,9 @@ function mockClient(overrides: {
|
|
|
46
48
|
describe('createDiscoverStateUnit', () => {
|
|
47
49
|
it('exposes recent resources from browse namespace', async () => {
|
|
48
50
|
const { client } = mockClient();
|
|
49
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
51
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
50
52
|
|
|
51
|
-
const recent = await firstValueFrom(stateUnit.
|
|
53
|
+
const recent = await firstValueFrom(stateUnit.recent.value$);
|
|
52
54
|
expect(recent).toEqual([{ '@id': 'r1' }]);
|
|
53
55
|
|
|
54
56
|
stateUnit.dispose();
|
|
@@ -56,9 +58,9 @@ describe('createDiscoverStateUnit', () => {
|
|
|
56
58
|
|
|
57
59
|
it('exposes entity types from browse namespace', async () => {
|
|
58
60
|
const { client } = mockClient();
|
|
59
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
61
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
60
62
|
|
|
61
|
-
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
63
|
+
const types = await firstValueFrom(stateUnit.entityTypes.value$);
|
|
62
64
|
expect(types).toEqual(['Person']);
|
|
63
65
|
|
|
64
66
|
stateUnit.dispose();
|
|
@@ -67,9 +69,9 @@ describe('createDiscoverStateUnit', () => {
|
|
|
67
69
|
it('falls back to [] when entityTypes() emits undefined', async () => {
|
|
68
70
|
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
69
71
|
const { client } = mockClient({ entityTypes$ });
|
|
70
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
72
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
71
73
|
|
|
72
|
-
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
74
|
+
const types = await firstValueFrom(stateUnit.entityTypes.value$);
|
|
73
75
|
expect(types).toEqual([]);
|
|
74
76
|
|
|
75
77
|
stateUnit.dispose();
|
|
@@ -78,13 +80,13 @@ describe('createDiscoverStateUnit', () => {
|
|
|
78
80
|
it('reports loading when resources are undefined', async () => {
|
|
79
81
|
const resources$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
80
82
|
const { client } = mockClient({ resources$ });
|
|
81
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
83
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
82
84
|
|
|
83
|
-
const loading = await firstValueFrom(stateUnit.
|
|
85
|
+
const loading = await firstValueFrom(stateUnit.recent.loading$);
|
|
84
86
|
expect(loading).toBe(true);
|
|
85
87
|
|
|
86
88
|
resources$.next([]);
|
|
87
|
-
const loaded = await firstValueFrom(stateUnit.
|
|
89
|
+
const loaded = await firstValueFrom(stateUnit.recent.loading$.pipe(filter((l) => !l)));
|
|
88
90
|
expect(loaded).toBe(false);
|
|
89
91
|
|
|
90
92
|
stateUnit.dispose();
|
|
@@ -92,7 +94,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
92
94
|
|
|
93
95
|
it('exposes a search pipeline', () => {
|
|
94
96
|
const { client } = mockClient();
|
|
95
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
97
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
96
98
|
|
|
97
99
|
expect(stateUnit.search).toBeDefined();
|
|
98
100
|
expect(typeof stateUnit.search.setQuery).toBe('function');
|
|
@@ -103,9 +105,9 @@ describe('createDiscoverStateUnit', () => {
|
|
|
103
105
|
|
|
104
106
|
it('initial selectedEntityType$ is empty and recent fetch carries no entityType', async () => {
|
|
105
107
|
const { client, resourceCalls } = mockClient();
|
|
106
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
108
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
107
109
|
|
|
108
|
-
await firstValueFrom(stateUnit.
|
|
110
|
+
await firstValueFrom(stateUnit.recent.value$);
|
|
109
111
|
|
|
110
112
|
expect(resourceCalls).toHaveLength(1);
|
|
111
113
|
expect(resourceCalls[0]).toEqual({ limit: 10, archived: false });
|
|
@@ -116,10 +118,10 @@ describe('createDiscoverStateUnit', () => {
|
|
|
116
118
|
|
|
117
119
|
it('setSelectedEntityType drives a refetch with the entityType filter', async () => {
|
|
118
120
|
const { client, resourceCalls } = mockClient();
|
|
119
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
121
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
120
122
|
|
|
121
123
|
// Prime the first subscription so the switchMap is live.
|
|
122
|
-
const sub = stateUnit.
|
|
124
|
+
const sub = stateUnit.recent.value$.subscribe();
|
|
123
125
|
|
|
124
126
|
stateUnit.setSelectedEntityType('Person');
|
|
125
127
|
|
|
@@ -136,7 +138,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
136
138
|
vi.useFakeTimers();
|
|
137
139
|
try {
|
|
138
140
|
const { client, resourceCalls } = mockClient();
|
|
139
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
141
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
140
142
|
|
|
141
143
|
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
142
144
|
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
@@ -161,7 +163,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
161
163
|
const { client, resourceCalls } = mockClient({
|
|
162
164
|
resourcesFn: (filters) => (filters.search ? results$ : new BehaviorSubject<unknown[] | undefined>([])),
|
|
163
165
|
});
|
|
164
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
166
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
165
167
|
|
|
166
168
|
const sub = stateUnit.search.state$.subscribe();
|
|
167
169
|
stateUnit.setSelectedEntityType('Person');
|
|
@@ -191,7 +193,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
191
193
|
const { client } = mockClient({
|
|
192
194
|
resourcesFn: () => results$,
|
|
193
195
|
});
|
|
194
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
196
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
195
197
|
|
|
196
198
|
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
197
199
|
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
@@ -215,7 +217,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
215
217
|
const { client } = mockClient({
|
|
216
218
|
resourcesFn: () => inflight$,
|
|
217
219
|
});
|
|
218
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
220
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
219
221
|
|
|
220
222
|
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
221
223
|
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
@@ -234,7 +236,7 @@ describe('createDiscoverStateUnit', () => {
|
|
|
234
236
|
|
|
235
237
|
it('search query observable echoes the latest setQuery value', async () => {
|
|
236
238
|
const { client } = mockClient();
|
|
237
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
239
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
238
240
|
|
|
239
241
|
const queries = stateUnit.search.query$.pipe(skip(1), take(1), toArray()).toPromise();
|
|
240
242
|
stateUnit.search.setQuery('alpha');
|
|
@@ -246,9 +248,9 @@ describe('createDiscoverStateUnit', () => {
|
|
|
246
248
|
|
|
247
249
|
it('omits entityType from the filter when the empty sentinel is selected', async () => {
|
|
248
250
|
const { client, resourceCalls } = mockClient();
|
|
249
|
-
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
251
|
+
const stateUnit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
250
252
|
|
|
251
|
-
const sub = stateUnit.
|
|
253
|
+
const sub = stateUnit.recent.value$.subscribe();
|
|
252
254
|
stateUnit.setSelectedEntityType('Person');
|
|
253
255
|
stateUnit.setSelectedEntityType('');
|
|
254
256
|
|
|
@@ -266,10 +268,73 @@ describe('DiscoverStateUnit — StateUnit axioms', () => {
|
|
|
266
268
|
setup: () => {
|
|
267
269
|
const browse = disposeProbe();
|
|
268
270
|
const { client } = mockClient();
|
|
269
|
-
return { unit: createDiscoverStateUnit(client, browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
271
|
+
return { unit: createDiscoverStateUnit(sessionOf(client), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
270
272
|
},
|
|
271
273
|
surfaces: (u) => [u.selectedEntityType$],
|
|
272
274
|
invocations: (u) => [() => u.setSelectedEntityType('')],
|
|
273
275
|
});
|
|
274
276
|
});
|
|
275
277
|
});
|
|
278
|
+
|
|
279
|
+
describe('createDiscoverStateUnit — terminal load failure', () => {
|
|
280
|
+
// `isLoadingRecent$` was `recent$.pipe(map(r => r === undefined))`, so a
|
|
281
|
+
// terminally failed list (B15) — which has no value either — left
|
|
282
|
+
// /know/discover spinning for ever. Same defect as the References and
|
|
283
|
+
// History panels. See .plans/PANEL-FAILURE-STATES.md
|
|
284
|
+
|
|
285
|
+
it('recent: stops loading and surfaces the reason instead of spinning for ever', async () => {
|
|
286
|
+
const resources$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
287
|
+
const { client } = mockClient({ resources$ });
|
|
288
|
+
const unit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
289
|
+
|
|
290
|
+
expect(await firstValueFrom(unit.recent.loading$)).toBe(true);
|
|
291
|
+
resources$.next({ status: 'failed', error: new Error('Resource not found') } as never);
|
|
292
|
+
|
|
293
|
+
expect(await firstValueFrom(unit.recent.loading$)).toBe(false);
|
|
294
|
+
expect((await firstValueFrom(unit.recent.error$) as Error | null)?.message)
|
|
295
|
+
.toBe('Resource not found');
|
|
296
|
+
|
|
297
|
+
unit.dispose();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('entityTypes: same, and keeps an empty list to render', async () => {
|
|
301
|
+
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
302
|
+
const { client } = mockClient({ entityTypes$ });
|
|
303
|
+
const unit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
304
|
+
|
|
305
|
+
entityTypes$.next({ status: 'failed', error: new Error('boom') } as never);
|
|
306
|
+
|
|
307
|
+
expect(await firstValueFrom(unit.entityTypes.loading$)).toBe(false);
|
|
308
|
+
expect(await firstValueFrom(unit.entityTypes.error$)).not.toBeNull();
|
|
309
|
+
expect(await firstValueFrom(unit.entityTypes.value$)).toEqual([]);
|
|
310
|
+
|
|
311
|
+
unit.dispose();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('recent.retry() re-issues the query and can recover', async () => {
|
|
315
|
+
// A fresh `observe()` is what clears the B15 marker, so retry must reach
|
|
316
|
+
// `browse.resources()` again rather than re-subscribing to a dead stream.
|
|
317
|
+
const attempts: BehaviorSubject<unknown[] | undefined>[] = [];
|
|
318
|
+
const { client } = mockClient({
|
|
319
|
+
resourcesFn: () => {
|
|
320
|
+
const s = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
321
|
+
attempts.push(s);
|
|
322
|
+
return s;
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
const unit = createDiscoverStateUnit(sessionOf(client), mockBrowse());
|
|
326
|
+
|
|
327
|
+
attempts[0]!.next({ status: 'failed', error: new Error('nope') } as never);
|
|
328
|
+
expect(await firstValueFrom(unit.recent.error$)).not.toBeNull();
|
|
329
|
+
|
|
330
|
+
unit.recent.retry();
|
|
331
|
+
expect(attempts.length).toBeGreaterThan(1);
|
|
332
|
+
expect(await firstValueFrom(unit.recent.error$)).toBeNull();
|
|
333
|
+
|
|
334
|
+
attempts[attempts.length - 1]!.next([{ '@id': 'r1' }]);
|
|
335
|
+
expect(await firstValueFrom(unit.recent.value$)).toHaveLength(1);
|
|
336
|
+
expect(await firstValueFrom(unit.recent.loading$)).toBe(false);
|
|
337
|
+
|
|
338
|
+
unit.dispose();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
|
|
2
|
+
import { readyValue } from '@semiont/sdk';
|
|
2
3
|
import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
|
|
3
4
|
import type { ResourceDescriptor } from '@semiont/core';
|
|
4
|
-
import type {
|
|
5
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
5
6
|
import type { StateUnit } from '@semiont/core';
|
|
6
7
|
import { createDisposer } from '@semiont/sdk';
|
|
7
8
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
9
|
+
import { trackList, type ListState } from '../../../state/list-state';
|
|
8
10
|
|
|
9
11
|
const RECENT_LIMIT = 10;
|
|
10
12
|
const SEARCH_LIMIT = 20;
|
|
@@ -19,17 +21,17 @@ export interface DiscoverSearchPipeline {
|
|
|
19
21
|
export interface DiscoverStateUnit extends StateUnit {
|
|
20
22
|
browse: ShellStateUnit;
|
|
21
23
|
search: DiscoverSearchPipeline;
|
|
22
|
-
|
|
23
|
-
entityTypes
|
|
24
|
-
isLoadingRecent$: Observable<boolean>;
|
|
24
|
+
recent: ListState<ResourceDescriptor[]>;
|
|
25
|
+
entityTypes: ListState<string[]>;
|
|
25
26
|
selectedEntityType$: Observable<string>;
|
|
26
27
|
setSelectedEntityType(value: string): void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function createDiscoverStateUnit(
|
|
30
|
-
|
|
31
|
+
session: SemiontSession,
|
|
31
32
|
browse: ShellStateUnit,
|
|
32
33
|
): DiscoverStateUnit {
|
|
34
|
+
const { client } = session;
|
|
33
35
|
const disposer = createDisposer();
|
|
34
36
|
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
35
37
|
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
@@ -44,28 +46,27 @@ export function createDiscoverStateUnit(
|
|
|
44
46
|
const queryInput$ = new Subject<string>();
|
|
45
47
|
disposer.add(() => queryInput$.complete());
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// `trackList` holds the single subscription and multicasts through its own
|
|
50
|
+
// subjects, so the previous `shareReplay` is unnecessary — and it would have
|
|
51
|
+
// been actively wrong here: a replayed ERROR cannot be retried away, and
|
|
52
|
+
// retry must reach `browse.resources()` again for B15 to clear the marker.
|
|
53
|
+
// The thunk therefore rebuilds the whole chain per attempt.
|
|
54
|
+
const recent = trackList<ResourceDescriptor[]>(
|
|
55
|
+
() => selectedEntityType$.pipe(
|
|
56
|
+
switchMap((et) =>
|
|
57
|
+
client.browse.resources({
|
|
58
|
+
limit: RECENT_LIMIT,
|
|
59
|
+
archived: false,
|
|
60
|
+
...(et ? { entityType: et } : {}),
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
54
63
|
),
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const recentResources$: Observable<ResourceDescriptor[]> = recent$.pipe(
|
|
59
|
-
map((r) => r ?? []),
|
|
64
|
+
[],
|
|
60
65
|
);
|
|
66
|
+
disposer.add(recent.dispose);
|
|
61
67
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
const entityTypes$: Observable<string[]> = client.browse.entityTypes().pipe(
|
|
67
|
-
map((e) => e ?? []),
|
|
68
|
-
);
|
|
68
|
+
const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
|
|
69
|
+
disposer.add(entityTypes.dispose);
|
|
69
70
|
|
|
70
71
|
const debouncedQuery$ = queryInput$.pipe(
|
|
71
72
|
startWith(''),
|
|
@@ -87,9 +88,9 @@ export function createDiscoverStateUnit(
|
|
|
87
88
|
...(et ? { entityType: et } : {}),
|
|
88
89
|
})
|
|
89
90
|
.pipe(
|
|
90
|
-
map((
|
|
91
|
-
results:
|
|
92
|
-
isSearching:
|
|
91
|
+
map((st) => ({
|
|
92
|
+
results: readyValue(st) ?? [],
|
|
93
|
+
isSearching: st.status === 'pending',
|
|
93
94
|
})),
|
|
94
95
|
startWith({ results: [] as ResourceDescriptor[], isSearching: true }),
|
|
95
96
|
);
|
|
@@ -106,9 +107,8 @@ export function createDiscoverStateUnit(
|
|
|
106
107
|
return {
|
|
107
108
|
browse,
|
|
108
109
|
search,
|
|
109
|
-
|
|
110
|
-
entityTypes
|
|
111
|
-
isLoadingRecent$,
|
|
110
|
+
recent: recent.state,
|
|
111
|
+
entityTypes: entityTypes.state,
|
|
112
112
|
selectedEntityType$: selectedEntityType$.asObservable(),
|
|
113
113
|
setSelectedEntityType: (value) => selectedEntityType$.next(value),
|
|
114
114
|
dispose: () => disposer.dispose(),
|
|
@@ -12,6 +12,7 @@ import { ResourceViewerPage } from '../components/ResourceViewerPage';
|
|
|
12
12
|
import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
|
|
13
13
|
import { ToastProvider } from '../../../components/Toast';
|
|
14
14
|
import { ThemeProvider } from '../../../contexts/ThemeContext';
|
|
15
|
+
import { LineNumbersProvider } from '../../../contexts/LineNumbersContext';
|
|
15
16
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
16
17
|
import type { ResourceId } from '@semiont/core';
|
|
17
18
|
|
|
@@ -57,6 +58,7 @@ const { stubBrowser } = vi.hoisted(() => {
|
|
|
57
58
|
removeOpenResource: vi.fn(),
|
|
58
59
|
updateOpenResourceName: vi.fn(),
|
|
59
60
|
reorderOpenResources: vi.fn(),
|
|
61
|
+
setLastViewedResource: vi.fn(),
|
|
60
62
|
emit: vi.fn(),
|
|
61
63
|
on: vi.fn(() => () => {}),
|
|
62
64
|
stream: vi.fn(() => ({ subscribe: () => ({ unsubscribe: () => {} }) })),
|
|
@@ -162,11 +164,13 @@ const renderWithProviders = (ui: React.ReactElement) => {
|
|
|
162
164
|
const { SemiontWrapper } = createTestSemiontWrapper();
|
|
163
165
|
return render(
|
|
164
166
|
<ThemeProvider>
|
|
165
|
-
<
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
<LineNumbersProvider>
|
|
168
|
+
<ToastProvider>
|
|
169
|
+
<SemiontWrapper>
|
|
170
|
+
{ui}
|
|
171
|
+
</SemiontWrapper>
|
|
172
|
+
</ToastProvider>
|
|
173
|
+
</LineNumbersProvider>
|
|
170
174
|
</ThemeProvider>
|
|
171
175
|
);
|
|
172
176
|
};
|