@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,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { sessionOf } from '../../../../__tests__/test-client';
|
|
2
3
|
import { firstValueFrom } from 'rxjs';
|
|
3
4
|
import { filter } from 'rxjs/operators';
|
|
4
5
|
import type { SemiontClient } from '@semiont/sdk';
|
|
@@ -28,7 +29,7 @@ describe('createAdminUsersStateUnit', () => {
|
|
|
28
29
|
it('fetches users and stats on creation', async () => {
|
|
29
30
|
const listUsers = vi.fn().mockResolvedValue({ users: [{ id: 'u1' }] });
|
|
30
31
|
const getUserStats = vi.fn().mockResolvedValue({ stats: { total: 1 } });
|
|
31
|
-
const stateUnit = createAdminUsersStateUnit(mockClient({ listUsers, getUserStats }), mockBrowse());
|
|
32
|
+
const stateUnit = createAdminUsersStateUnit(sessionOf(mockClient({ listUsers, getUserStats })), mockBrowse());
|
|
32
33
|
|
|
33
34
|
const users = await firstValueFrom(stateUnit.users$.pipe(filter((u) => u.length > 0)));
|
|
34
35
|
expect(users).toEqual([{ id: 'u1' }]);
|
|
@@ -40,7 +41,7 @@ describe('createAdminUsersStateUnit', () => {
|
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
it('starts with loading true, resolves to false', async () => {
|
|
43
|
-
const stateUnit = createAdminUsersStateUnit(mockClient(), mockBrowse());
|
|
44
|
+
const stateUnit = createAdminUsersStateUnit(sessionOf(mockClient()), mockBrowse());
|
|
44
45
|
|
|
45
46
|
await firstValueFrom(stateUnit.usersLoading$.pipe(filter((l) => !l)));
|
|
46
47
|
await firstValueFrom(stateUnit.statsLoading$.pipe(filter((l) => !l)));
|
|
@@ -51,7 +52,7 @@ describe('createAdminUsersStateUnit', () => {
|
|
|
51
52
|
it('sets loading false on fetch error', async () => {
|
|
52
53
|
const listUsers = vi.fn().mockRejectedValue(new Error('fail'));
|
|
53
54
|
const getUserStats = vi.fn().mockRejectedValue(new Error('fail'));
|
|
54
|
-
const stateUnit = createAdminUsersStateUnit(mockClient({ listUsers, getUserStats }), mockBrowse());
|
|
55
|
+
const stateUnit = createAdminUsersStateUnit(sessionOf(mockClient({ listUsers, getUserStats })), mockBrowse());
|
|
55
56
|
|
|
56
57
|
await firstValueFrom(stateUnit.usersLoading$.pipe(filter((l) => !l)));
|
|
57
58
|
await firstValueFrom(stateUnit.statsLoading$.pipe(filter((l) => !l)));
|
|
@@ -63,7 +64,7 @@ describe('createAdminUsersStateUnit', () => {
|
|
|
63
64
|
const listUsers = vi.fn().mockResolvedValue({ users: [] });
|
|
64
65
|
const getUserStats = vi.fn().mockResolvedValue({ stats: null });
|
|
65
66
|
const updateUser = vi.fn().mockResolvedValue(undefined);
|
|
66
|
-
const stateUnit = createAdminUsersStateUnit(mockClient({ listUsers, getUserStats, updateUser }), mockBrowse());
|
|
67
|
+
const stateUnit = createAdminUsersStateUnit(sessionOf(mockClient({ listUsers, getUserStats, updateUser })), mockBrowse());
|
|
67
68
|
|
|
68
69
|
await firstValueFrom(stateUnit.usersLoading$.pipe(filter((l) => !l)));
|
|
69
70
|
listUsers.mockClear();
|
|
@@ -85,7 +86,7 @@ describe('AdminUsersStateUnit — StateUnit axioms', () => {
|
|
|
85
86
|
assertStateUnitAxioms({
|
|
86
87
|
setup: () => {
|
|
87
88
|
const browse = disposeProbe();
|
|
88
|
-
return { unit: createAdminUsersStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
89
|
+
return { unit: createAdminUsersStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
89
90
|
},
|
|
90
91
|
surfaces: (u) => [u.users$, u.stats$, u.usersLoading$, u.statsLoading$],
|
|
91
92
|
});
|
|
@@ -3,7 +3,7 @@ import { userDID } from '@semiont/core';
|
|
|
3
3
|
import { createDisposer } from '@semiont/sdk';
|
|
4
4
|
import type { StateUnit } from '@semiont/core';
|
|
5
5
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
6
|
-
import type {
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
7
7
|
|
|
8
8
|
export interface AdminUsersStateUnit extends StateUnit {
|
|
9
9
|
browse: ShellStateUnit;
|
|
@@ -15,9 +15,10 @@ export interface AdminUsersStateUnit extends StateUnit {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function createAdminUsersStateUnit(
|
|
18
|
-
|
|
18
|
+
session: SemiontSession,
|
|
19
19
|
browse: ShellStateUnit,
|
|
20
20
|
): AdminUsersStateUnit {
|
|
21
|
+
const { client } = session;
|
|
21
22
|
const disposer = createDisposer();
|
|
22
23
|
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
23
24
|
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
@@ -1,26 +1,37 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { firstValueFrom } from 'rxjs';
|
|
3
3
|
import { filter } from 'rxjs/operators';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
5
|
+
import { AuthNamespace } from '@semiont/sdk';
|
|
6
|
+
import { createTestSession, stubBackend } from '@semiont/sdk/testing';
|
|
5
7
|
import { createWelcomeStateUnit } from '../welcome-state-unit';
|
|
6
8
|
import { assertStateUnitAxioms } from '@semiont/core/testing';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Real session over the scriptable transport (SESSION-TYPED-FACTORIES pilot);
|
|
11
|
+
// auth behavior scripted by prototype spies, the AuthShell precedent — the
|
|
12
|
+
// previous `as unknown as SemiontClient` hand-mock was the M1 disease.
|
|
13
|
+
let getMe: ReturnType<typeof vi.spyOn>;
|
|
14
|
+
let acceptTermsSpy: ReturnType<typeof vi.spyOn>;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
getMe = vi.spyOn(AuthNamespace.prototype, 'me');
|
|
18
|
+
getMe.mockResolvedValue({ termsAcceptedAt: undefined } as never);
|
|
19
|
+
acceptTermsSpy = vi.spyOn(AuthNamespace.prototype, 'acceptTerms');
|
|
20
|
+
acceptTermsSpy.mockResolvedValue(undefined as never);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function makeSession(): SemiontSession {
|
|
28
|
+
return createTestSession({ backend: stubBackend() }).session;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
describe('createWelcomeStateUnit', () => {
|
|
21
32
|
it('fetches user data on creation', async () => {
|
|
22
|
-
|
|
23
|
-
const stateUnit = createWelcomeStateUnit(
|
|
33
|
+
getMe.mockResolvedValue({ termsAcceptedAt: '2026-01-01' } as never);
|
|
34
|
+
const stateUnit = createWelcomeStateUnit(makeSession());
|
|
24
35
|
|
|
25
36
|
const data = await firstValueFrom(stateUnit.userData$.pipe(filter((d) => d !== null)));
|
|
26
37
|
expect(data).toEqual({ termsAcceptedAt: '2026-01-01' });
|
|
@@ -29,8 +40,8 @@ describe('createWelcomeStateUnit', () => {
|
|
|
29
40
|
});
|
|
30
41
|
|
|
31
42
|
it('initializes with null userData and not processing', async () => {
|
|
32
|
-
|
|
33
|
-
const stateUnit = createWelcomeStateUnit(
|
|
43
|
+
getMe.mockReturnValue(new Promise(() => {}) as never);
|
|
44
|
+
const stateUnit = createWelcomeStateUnit(makeSession());
|
|
34
45
|
|
|
35
46
|
const data = await firstValueFrom(stateUnit.userData$);
|
|
36
47
|
const processing = await firstValueFrom(stateUnit.isProcessing$);
|
|
@@ -41,14 +52,13 @@ describe('createWelcomeStateUnit', () => {
|
|
|
41
52
|
});
|
|
42
53
|
|
|
43
54
|
it('acceptTerms sets isProcessing and updates userData', async () => {
|
|
44
|
-
const
|
|
45
|
-
const stateUnit = createWelcomeStateUnit(mockClient({ acceptTerms }));
|
|
55
|
+
const stateUnit = createWelcomeStateUnit(makeSession());
|
|
46
56
|
|
|
47
57
|
await firstValueFrom(stateUnit.userData$.pipe(filter((d) => d !== null)));
|
|
48
58
|
|
|
49
59
|
await stateUnit.acceptTerms();
|
|
50
60
|
|
|
51
|
-
expect(
|
|
61
|
+
expect(acceptTermsSpy).toHaveBeenCalledOnce();
|
|
52
62
|
|
|
53
63
|
const data = await firstValueFrom(stateUnit.userData$);
|
|
54
64
|
expect(data?.termsAcceptedAt).toBeDefined();
|
|
@@ -60,8 +70,8 @@ describe('createWelcomeStateUnit', () => {
|
|
|
60
70
|
});
|
|
61
71
|
|
|
62
72
|
it('acceptTerms resets isProcessing on error', async () => {
|
|
63
|
-
|
|
64
|
-
const stateUnit = createWelcomeStateUnit(
|
|
73
|
+
acceptTermsSpy.mockRejectedValue(new Error('fail') as never);
|
|
74
|
+
const stateUnit = createWelcomeStateUnit(makeSession());
|
|
65
75
|
|
|
66
76
|
await firstValueFrom(stateUnit.userData$.pipe(filter((d) => d !== null)));
|
|
67
77
|
|
|
@@ -74,8 +84,8 @@ describe('createWelcomeStateUnit', () => {
|
|
|
74
84
|
});
|
|
75
85
|
|
|
76
86
|
it('handles getMe failure gracefully', async () => {
|
|
77
|
-
|
|
78
|
-
const stateUnit = createWelcomeStateUnit(
|
|
87
|
+
getMe.mockRejectedValue(new Error('unauthorized') as never);
|
|
88
|
+
const stateUnit = createWelcomeStateUnit(makeSession());
|
|
79
89
|
|
|
80
90
|
await vi.waitFor(() => expect(getMe).toHaveBeenCalled());
|
|
81
91
|
|
|
@@ -89,7 +99,7 @@ describe('createWelcomeStateUnit', () => {
|
|
|
89
99
|
describe('WelcomeStateUnit — StateUnit axioms', () => {
|
|
90
100
|
it('satisfies the StateUnit axioms', () => {
|
|
91
101
|
assertStateUnitAxioms({
|
|
92
|
-
setup: () => createWelcomeStateUnit(
|
|
102
|
+
setup: () => createWelcomeStateUnit(makeSession()),
|
|
93
103
|
surfaces: (u) => [u.userData$, u.isProcessing$],
|
|
94
104
|
invocations: (u) => [() => u.acceptTerms()],
|
|
95
105
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import { createDisposer } from '@semiont/sdk';
|
|
3
3
|
import type { StateUnit } from '@semiont/core';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
5
5
|
|
|
6
6
|
export interface WelcomeStateUnit extends StateUnit {
|
|
7
7
|
userData$: Observable<{ termsAcceptedAt?: string } | null>;
|
|
@@ -9,9 +9,14 @@ export interface WelcomeStateUnit extends StateUnit {
|
|
|
9
9
|
acceptTerms(): Promise<void>;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
// Session-typed (SESSION-TYPED-FACTORIES.md D1): the parameter is the
|
|
13
|
+
// lifetime this unit must not outlive. This page crashed in production when
|
|
14
|
+
// the unit was constructed with `undefined` during session activation —
|
|
15
|
+
// `useSessionStateUnit` + this signature make that unrepresentable.
|
|
12
16
|
export function createWelcomeStateUnit(
|
|
13
|
-
|
|
17
|
+
session: SemiontSession,
|
|
14
18
|
): WelcomeStateUnit {
|
|
19
|
+
const { client } = session;
|
|
15
20
|
const disposer = createDisposer();
|
|
16
21
|
|
|
17
22
|
const userData$ = new BehaviorSubject<{ termsAcceptedAt?: string } | null>(null);
|
|
@@ -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 } from 'rxjs/operators';
|
|
4
6
|
import type { SemiontClient } from '@semiont/sdk';
|
|
@@ -17,7 +19,7 @@ function mockClient(overrides: {
|
|
|
17
19
|
const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person', 'Place']);
|
|
18
20
|
return {
|
|
19
21
|
browse: {
|
|
20
|
-
entityTypes: () => entityTypes$.asObservable(),
|
|
22
|
+
entityTypes: () => asStates(entityTypes$.asObservable()),
|
|
21
23
|
},
|
|
22
24
|
frame: {
|
|
23
25
|
addEntityType: overrides.addEntityType ?? vi.fn().mockResolvedValue(undefined),
|
|
@@ -27,9 +29,9 @@ function mockClient(overrides: {
|
|
|
27
29
|
|
|
28
30
|
describe('createEntityTagsStateUnit', () => {
|
|
29
31
|
it('exposes entity types from browse namespace', async () => {
|
|
30
|
-
const stateUnit = createEntityTagsStateUnit(mockClient(), mockBrowse());
|
|
32
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient()), mockBrowse());
|
|
31
33
|
|
|
32
|
-
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
34
|
+
const types = await firstValueFrom(stateUnit.entityTypes.value$);
|
|
33
35
|
expect(types).toEqual(['Person', 'Place']);
|
|
34
36
|
|
|
35
37
|
stateUnit.dispose();
|
|
@@ -37,20 +39,20 @@ describe('createEntityTagsStateUnit', () => {
|
|
|
37
39
|
|
|
38
40
|
it('reports loading when entity types are undefined', async () => {
|
|
39
41
|
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
40
|
-
const stateUnit = createEntityTagsStateUnit(mockClient({ entityTypes$ }), mockBrowse());
|
|
42
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse());
|
|
41
43
|
|
|
42
|
-
const loading = await firstValueFrom(stateUnit.
|
|
44
|
+
const loading = await firstValueFrom(stateUnit.entityTypes.loading$);
|
|
43
45
|
expect(loading).toBe(true);
|
|
44
46
|
|
|
45
47
|
entityTypes$.next(['Tag']);
|
|
46
|
-
const loaded = await firstValueFrom(stateUnit.
|
|
48
|
+
const loaded = await firstValueFrom(stateUnit.entityTypes.loading$.pipe(filter((l) => !l)));
|
|
47
49
|
expect(loaded).toBe(false);
|
|
48
50
|
|
|
49
51
|
stateUnit.dispose();
|
|
50
52
|
});
|
|
51
53
|
|
|
52
54
|
it('setNewTag updates newTag$', async () => {
|
|
53
|
-
const stateUnit = createEntityTagsStateUnit(mockClient(), mockBrowse());
|
|
55
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient()), mockBrowse());
|
|
54
56
|
|
|
55
57
|
stateUnit.setNewTag('Organization');
|
|
56
58
|
const tag = await firstValueFrom(stateUnit.newTag$);
|
|
@@ -61,7 +63,7 @@ describe('createEntityTagsStateUnit', () => {
|
|
|
61
63
|
|
|
62
64
|
it('addTag calls client and clears newTag$', async () => {
|
|
63
65
|
const addEntityType = vi.fn().mockResolvedValue(undefined);
|
|
64
|
-
const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
|
|
66
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
|
|
65
67
|
|
|
66
68
|
stateUnit.setNewTag('Event');
|
|
67
69
|
await stateUnit.addTag();
|
|
@@ -75,7 +77,7 @@ describe('createEntityTagsStateUnit', () => {
|
|
|
75
77
|
|
|
76
78
|
it('addTag ignores empty/whitespace input', async () => {
|
|
77
79
|
const addEntityType = vi.fn();
|
|
78
|
-
const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
|
|
80
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
|
|
79
81
|
|
|
80
82
|
stateUnit.setNewTag(' ');
|
|
81
83
|
await stateUnit.addTag();
|
|
@@ -87,7 +89,7 @@ describe('createEntityTagsStateUnit', () => {
|
|
|
87
89
|
|
|
88
90
|
it('addTag sets error on failure', async () => {
|
|
89
91
|
const addEntityType = vi.fn().mockRejectedValue(new Error('duplicate'));
|
|
90
|
-
const stateUnit = createEntityTagsStateUnit(mockClient({ addEntityType }), mockBrowse());
|
|
92
|
+
const stateUnit = createEntityTagsStateUnit(sessionOf(mockClient({ addEntityType })), mockBrowse());
|
|
91
93
|
|
|
92
94
|
stateUnit.setNewTag('Person');
|
|
93
95
|
await stateUnit.addTag();
|
|
@@ -107,10 +109,33 @@ describe('EntityTagsStateUnit — StateUnit axioms', () => {
|
|
|
107
109
|
assertStateUnitAxioms({
|
|
108
110
|
setup: () => {
|
|
109
111
|
const browse = disposeProbe();
|
|
110
|
-
return { unit: createEntityTagsStateUnit(mockClient(), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
112
|
+
return { unit: createEntityTagsStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit), passedIn: [browse] };
|
|
111
113
|
},
|
|
112
114
|
surfaces: (u) => [u.newTag$, u.error$, u.isAdding$],
|
|
113
115
|
invocations: (u) => [() => u.setNewTag(''), () => u.addTag()],
|
|
114
116
|
});
|
|
115
117
|
});
|
|
116
118
|
});
|
|
119
|
+
|
|
120
|
+
describe('createEntityTagsStateUnit — terminal load failure', () => {
|
|
121
|
+
// `isLoading$` was `raw$.pipe(map(e => e === undefined))` and
|
|
122
|
+
// moderate/entity-tags/page.tsx returns a full-page loading screen off it,
|
|
123
|
+
// so a failed load froze the route. The existing `error$` is the ADD-tag
|
|
124
|
+
// error and is deliberately left alone.
|
|
125
|
+
// See .plans/PANEL-FAILURE-STATES.md
|
|
126
|
+
|
|
127
|
+
it('stops loading and surfaces the load failure, distinct from the add-tag error', async () => {
|
|
128
|
+
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
129
|
+
const unit = createEntityTagsStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse());
|
|
130
|
+
|
|
131
|
+
expect(await firstValueFrom(unit.entityTypes.loading$)).toBe(true);
|
|
132
|
+
entityTypes$.next({ status: 'failed', error: new Error('Resource not found') } as never);
|
|
133
|
+
|
|
134
|
+
expect(await firstValueFrom(unit.entityTypes.loading$)).toBe(false);
|
|
135
|
+
expect(await firstValueFrom(unit.entityTypes.error$)).not.toBeNull();
|
|
136
|
+
// The add-tag error channel is untouched by a load failure.
|
|
137
|
+
expect(await firstValueFrom(unit.error$)).toBe('');
|
|
138
|
+
|
|
139
|
+
unit.dispose();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { BehaviorSubject, type Observable
|
|
1
|
+
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import { createDisposer } from '@semiont/sdk';
|
|
3
3
|
import type { StateUnit } from '@semiont/core';
|
|
4
4
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
5
|
-
import type
|
|
5
|
+
import { trackList, type ListState } from '../../../state/list-state';
|
|
6
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
6
7
|
|
|
7
8
|
export interface EntityTagsStateUnit extends StateUnit {
|
|
8
9
|
browse: ShellStateUnit;
|
|
9
|
-
entityTypes
|
|
10
|
-
isLoading$: Observable<boolean>;
|
|
10
|
+
entityTypes: ListState<string[]>;
|
|
11
11
|
newTag$: Observable<string>;
|
|
12
12
|
error$: Observable<string>;
|
|
13
13
|
isAdding$: Observable<boolean>;
|
|
@@ -16,9 +16,10 @@ export interface EntityTagsStateUnit extends StateUnit {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function createEntityTagsStateUnit(
|
|
19
|
-
|
|
19
|
+
session: SemiontSession,
|
|
20
20
|
browse: ShellStateUnit,
|
|
21
21
|
): EntityTagsStateUnit {
|
|
22
|
+
const { client } = session;
|
|
22
23
|
const disposer = createDisposer();
|
|
23
24
|
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
24
25
|
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
@@ -28,9 +29,8 @@ export function createEntityTagsStateUnit(
|
|
|
28
29
|
const error$ = new BehaviorSubject<string>('');
|
|
29
30
|
const isAdding$ = new BehaviorSubject<boolean>(false);
|
|
30
31
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const isLoading$: Observable<boolean> = raw$.pipe(map((e) => e === undefined));
|
|
32
|
+
const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
|
|
33
|
+
disposer.add(entityTypes.dispose);
|
|
34
34
|
|
|
35
35
|
const addTag = async (): Promise<void> => {
|
|
36
36
|
const tag = newTag$.getValue().trim();
|
|
@@ -49,8 +49,7 @@ export function createEntityTagsStateUnit(
|
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
51
|
browse,
|
|
52
|
-
entityTypes
|
|
53
|
-
isLoading$,
|
|
52
|
+
entityTypes: entityTypes.state,
|
|
54
53
|
newTag$: newTag$.asObservable(),
|
|
55
54
|
error$: error$.asObservable(),
|
|
56
55
|
isAdding$: isAdding$.asObservable(),
|
|
@@ -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, Observable, firstValueFrom } from 'rxjs';
|
|
3
5
|
import { filter } from 'rxjs/operators';
|
|
4
6
|
import type { SemiontClient } from '@semiont/sdk';
|
|
@@ -32,7 +34,7 @@ function mockClient(overrides: {
|
|
|
32
34
|
const entityTypes$ = overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
|
|
33
35
|
return {
|
|
34
36
|
browse: {
|
|
35
|
-
entityTypes: () => entityTypes$.asObservable(),
|
|
37
|
+
entityTypes: () => asStates(entityTypes$.asObservable()),
|
|
36
38
|
},
|
|
37
39
|
yield: {
|
|
38
40
|
fromToken: overrides.fromToken ?? vi.fn().mockResolvedValue({ '@id': 'src-1', representations: [{ mediaType: 'text/plain' }] }),
|
|
@@ -51,7 +53,7 @@ function mockClient(overrides: {
|
|
|
51
53
|
|
|
52
54
|
describe('createComposePageStateUnit', () => {
|
|
53
55
|
it('defaults to "new" mode', async () => {
|
|
54
|
-
const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {});
|
|
56
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
|
|
55
57
|
|
|
56
58
|
const mode = await firstValueFrom(stateUnit.mode$);
|
|
57
59
|
expect(mode).toBe('new');
|
|
@@ -63,7 +65,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
it('detects reference mode from params', async () => {
|
|
66
|
-
const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
|
|
68
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
67
69
|
annotationUri: 'ann-1',
|
|
68
70
|
sourceDocumentId: 'doc-1',
|
|
69
71
|
name: 'Reference Doc',
|
|
@@ -82,7 +84,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
82
84
|
|
|
83
85
|
it('parses storedContext in reference mode', async () => {
|
|
84
86
|
const context = { annotation: { id: 'ann-1' }, sourceContext: 'text' };
|
|
85
|
-
const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
|
|
87
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
86
88
|
annotationUri: 'ann-1',
|
|
87
89
|
sourceDocumentId: 'doc-1',
|
|
88
90
|
name: 'Ref',
|
|
@@ -96,7 +98,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
it('ignores malformed storedContext', async () => {
|
|
99
|
-
const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {
|
|
101
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {
|
|
100
102
|
annotationUri: 'ann-1',
|
|
101
103
|
sourceDocumentId: 'doc-1',
|
|
102
104
|
name: 'Ref',
|
|
@@ -113,9 +115,9 @@ describe('createComposePageStateUnit', () => {
|
|
|
113
115
|
});
|
|
114
116
|
|
|
115
117
|
it('exposes entity types', async () => {
|
|
116
|
-
const stateUnit = createComposePageStateUnit(mockClient(), mockBrowse(), {});
|
|
118
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient()), mockBrowse(), {});
|
|
117
119
|
|
|
118
|
-
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
120
|
+
const types = await firstValueFrom(stateUnit.entityTypes.value$);
|
|
119
121
|
expect(types).toEqual(['Person']);
|
|
120
122
|
|
|
121
123
|
stateUnit.dispose();
|
|
@@ -123,7 +125,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
123
125
|
|
|
124
126
|
it('save in new mode calls yield.resource', async () => {
|
|
125
127
|
const resource = mockUpload('new-3');
|
|
126
|
-
const stateUnit = createComposePageStateUnit(mockClient({ resource }), mockBrowse(), {});
|
|
128
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource })), mockBrowse(), {});
|
|
127
129
|
|
|
128
130
|
const id = await stateUnit.save({
|
|
129
131
|
mode: 'new',
|
|
@@ -143,7 +145,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
143
145
|
it('save in reference mode calls yield.resource then bind.body', async () => {
|
|
144
146
|
const resource = mockUpload('new-4');
|
|
145
147
|
const body = vi.fn().mockResolvedValue(undefined);
|
|
146
|
-
const stateUnit = createComposePageStateUnit(mockClient({ resource, body }), mockBrowse(), {
|
|
148
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ resource, body })), mockBrowse(), {
|
|
147
149
|
annotationUri: 'ann-1',
|
|
148
150
|
sourceDocumentId: 'doc-1',
|
|
149
151
|
name: 'Ref',
|
|
@@ -167,7 +169,7 @@ describe('createComposePageStateUnit', () => {
|
|
|
167
169
|
|
|
168
170
|
it('save in clone mode calls yield.createFromToken', async () => {
|
|
169
171
|
const createFromToken = vi.fn().mockResolvedValue({ resourceId: 'cloned-1' });
|
|
170
|
-
const stateUnit = createComposePageStateUnit(mockClient({ createFromToken }), mockBrowse(), {
|
|
172
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ createFromToken })), mockBrowse(), {
|
|
171
173
|
mode: 'clone',
|
|
172
174
|
token: 'tok-abc',
|
|
173
175
|
});
|
|
@@ -192,9 +194,28 @@ describe('ComposePageStateUnit — StateUnit axioms', () => {
|
|
|
192
194
|
assertStateUnitAxioms({
|
|
193
195
|
setup: () => {
|
|
194
196
|
const browse = disposeProbe();
|
|
195
|
-
return { unit: createComposePageStateUnit(mockClient(), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
|
|
197
|
+
return { unit: createComposePageStateUnit(sessionOf(mockClient()), browse as unknown as ShellStateUnit, {}), passedIn: [browse] };
|
|
196
198
|
},
|
|
197
199
|
surfaces: (u) => [u.mode$, u.loading$, u.cloneData$, u.referenceData$, u.gatheredContext$, u.uploadProgress$],
|
|
198
200
|
});
|
|
199
201
|
});
|
|
200
202
|
});
|
|
203
|
+
|
|
204
|
+
describe('createComposePageStateUnit — entity types load failure', () => {
|
|
205
|
+
// Lower severity than the two hangs: compose defaults to an empty picker,
|
|
206
|
+
// which misrepresents a failed load as "this KB has no entity types".
|
|
207
|
+
// See .plans/PANEL-FAILURE-STATES.md
|
|
208
|
+
|
|
209
|
+
it('surfaces the failure rather than presenting an empty picker as fact', async () => {
|
|
210
|
+
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
211
|
+
const stateUnit = createComposePageStateUnit(sessionOf(mockClient({ entityTypes$ })), mockBrowse(), {});
|
|
212
|
+
|
|
213
|
+
entityTypes$.next({ status: 'failed', error: new Error('boom') } as never);
|
|
214
|
+
|
|
215
|
+
expect(await firstValueFrom(stateUnit.entityTypes.loading$)).toBe(false);
|
|
216
|
+
expect(await firstValueFrom(stateUnit.entityTypes.error$)).not.toBeNull();
|
|
217
|
+
expect(await firstValueFrom(stateUnit.entityTypes.value$)).toEqual([]);
|
|
218
|
+
|
|
219
|
+
stateUnit.dispose();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { BehaviorSubject, type Observable
|
|
1
|
+
import { BehaviorSubject, type Observable } from 'rxjs';
|
|
2
2
|
import type { GatheredContext, AnnotationId, AccessToken, ResourceDescriptor, ResourceId } from '@semiont/core';
|
|
3
3
|
import { 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';
|
|
7
|
-
import type
|
|
7
|
+
import { trackList, type ListState } from '../../../state/list-state';
|
|
8
|
+
import type { SemiontSession } from '@semiont/sdk';
|
|
8
9
|
import { decodeWithCharset, extensionForMediaType } from '@semiont/core';
|
|
9
10
|
import type { UploadProgress } from '@semiont/sdk';
|
|
10
11
|
|
|
@@ -54,7 +55,7 @@ export interface ComposePageStateUnit extends StateUnit {
|
|
|
54
55
|
cloneData$: Observable<CloneData | null>;
|
|
55
56
|
referenceData$: Observable<ReferenceData | null>;
|
|
56
57
|
gatheredContext$: Observable<GatheredContext | null>;
|
|
57
|
-
entityTypes
|
|
58
|
+
entityTypes: ListState<string[]>;
|
|
58
59
|
/**
|
|
59
60
|
* Live upload-progress for the in-flight `save(...)` call. Emits the
|
|
60
61
|
* full `UploadProgress` lifecycle (started → finished) while a save is
|
|
@@ -66,11 +67,12 @@ export interface ComposePageStateUnit extends StateUnit {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export function createComposePageStateUnit(
|
|
69
|
-
|
|
70
|
+
session: SemiontSession,
|
|
70
71
|
browse: ShellStateUnit,
|
|
71
72
|
params: ComposeParams,
|
|
72
73
|
auth?: AccessToken,
|
|
73
74
|
): ComposePageStateUnit {
|
|
75
|
+
const { client } = session;
|
|
74
76
|
const disposer = createDisposer();
|
|
75
77
|
// `browse` (ShellStateUnit) is a *passed-in* dependency owned by the caller
|
|
76
78
|
// (`useShellStateUnit`), not this unit — do NOT add it to the disposer (it's the
|
|
@@ -87,9 +89,8 @@ export function createComposePageStateUnit(
|
|
|
87
89
|
const gatheredContext$ = new BehaviorSubject<GatheredContext | null>(null);
|
|
88
90
|
const uploadProgress$ = new BehaviorSubject<UploadProgress | null>(null);
|
|
89
91
|
|
|
90
|
-
const entityTypes
|
|
91
|
-
|
|
92
|
-
);
|
|
92
|
+
const entityTypes = trackList<string[]>(() => client.browse.entityTypes(), []);
|
|
93
|
+
disposer.add(entityTypes.dispose);
|
|
93
94
|
|
|
94
95
|
// Initialize based on mode
|
|
95
96
|
if (isReferenceMode) {
|
|
@@ -192,7 +193,7 @@ export function createComposePageStateUnit(
|
|
|
192
193
|
cloneData$: cloneData$.asObservable(),
|
|
193
194
|
referenceData$: referenceData$.asObservable(),
|
|
194
195
|
gatheredContext$: gatheredContext$.asObservable(),
|
|
195
|
-
entityTypes
|
|
196
|
+
entityTypes: entityTypes.state,
|
|
196
197
|
uploadProgress$: uploadProgress$.asObservable(),
|
|
197
198
|
save,
|
|
198
199
|
dispose: () => {
|
|
@@ -56,7 +56,9 @@ const createMockProps = (overrides?: Partial<ResourceDiscoveryPageProps>): Resou
|
|
|
56
56
|
composeFirstResource: 'Compose First Resource',
|
|
57
57
|
archived: 'Archived',
|
|
58
58
|
created: 'Created:',
|
|
59
|
-
|
|
59
|
+
recentFailed: 'Could not load resources.',
|
|
60
|
+
retry: 'Try again',
|
|
61
|
+
loadingKnowledgeBase: 'Loading knowledge base...',
|
|
60
62
|
},
|
|
61
63
|
ToolbarPanels: ({ children }: any) => <div data-testid="toolbar-panels">{children}</div>,
|
|
62
64
|
...overrides,
|
|
@@ -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
|
|
@@ -58,6 +65,8 @@ export interface ResourceDiscoveryPageProps {
|
|
|
58
65
|
archived: string;
|
|
59
66
|
created: string;
|
|
60
67
|
loadingKnowledgeBase: string;
|
|
68
|
+
recentFailed: string;
|
|
69
|
+
retry: string;
|
|
61
70
|
};
|
|
62
71
|
|
|
63
72
|
// Component dependencies
|
|
@@ -69,6 +78,8 @@ export function ResourceDiscoveryPage({
|
|
|
69
78
|
searchDocuments,
|
|
70
79
|
entityTypes,
|
|
71
80
|
isLoadingRecent,
|
|
81
|
+
recentError = null,
|
|
82
|
+
onRetryRecent,
|
|
72
83
|
isSearching,
|
|
73
84
|
searchQuery,
|
|
74
85
|
onSearchQueryChange,
|
|
@@ -116,6 +127,20 @@ export function ResourceDiscoveryPage({
|
|
|
116
127
|
}
|
|
117
128
|
}, []);
|
|
118
129
|
|
|
130
|
+
// A terminally failed load is not still loading — check it first.
|
|
131
|
+
if (recentError) {
|
|
132
|
+
return (
|
|
133
|
+
<div className="semiont-page__loading">
|
|
134
|
+
<p className="semiont-page__loading-text">{t.recentFailed}</p>
|
|
135
|
+
{onRetryRecent && (
|
|
136
|
+
<button type="button" onClick={onRetryRecent} className="semiont-button" data-variant="secondary">
|
|
137
|
+
{t.retry}
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
119
144
|
// Loading state
|
|
120
145
|
if (isLoadingRecent) {
|
|
121
146
|
return (
|