@semiont/react-ui 0.5.22 → 0.5.23
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-R52YAGHK.js → chunk-5Y5JDCKC.js} +11 -4
- package/dist/chunk-5Y5JDCKC.js.map +1 -0
- package/dist/{chunk-PFUXQPWJ.js → chunk-RO5CRVIA.js} +31 -31
- 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 +253 -41
- package/dist/index.js +1935 -1678
- 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 +7 -2
- 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/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/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__/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/features/admin-security/state/__tests__/admin-security-state-unit.test.ts +9 -8
- package/src/features/admin-security/state/admin-security-state-unit.ts +3 -2
- package/src/features/admin-users/state/__tests__/admin-users-state-unit.test.ts +6 -5
- 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 +34 -24
- package/src/features/auth-welcome/state/welcome-state-unit.ts +7 -2
- package/src/features/moderate-entity-tags/state/__tests__/entity-tags-state-unit.test.ts +36 -11
- package/src/features/moderate-entity-tags/state/entity-tags-state-unit.ts +9 -10
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +32 -11
- package/src/features/resource-compose/state/compose-page-state-unit.ts +9 -8
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +3 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +25 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +89 -24
- package/src/features/resource-discovery/state/discover-state-unit.ts +30 -30
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +1 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +67 -30
- package/src/features/resource-viewer/state/__tests__/resource-loader-state-unit.test.ts +110 -7
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +166 -26
- 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 +27 -26
- 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-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/{chunk-PFUXQPWJ.js.map → chunk-RO5CRVIA.js.map} +0 -0
- /package/dist/{en-P2L4X27N.js.map → en-QFIEG3CA.js.map} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
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';
|
|
@@ -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(),
|
|
@@ -57,6 +57,7 @@ const { stubBrowser } = vi.hoisted(() => {
|
|
|
57
57
|
removeOpenResource: vi.fn(),
|
|
58
58
|
updateOpenResourceName: vi.fn(),
|
|
59
59
|
reorderOpenResources: vi.fn(),
|
|
60
|
+
setLastViewedResource: vi.fn(),
|
|
60
61
|
emit: vi.fn(),
|
|
61
62
|
on: vi.fn(() => () => {}),
|
|
62
63
|
stream: vi.fn(() => ({ subscribe: () => ({ unsubscribe: () => {} }) })),
|
|
@@ -20,6 +20,7 @@ import { JsonLdPanel } from '@semiont/react-ui';
|
|
|
20
20
|
import { Toolbar } from '@semiont/react-ui';
|
|
21
21
|
import { useResourceLoadingAnnouncements } from '@semiont/react-ui';
|
|
22
22
|
import { ResourceViewer } from '@semiont/react-ui';
|
|
23
|
+
import type { BrowseMediaRenderers, AnnotateMediaRenderers } from '@semiont/react-ui';
|
|
23
24
|
import { useObservable } from '@semiont/react-ui';
|
|
24
25
|
import { useResourceContent } from '../../../hooks/useResourceContent';
|
|
25
26
|
import { useMediaToken } from '../../../hooks/useMediaToken';
|
|
@@ -35,7 +36,7 @@ import { getSelectorType } from '../../../lib/media-shapes';
|
|
|
35
36
|
import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
|
|
36
37
|
import { useSemiont } from '../../../session/SemiontProvider';
|
|
37
38
|
import { createResourceViewerPageStateUnit } from '../state/resource-viewer-page-state-unit';
|
|
38
|
-
import {
|
|
39
|
+
import { useSessionStateUnit } from '../../../hooks/useSessionStateUnit';
|
|
39
40
|
import { useShellStateUnit } from '../../../hooks/useShellStateUnit';
|
|
40
41
|
import { useTranslations } from '../../../contexts/TranslationContext';
|
|
41
42
|
import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
|
|
@@ -92,6 +93,14 @@ export interface ResourceViewerPageProps {
|
|
|
92
93
|
* Name of the active knowledge base (for display in panels)
|
|
93
94
|
*/
|
|
94
95
|
knowledgeBaseName?: string | undefined;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Media-renderer overrides, forwarded to `ResourceViewer`. Present at this
|
|
99
|
+
* tier too so a host embedding the whole page — not just the viewer — can
|
|
100
|
+
* still swap a renderer. See .plans/ANNOTATE-RENDERER-REGISTRY.md (D5)
|
|
101
|
+
*/
|
|
102
|
+
browseRenderers?: BrowseMediaRenderers;
|
|
103
|
+
annotateRenderers?: AnnotateMediaRenderers;
|
|
95
104
|
}
|
|
96
105
|
|
|
97
106
|
/**
|
|
@@ -126,6 +135,8 @@ export function ResourceViewerPage({
|
|
|
126
135
|
refetchDocument,
|
|
127
136
|
streamStatus,
|
|
128
137
|
knowledgeBaseName,
|
|
138
|
+
browseRenderers,
|
|
139
|
+
annotateRenderers,
|
|
129
140
|
}: ResourceViewerPageProps) {
|
|
130
141
|
// Translations
|
|
131
142
|
const tw = useTranslations('ReferenceWizard');
|
|
@@ -185,27 +196,40 @@ export function ResourceViewerPage({
|
|
|
185
196
|
|
|
186
197
|
// Composite state unit — owns all flow VMs, wizard state, annotations, entity types
|
|
187
198
|
const browseStateUnit = useShellStateUnit();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
199
|
+
// Session-typed + session-keyed (SESSION-TYPED-FACTORIES.md): no `!`, no
|
|
200
|
+
// construction without a session, dispose-first rebuild on session swap.
|
|
201
|
+
const stateUnit = useSessionStateUnit(
|
|
202
|
+
session ?? undefined,
|
|
203
|
+
(s) => createResourceViewerPageStateUnit(s, rUri, locale, browseStateUnit),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const annotations = useObservable(stateUnit?.annotations.value$) ?? [];
|
|
207
|
+
const annotationsError = useObservable(stateUnit?.annotations.error$) ?? null;
|
|
208
|
+
const groups = useObservable(stateUnit?.annotationGroups$);
|
|
209
|
+
const allEntityTypes = useObservable(stateUnit?.entityTypes.value$) ?? [];
|
|
210
|
+
const entityTypesError = useObservable(stateUnit?.entityTypes.error$) ?? null;
|
|
211
|
+
// Three states, not two: a terminally failed list has no value EITHER, so
|
|
212
|
+
// deriving "loading" from `undefined` leaves a dead request spinning for
|
|
213
|
+
// ever. See .plans/PANEL-FAILURE-STATES.md
|
|
214
|
+
const referencedBy = useObservable(stateUnit?.referencedBy.value$) ?? [];
|
|
215
|
+
const referencedByLoading = useObservable(stateUnit?.referencedBy.loading$) ?? true;
|
|
216
|
+
const referencedByError = useObservable(stateUnit?.referencedBy.error$) ?? null;
|
|
217
|
+
const events = useObservable(stateUnit?.events.value$) ?? [];
|
|
218
|
+
const eventsLoading = useObservable(stateUnit?.events.loading$) ?? true;
|
|
219
|
+
const eventsError = useObservable(stateUnit?.events.error$) ?? null;
|
|
220
|
+
const hoveredAnnotationId = useObservable(stateUnit?.beckon.hoveredAnnotationId$) ?? null;
|
|
221
|
+
const pendingAnnotation = useObservable(stateUnit?.mark.pendingAnnotation$) ?? null;
|
|
222
|
+
const assistingMotivation = useObservable(stateUnit?.mark.assistingMotivation$) ?? null;
|
|
223
|
+
const progress = useObservable(stateUnit?.mark.progress$) ?? null;
|
|
224
|
+
const activePanel = useObservable(stateUnit?.browse.activePanel$) ?? null;
|
|
225
|
+
const scrollToAnnotationId = useObservable(stateUnit?.browse.scrollToAnnotationId$) ?? null;
|
|
226
|
+
const panelInitialTab = useObservable(stateUnit?.browse.panelInitialTab$) ?? null;
|
|
227
|
+
const onScrollCompleted = stateUnit?.browse.onScrollCompleted;
|
|
228
|
+
const generationProgress = useObservable(stateUnit?.yield.progress$) ?? null;
|
|
229
|
+
const gatherContext = useObservable(stateUnit?.gather.context$) ?? null;
|
|
230
|
+
const gatherLoading = useObservable(stateUnit?.gather.loading$) ?? false;
|
|
231
|
+
const gatherError = useObservable(stateUnit?.gather.error$) ?? null;
|
|
232
|
+
const wizardState = useObservable(stateUnit?.wizard$);
|
|
209
233
|
const wizardOpen = wizardState?.open ?? false;
|
|
210
234
|
const wizardAnnotationId = wizardState?.annotationId ?? null;
|
|
211
235
|
const wizardResourceId = wizardState?.resourceId ?? null;
|
|
@@ -214,12 +238,12 @@ export function ResourceViewerPage({
|
|
|
214
238
|
const [generateOpen, setGenerateOpen] = useState(false);
|
|
215
239
|
|
|
216
240
|
const handleWizardClose = useCallback(() => {
|
|
217
|
-
stateUnit
|
|
241
|
+
stateUnit?.closeWizard();
|
|
218
242
|
}, [stateUnit]);
|
|
219
243
|
|
|
220
244
|
const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
|
|
221
245
|
clearNewAnnotationId(annotationId(referenceId));
|
|
222
|
-
stateUnit
|
|
246
|
+
stateUnit?.yield.generate(referenceId, {
|
|
223
247
|
title: config.title,
|
|
224
248
|
storageUri: config.storagePath,
|
|
225
249
|
prompt: config.prompt,
|
|
@@ -239,7 +263,7 @@ export function ResourceViewerPage({
|
|
|
239
263
|
// NOT a toast. `generateFromResource` is Phase 6 (the @semiont/sdk session);
|
|
240
264
|
// this is declared RED until that method lands. Do not re-impl it here.
|
|
241
265
|
const handleResourceGenerateSubmit = useCallback((_resourceId: string, config: GenerationConfig) => {
|
|
242
|
-
stateUnit
|
|
266
|
+
stateUnit?.yield.generateFromResource({
|
|
243
267
|
title: config.title,
|
|
244
268
|
storageUri: config.storagePath,
|
|
245
269
|
...(config.prompt ? { prompt: config.prompt } : {}),
|
|
@@ -286,14 +310,15 @@ export function ResourceViewerPage({
|
|
|
286
310
|
});
|
|
287
311
|
}, [browser]);
|
|
288
312
|
|
|
289
|
-
// Add resource to open tabs when it loads
|
|
313
|
+
// Add resource to open tabs when it loads, and record it as this KB's
|
|
314
|
+
// last-viewed for the /know landing route to resume from. Both are per-KB
|
|
315
|
+
// state owned by the browser — a global last-viewed id sends that redirect
|
|
316
|
+
// into the previously active KB's resource after a switch.
|
|
290
317
|
useEffect(() => {
|
|
291
318
|
if (resource && rUri) {
|
|
292
319
|
const mediaType = getPrimaryMediaType(resource);
|
|
293
320
|
browser.addOpenResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
|
|
294
|
-
|
|
295
|
-
localStorage.setItem('lastViewedDocumentId', rUri);
|
|
296
|
-
}
|
|
321
|
+
browser.setLastViewedResource(rUri);
|
|
297
322
|
}
|
|
298
323
|
}, [resource, rUri, browser]);
|
|
299
324
|
|
|
@@ -503,6 +528,8 @@ export function ResourceViewerPage({
|
|
|
503
528
|
showLineNumbers={showLineNumbers}
|
|
504
529
|
hoverDelayMs={hoverDelayMs}
|
|
505
530
|
hoveredAnnotationId={hoveredAnnotationId}
|
|
531
|
+
{...(browseRenderers && { browseRenderers })}
|
|
532
|
+
{...(annotateRenderers && { annotateRenderers })}
|
|
506
533
|
/>
|
|
507
534
|
)}
|
|
508
535
|
</ErrorBoundary>
|
|
@@ -543,9 +570,14 @@ export function ResourceViewerPage({
|
|
|
543
570
|
progress={progress}
|
|
544
571
|
pendingAnnotation={pendingAnnotation}
|
|
545
572
|
allEntityTypes={allEntityTypes}
|
|
573
|
+
annotationsError={annotationsError}
|
|
574
|
+
onRetryAnnotations={stateUnit?.annotations.retry}
|
|
575
|
+
entityTypesError={entityTypesError}
|
|
546
576
|
generatingReferenceId={generationProgress?.annotationId ?? null}
|
|
547
577
|
referencedBy={referencedBy}
|
|
548
578
|
referencedByLoading={referencedByLoading}
|
|
579
|
+
referencedByError={referencedByError}
|
|
580
|
+
onRetryReferencedBy={stateUnit?.referencedBy.retry}
|
|
549
581
|
resourceId={rUri}
|
|
550
582
|
locale={locale}
|
|
551
583
|
sourceLanguage={getLanguage(resource)}
|
|
@@ -562,7 +594,11 @@ export function ResourceViewerPage({
|
|
|
562
594
|
{/* History Panel */}
|
|
563
595
|
{activePanel === 'history' && (
|
|
564
596
|
<AnnotationHistory
|
|
565
|
-
|
|
597
|
+
events={events}
|
|
598
|
+
eventsLoading={eventsLoading}
|
|
599
|
+
eventsError={eventsError}
|
|
600
|
+
onRetryEvents={stateUnit?.events.retry}
|
|
601
|
+
annotations={annotations}
|
|
566
602
|
hoveredAnnotationId={hoveredAnnotationId}
|
|
567
603
|
onEventHover={handleEventHover}
|
|
568
604
|
onEventClick={handleEventClick}
|
|
@@ -676,6 +712,7 @@ export function ResourceViewerPage({
|
|
|
676
712
|
resourceId={rUri}
|
|
677
713
|
defaultTitle=""
|
|
678
714
|
locale={locale}
|
|
715
|
+
entityTypeOptions={allEntityTypes}
|
|
679
716
|
onGenerateSubmit={handleResourceGenerateSubmit}
|
|
680
717
|
translations={{
|
|
681
718
|
gatherTitle: tg('gatherTitle'),
|