@semiont/react-ui 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -55
- package/dist/{PdfAnnotationCanvas.client-CN3C3S55.js → PdfAnnotationCanvas.client-NIMALXNZ.js} +7 -27
- package/dist/PdfAnnotationCanvas.client-NIMALXNZ.js.map +1 -0
- package/dist/{ar-U2EXWUMQ.js → ar-SONK6MON.js} +3 -7
- package/dist/ar-SONK6MON.js.map +1 -0
- package/dist/{bn-DRJGV772.js → bn-ZKPRITNG.js} +3 -7
- package/dist/bn-ZKPRITNG.js.map +1 -0
- package/dist/{chunk-3Q3TUKWP.js → chunk-Y2EEAOMZ.js} +29 -29
- package/dist/{cs-PTWDM23V.js → cs-LPXQ7NHQ.js} +3 -7
- package/dist/cs-LPXQ7NHQ.js.map +1 -0
- package/dist/{da-KSNIKYSS.js → da-6TKY7MCY.js} +6 -10
- package/dist/da-6TKY7MCY.js.map +1 -0
- package/dist/{de-F2XBEWFY.js → de-C3GNII74.js} +3 -7
- package/dist/de-C3GNII74.js.map +1 -0
- package/dist/{el-DLD2GWAP.js → el-UBCXQDJ7.js} +3 -7
- package/dist/el-UBCXQDJ7.js.map +1 -0
- package/dist/{es-WLPYWGB5.js → es-BQ23TRI7.js} +11 -15
- package/dist/es-BQ23TRI7.js.map +1 -0
- package/dist/{fa-BAXHSDZG.js → fa-AFTBZB77.js} +3 -7
- package/dist/fa-AFTBZB77.js.map +1 -0
- package/dist/{fi-FCHSYVOT.js → fi-WOYNLZC2.js} +3 -7
- package/dist/fi-WOYNLZC2.js.map +1 -0
- package/dist/{fr-3UERBSL6.js → fr-NDSMIFJM.js} +3 -7
- package/dist/fr-NDSMIFJM.js.map +1 -0
- package/dist/{he-F6F3FV2K.js → he-VJXVRDOY.js} +3 -7
- package/dist/he-VJXVRDOY.js.map +1 -0
- package/dist/{hi-4BK6IK7Q.js → hi-BF6PHIE2.js} +3 -7
- package/dist/hi-BF6PHIE2.js.map +1 -0
- package/dist/{id-7ECCWP3J.js → id-GXG5QCZY.js} +3 -7
- package/dist/id-GXG5QCZY.js.map +1 -0
- package/dist/index.css +103 -0
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +271 -120
- package/dist/index.js +877 -698
- package/dist/index.js.map +1 -1
- package/dist/{it-234Z6XK6.js → it-XKHHCBAF.js} +3 -7
- package/dist/it-XKHHCBAF.js.map +1 -0
- package/dist/{ja-PJWQI4OQ.js → ja-TX7VM4XD.js} +3 -7
- package/dist/ja-TX7VM4XD.js.map +1 -0
- package/dist/{ko-APUEW2RS.js → ko-DNC7EQ7J.js} +3 -7
- package/dist/ko-DNC7EQ7J.js.map +1 -0
- package/dist/{ms-PJBZWZWD.js → ms-POZGBKPH.js} +3 -7
- package/dist/ms-POZGBKPH.js.map +1 -0
- package/dist/{nl-L4C3ZBCU.js → nl-IRMTKI7Z.js} +4 -11
- package/dist/nl-IRMTKI7Z.js.map +1 -0
- package/dist/{no-QE5N5KNG.js → no-ZUDJA4S6.js} +20 -24
- package/dist/no-ZUDJA4S6.js.map +1 -0
- package/dist/{pl-5Q2D23PD.js → pl-2NGAXL5U.js} +3 -7
- package/dist/pl-2NGAXL5U.js.map +1 -0
- package/dist/{pt-AIGUOIOC.js → pt-ABMCXZUM.js} +118 -122
- package/dist/pt-ABMCXZUM.js.map +1 -0
- package/dist/{ro-T56CSHTY.js → ro-VOJP6O5X.js} +3 -7
- package/dist/ro-VOJP6O5X.js.map +1 -0
- package/dist/{sv-L4TJQ2UH.js → sv-4HVFIIE5.js} +43 -47
- package/dist/sv-4HVFIIE5.js.map +1 -0
- package/dist/test-utils.js +2 -2
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-6O7Y6O2Q.js → th-IFPZP3HQ.js} +3 -7
- package/dist/th-IFPZP3HQ.js.map +1 -0
- package/dist/{tr-D4CQCSNO.js → tr-2GYEAMJ4.js} +3 -7
- package/dist/tr-2GYEAMJ4.js.map +1 -0
- package/dist/{uk-2HMQG6ND.js → uk-XCJBVLLD.js} +3 -7
- package/dist/uk-XCJBVLLD.js.map +1 -0
- package/dist/{vi-XVJ4RUEJ.js → vi-4FR7CB2F.js} +3 -7
- package/dist/vi-4FR7CB2F.js.map +1 -0
- package/dist/{zh-K2KDPGHK.js → zh-NSKFOINB.js} +3 -7
- package/dist/zh-NSKFOINB.js.map +1 -0
- package/package.json +17 -13
- package/src/components/Button/__tests__/Button.test.tsx +0 -2
- package/src/components/CodeMirrorRenderer.tsx +2 -0
- package/src/components/ErrorBoundary.tsx +0 -9
- package/src/components/ProtectedErrorBoundary.css +119 -0
- package/src/components/ProtectedErrorBoundary.tsx +24 -15
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +0 -1
- package/src/components/__tests__/ErrorBoundary.test.tsx +20 -13
- package/src/components/__tests__/LiveRegion.hooks.test.tsx +1 -1
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +2 -1
- package/src/components/__tests__/ResizeHandle.test.tsx +0 -1
- package/src/components/__tests__/SessionExpiryBanner.test.tsx +0 -1
- package/src/components/__tests__/StatusDisplay.test.tsx +0 -1
- package/src/components/__tests__/Toast.test.tsx +2 -3
- package/src/components/__tests__/Toolbar.test.tsx +0 -1
- package/src/components/annotation/annotations.css +14 -0
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +3 -5
- package/src/components/annotation-popups/__tests__/SharedPopupElements.test.tsx +0 -1
- package/src/components/branding/__tests__/SemiontBranding.test.tsx +1 -2
- package/src/components/layout/__tests__/LeftSidebar.test.tsx +5 -6
- package/src/components/layout/__tests__/PageLayout.test.tsx +1 -3
- package/src/components/layout/__tests__/SkipLinks.a11y.test.tsx +8 -8
- package/src/components/layout/__tests__/UnifiedHeader.test.tsx +12 -1
- package/src/components/modals/__tests__/KeyboardShortcutsHelpModal.test.tsx +0 -1
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +3 -4
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +1 -2
- package/src/components/modals/__tests__/SearchModal.basic.test.tsx +1 -1
- package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +0 -5
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +1 -2
- package/src/components/modals/__tests__/SearchModal.visual.test.tsx +2 -2
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +0 -1
- package/src/components/navigation/NavigationMenu.tsx +1 -1
- package/src/components/navigation/__tests__/Footer.a11y.test.tsx +4 -0
- package/src/components/navigation/__tests__/Footer.test.tsx +3 -6
- package/src/components/navigation/__tests__/NavigationMenu.a11y.test.tsx +1 -1
- package/src/components/navigation/__tests__/NavigationMenu.test.tsx +7 -9
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +0 -1
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +1 -2
- package/src/components/navigation/__tests__/SortableResourceTab.test.tsx +0 -1
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +6 -4
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +10 -19
- package/src/components/resource/AnnotateView.tsx +35 -37
- package/src/components/resource/BrowseView.tsx +31 -31
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +0 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +12 -14
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +0 -5
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +4 -6
- package/src/components/resource/__tests__/event-formatting.test.ts +1 -1
- package/src/components/resource/panels/CollaborationPanel.tsx +1 -1
- package/src/components/resource/panels/JsonLdPanel.tsx +33 -16
- package/src/components/resource/panels/ReferencesPanel.tsx +1 -1
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -5
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +8 -7
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +14 -10
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +31 -18
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +7 -6
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +5 -6
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +19 -13
- package/src/components/resource/panels/__tests__/JsonLdPanel.test.tsx +95 -426
- package/src/components/resource/panels/__tests__/PanelHeader.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +5 -5
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +40 -7
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +30 -32
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +6 -6
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +7 -6
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +0 -1
- package/src/components/viewers/__tests__/ImageViewer.test.tsx +0 -1
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +7 -10
- package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +38 -27
- package/src/features/admin-exchange/components/ImportProgress.tsx +28 -34
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +2 -0
- package/src/features/auth/__tests__/SignUpForm.a11y.test.tsx +11 -12
- package/src/features/auth/__tests__/SignUpForm.test.tsx +3 -3
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -0
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +11 -9
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +2 -1
- package/src/features/resource-compose/components/ResourceComposePage.tsx +36 -9
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -8
- package/src/features/resource-discovery/__tests__/ResourceCard.test.tsx +0 -1
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +33 -35
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +12 -11
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +204 -11
- package/src/features/resource-discovery/state/discover-state-unit.ts +70 -11
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +2 -2
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +10 -7
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +37 -1
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +14 -7
- package/src/integrations/__tests__/css-modules-helper.test.tsx +2 -3
- package/src/integrations/__tests__/styled-components-theme.test.ts +1 -3
- package/src/styles/features/exchange.css +0 -30
- package/src/styles/index.css +1 -0
- package/translations/ar.json +1 -3
- package/translations/bn.json +1 -3
- package/translations/cs.json +1 -3
- package/translations/da.json +4 -6
- package/translations/de.json +1 -3
- package/translations/el.json +1 -3
- package/translations/es.json +9 -11
- package/translations/fa.json +1 -3
- package/translations/fi.json +1 -3
- package/translations/fr.json +1 -3
- package/translations/he.json +1 -3
- package/translations/hi.json +1 -3
- package/translations/id.json +1 -3
- package/translations/it.json +1 -3
- package/translations/ja.json +1 -3
- package/translations/ko.json +1 -3
- package/translations/ms.json +1 -3
- package/translations/nl.json +2 -7
- package/translations/no.json +18 -20
- package/translations/pl.json +1 -3
- package/translations/pt.json +116 -118
- package/translations/ro.json +1 -3
- package/translations/sv.json +41 -43
- package/translations/th.json +1 -3
- package/translations/tr.json +1 -3
- package/translations/uk.json +1 -3
- package/translations/vi.json +1 -3
- package/translations/zh.json +1 -3
- package/dist/PdfAnnotationCanvas.client-CN3C3S55.js.map +0 -1
- package/dist/ar-U2EXWUMQ.js.map +0 -1
- package/dist/bn-DRJGV772.js.map +0 -1
- package/dist/cs-PTWDM23V.js.map +0 -1
- package/dist/da-KSNIKYSS.js.map +0 -1
- package/dist/de-F2XBEWFY.js.map +0 -1
- package/dist/el-DLD2GWAP.js.map +0 -1
- package/dist/es-WLPYWGB5.js.map +0 -1
- package/dist/fa-BAXHSDZG.js.map +0 -1
- package/dist/fi-FCHSYVOT.js.map +0 -1
- package/dist/fr-3UERBSL6.js.map +0 -1
- package/dist/he-F6F3FV2K.js.map +0 -1
- package/dist/hi-4BK6IK7Q.js.map +0 -1
- package/dist/id-7ECCWP3J.js.map +0 -1
- package/dist/it-234Z6XK6.js.map +0 -1
- package/dist/ja-PJWQI4OQ.js.map +0 -1
- package/dist/ko-APUEW2RS.js.map +0 -1
- package/dist/ms-PJBZWZWD.js.map +0 -1
- package/dist/nl-L4C3ZBCU.js.map +0 -1
- package/dist/no-QE5N5KNG.js.map +0 -1
- package/dist/pl-5Q2D23PD.js.map +0 -1
- package/dist/pt-AIGUOIOC.js.map +0 -1
- package/dist/ro-T56CSHTY.js.map +0 -1
- package/dist/sv-L4TJQ2UH.js.map +0 -1
- package/dist/th-6O7Y6O2Q.js.map +0 -1
- package/dist/tr-D4CQCSNO.js.map +0 -1
- package/dist/uk-2HMQG6ND.js.map +0 -1
- package/dist/vi-XVJ4RUEJ.js.map +0 -1
- package/dist/zh-K2KDPGHK.js.map +0 -1
- /package/dist/{chunk-3Q3TUKWP.js.map → chunk-Y2EEAOMZ.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { BehaviorSubject, firstValueFrom } from 'rxjs';
|
|
3
|
-
import { filter } from 'rxjs/operators';
|
|
3
|
+
import { filter, skip, take, toArray } from 'rxjs/operators';
|
|
4
4
|
import type { SemiontClient } from '@semiont/sdk';
|
|
5
5
|
import type { ShellStateUnit } from '../../../../state/shell-state-unit';
|
|
6
6
|
import { createDiscoverStateUnit } from '../discover-state-unit';
|
|
@@ -9,23 +9,43 @@ function mockBrowse(): ShellStateUnit {
|
|
|
9
9
|
return { dispose: vi.fn() } as unknown as ShellStateUnit;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface BrowseFilters {
|
|
13
|
+
limit?: number;
|
|
14
|
+
archived?: boolean;
|
|
15
|
+
search?: string;
|
|
16
|
+
entityType?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
function mockClient(overrides: {
|
|
13
20
|
resources$?: BehaviorSubject<unknown[] | undefined>;
|
|
14
21
|
entityTypes$?: BehaviorSubject<string[] | undefined>;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
22
|
+
resourcesFn?: (filters: BrowseFilters) => BehaviorSubject<unknown[] | undefined>;
|
|
23
|
+
} = {}): { client: SemiontClient; resourceCalls: BrowseFilters[] } {
|
|
24
|
+
const resourceCalls: BrowseFilters[] = [];
|
|
25
|
+
const defaultResources$ =
|
|
26
|
+
overrides.resources$ ?? new BehaviorSubject<unknown[] | undefined>([{ '@id': 'r1' }]);
|
|
27
|
+
const entityTypes$ =
|
|
28
|
+
overrides.entityTypes$ ?? new BehaviorSubject<string[] | undefined>(['Person']);
|
|
29
|
+
|
|
30
|
+
const resourcesFn = overrides.resourcesFn ?? (() => defaultResources$);
|
|
31
|
+
|
|
32
|
+
const client = {
|
|
19
33
|
browse: {
|
|
20
|
-
resources: () =>
|
|
34
|
+
resources: (filters: BrowseFilters = {}) => {
|
|
35
|
+
resourceCalls.push(filters);
|
|
36
|
+
return resourcesFn(filters).asObservable();
|
|
37
|
+
},
|
|
21
38
|
entityTypes: () => entityTypes$.asObservable(),
|
|
22
39
|
},
|
|
23
40
|
} as unknown as SemiontClient;
|
|
41
|
+
|
|
42
|
+
return { client, resourceCalls };
|
|
24
43
|
}
|
|
25
44
|
|
|
26
45
|
describe('createDiscoverStateUnit', () => {
|
|
27
46
|
it('exposes recent resources from browse namespace', async () => {
|
|
28
|
-
const
|
|
47
|
+
const { client } = mockClient();
|
|
48
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
29
49
|
|
|
30
50
|
const recent = await firstValueFrom(stateUnit.recentResources$);
|
|
31
51
|
expect(recent).toEqual([{ '@id': 'r1' }]);
|
|
@@ -34,7 +54,8 @@ describe('createDiscoverStateUnit', () => {
|
|
|
34
54
|
});
|
|
35
55
|
|
|
36
56
|
it('exposes entity types from browse namespace', async () => {
|
|
37
|
-
const
|
|
57
|
+
const { client } = mockClient();
|
|
58
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
38
59
|
|
|
39
60
|
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
40
61
|
expect(types).toEqual(['Person']);
|
|
@@ -42,9 +63,21 @@ describe('createDiscoverStateUnit', () => {
|
|
|
42
63
|
stateUnit.dispose();
|
|
43
64
|
});
|
|
44
65
|
|
|
66
|
+
it('falls back to [] when entityTypes() emits undefined', async () => {
|
|
67
|
+
const entityTypes$ = new BehaviorSubject<string[] | undefined>(undefined);
|
|
68
|
+
const { client } = mockClient({ entityTypes$ });
|
|
69
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
70
|
+
|
|
71
|
+
const types = await firstValueFrom(stateUnit.entityTypes$);
|
|
72
|
+
expect(types).toEqual([]);
|
|
73
|
+
|
|
74
|
+
stateUnit.dispose();
|
|
75
|
+
});
|
|
76
|
+
|
|
45
77
|
it('reports loading when resources are undefined', async () => {
|
|
46
78
|
const resources$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
47
|
-
const
|
|
79
|
+
const { client } = mockClient({ resources$ });
|
|
80
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
48
81
|
|
|
49
82
|
const loading = await firstValueFrom(stateUnit.isLoadingRecent$);
|
|
50
83
|
expect(loading).toBe(true);
|
|
@@ -57,7 +90,8 @@ describe('createDiscoverStateUnit', () => {
|
|
|
57
90
|
});
|
|
58
91
|
|
|
59
92
|
it('exposes a search pipeline', () => {
|
|
60
|
-
const
|
|
93
|
+
const { client } = mockClient();
|
|
94
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
61
95
|
|
|
62
96
|
expect(stateUnit.search).toBeDefined();
|
|
63
97
|
expect(typeof stateUnit.search.setQuery).toBe('function');
|
|
@@ -68,9 +102,168 @@ describe('createDiscoverStateUnit', () => {
|
|
|
68
102
|
|
|
69
103
|
it('disposes browse and search on dispose', () => {
|
|
70
104
|
const browse = mockBrowse();
|
|
71
|
-
const
|
|
105
|
+
const { client } = mockClient();
|
|
106
|
+
const stateUnit = createDiscoverStateUnit(client, browse);
|
|
72
107
|
stateUnit.dispose();
|
|
73
108
|
|
|
74
109
|
expect(browse.dispose).toHaveBeenCalled();
|
|
75
110
|
});
|
|
111
|
+
|
|
112
|
+
it('initial selectedEntityType$ is empty and recent fetch carries no entityType', async () => {
|
|
113
|
+
const { client, resourceCalls } = mockClient();
|
|
114
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
115
|
+
|
|
116
|
+
await firstValueFrom(stateUnit.recentResources$);
|
|
117
|
+
|
|
118
|
+
expect(resourceCalls).toHaveLength(1);
|
|
119
|
+
expect(resourceCalls[0]).toEqual({ limit: 10, archived: false });
|
|
120
|
+
expect(await firstValueFrom(stateUnit.selectedEntityType$)).toBe('');
|
|
121
|
+
|
|
122
|
+
stateUnit.dispose();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('setSelectedEntityType drives a refetch with the entityType filter', async () => {
|
|
126
|
+
const { client, resourceCalls } = mockClient();
|
|
127
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
128
|
+
|
|
129
|
+
// Prime the first subscription so the switchMap is live.
|
|
130
|
+
const sub = stateUnit.recentResources$.subscribe();
|
|
131
|
+
|
|
132
|
+
stateUnit.setSelectedEntityType('Person');
|
|
133
|
+
|
|
134
|
+
expect(await firstValueFrom(stateUnit.selectedEntityType$)).toBe('Person');
|
|
135
|
+
// Two calls expected: initial '' then 'Person'.
|
|
136
|
+
expect(resourceCalls.length).toBeGreaterThanOrEqual(2);
|
|
137
|
+
expect(resourceCalls.at(-1)).toEqual({ limit: 10, archived: false, entityType: 'Person' });
|
|
138
|
+
|
|
139
|
+
sub.unsubscribe();
|
|
140
|
+
stateUnit.dispose();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('search with an empty query yields no results without hitting the wire', async () => {
|
|
144
|
+
vi.useFakeTimers();
|
|
145
|
+
try {
|
|
146
|
+
const { client, resourceCalls } = mockClient();
|
|
147
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
148
|
+
|
|
149
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
150
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
151
|
+
|
|
152
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
153
|
+
|
|
154
|
+
expect(collected.at(-1)).toEqual({ results: [], isSearching: false });
|
|
155
|
+
const searchCalls = resourceCalls.filter((c) => c.search !== undefined);
|
|
156
|
+
expect(searchCalls).toHaveLength(0);
|
|
157
|
+
|
|
158
|
+
sub.unsubscribe();
|
|
159
|
+
stateUnit.dispose();
|
|
160
|
+
} finally {
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('search with a non-empty query and selected entityType pushes both into the filter', async () => {
|
|
166
|
+
vi.useFakeTimers();
|
|
167
|
+
try {
|
|
168
|
+
const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'hit' }]);
|
|
169
|
+
const { client, resourceCalls } = mockClient({
|
|
170
|
+
resourcesFn: (filters) => (filters.search ? results$ : new BehaviorSubject<unknown[] | undefined>([])),
|
|
171
|
+
});
|
|
172
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
173
|
+
|
|
174
|
+
const sub = stateUnit.search.state$.subscribe();
|
|
175
|
+
stateUnit.setSelectedEntityType('Person');
|
|
176
|
+
stateUnit.search.setQuery('lincoln');
|
|
177
|
+
|
|
178
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
179
|
+
|
|
180
|
+
const searchCalls = resourceCalls.filter((c) => c.search !== undefined);
|
|
181
|
+
expect(searchCalls.length).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect(searchCalls.at(-1)).toEqual({
|
|
183
|
+
search: 'lincoln',
|
|
184
|
+
limit: 20,
|
|
185
|
+
entityType: 'Person',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
sub.unsubscribe();
|
|
189
|
+
stateUnit.dispose();
|
|
190
|
+
} finally {
|
|
191
|
+
vi.useRealTimers();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('search results flow through state$ once the debounced query fetches', async () => {
|
|
196
|
+
vi.useFakeTimers();
|
|
197
|
+
try {
|
|
198
|
+
const results$ = new BehaviorSubject<unknown[] | undefined>([{ '@id': 'hit' }]);
|
|
199
|
+
const { client } = mockClient({
|
|
200
|
+
resourcesFn: () => results$,
|
|
201
|
+
});
|
|
202
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
203
|
+
|
|
204
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
205
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
206
|
+
|
|
207
|
+
stateUnit.search.setQuery('lincoln');
|
|
208
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
209
|
+
|
|
210
|
+
expect(collected.at(-1)).toEqual({ results: [{ '@id': 'hit' }], isSearching: false });
|
|
211
|
+
|
|
212
|
+
sub.unsubscribe();
|
|
213
|
+
stateUnit.dispose();
|
|
214
|
+
} finally {
|
|
215
|
+
vi.useRealTimers();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('search reports isSearching while the fetch is in flight', async () => {
|
|
220
|
+
vi.useFakeTimers();
|
|
221
|
+
try {
|
|
222
|
+
const inflight$ = new BehaviorSubject<unknown[] | undefined>(undefined);
|
|
223
|
+
const { client } = mockClient({
|
|
224
|
+
resourcesFn: () => inflight$,
|
|
225
|
+
});
|
|
226
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
227
|
+
|
|
228
|
+
const collected: Array<{ results: unknown[]; isSearching: boolean }> = [];
|
|
229
|
+
const sub = stateUnit.search.state$.subscribe((s) => collected.push(s));
|
|
230
|
+
|
|
231
|
+
stateUnit.search.setQuery('lincoln');
|
|
232
|
+
await vi.advanceTimersByTimeAsync(300);
|
|
233
|
+
|
|
234
|
+
expect(collected.at(-1)).toEqual({ results: [], isSearching: true });
|
|
235
|
+
|
|
236
|
+
sub.unsubscribe();
|
|
237
|
+
stateUnit.dispose();
|
|
238
|
+
} finally {
|
|
239
|
+
vi.useRealTimers();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('search query observable echoes the latest setQuery value', async () => {
|
|
244
|
+
const { client } = mockClient();
|
|
245
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
246
|
+
|
|
247
|
+
const queries = stateUnit.search.query$.pipe(skip(1), take(1), toArray()).toPromise();
|
|
248
|
+
stateUnit.search.setQuery('alpha');
|
|
249
|
+
|
|
250
|
+
expect(await queries).toEqual(['alpha']);
|
|
251
|
+
|
|
252
|
+
stateUnit.dispose();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('omits entityType from the filter when the empty sentinel is selected', async () => {
|
|
256
|
+
const { client, resourceCalls } = mockClient();
|
|
257
|
+
const stateUnit = createDiscoverStateUnit(client, mockBrowse());
|
|
258
|
+
|
|
259
|
+
const sub = stateUnit.recentResources$.subscribe();
|
|
260
|
+
stateUnit.setSelectedEntityType('Person');
|
|
261
|
+
stateUnit.setSelectedEntityType('');
|
|
262
|
+
|
|
263
|
+
const last = resourceCalls.at(-1)!;
|
|
264
|
+
expect(last.entityType).toBeUndefined();
|
|
265
|
+
|
|
266
|
+
sub.unsubscribe();
|
|
267
|
+
stateUnit.dispose();
|
|
268
|
+
});
|
|
76
269
|
});
|
|
@@ -1,20 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BehaviorSubject, Subject, combineLatest, of, type Observable } from 'rxjs';
|
|
2
|
+
import { debounceTime, distinctUntilChanged, map, startWith, switchMap, shareReplay } from 'rxjs/operators';
|
|
2
3
|
import type { ResourceDescriptor } from '@semiont/core';
|
|
3
|
-
import type { StateUnit } from '@semiont/sdk';
|
|
4
|
+
import type { SemiontClient, StateUnit } from '@semiont/sdk';
|
|
4
5
|
import { createDisposer } from '@semiont/sdk';
|
|
5
6
|
import type { ShellStateUnit } from '../../../state/shell-state-unit';
|
|
6
|
-
import { createSearchPipeline, type SearchPipeline } from '@semiont/sdk';
|
|
7
|
-
import type { SemiontClient } from '@semiont/sdk';
|
|
8
7
|
|
|
9
8
|
const RECENT_LIMIT = 10;
|
|
10
9
|
const SEARCH_LIMIT = 20;
|
|
10
|
+
const DEBOUNCE_MS = 250;
|
|
11
|
+
|
|
12
|
+
export interface DiscoverSearchPipeline {
|
|
13
|
+
query$: Observable<string>;
|
|
14
|
+
state$: Observable<{ results: ResourceDescriptor[]; isSearching: boolean }>;
|
|
15
|
+
setQuery(value: string): void;
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
export interface DiscoverStateUnit extends StateUnit {
|
|
13
19
|
browse: ShellStateUnit;
|
|
14
|
-
search:
|
|
20
|
+
search: DiscoverSearchPipeline;
|
|
15
21
|
recentResources$: Observable<ResourceDescriptor[]>;
|
|
16
22
|
entityTypes$: Observable<string[]>;
|
|
17
23
|
isLoadingRecent$: Observable<boolean>;
|
|
24
|
+
selectedEntityType$: Observable<string>;
|
|
25
|
+
setSelectedEntityType(value: string): void;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function createDiscoverStateUnit(
|
|
@@ -22,14 +30,27 @@ export function createDiscoverStateUnit(
|
|
|
22
30
|
browse: ShellStateUnit,
|
|
23
31
|
): DiscoverStateUnit {
|
|
24
32
|
const disposer = createDisposer();
|
|
25
|
-
|
|
26
|
-
const search = createSearchPipeline<ResourceDescriptor>((q) =>
|
|
27
|
-
client.browse.resources({ search: q, limit: SEARCH_LIMIT }),
|
|
28
|
-
);
|
|
29
|
-
disposer.add(search);
|
|
30
33
|
disposer.add(browse);
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
// Selected entity-type chip on the Discover page. Drives both the
|
|
36
|
+
// `recent` list and the search results — filtering happens on the
|
|
37
|
+
// backend, not via post-fetch array filtering.
|
|
38
|
+
const selectedEntityType$ = new BehaviorSubject<string>('');
|
|
39
|
+
disposer.add(() => selectedEntityType$.complete());
|
|
40
|
+
|
|
41
|
+
const queryInput$ = new Subject<string>();
|
|
42
|
+
disposer.add(() => queryInput$.complete());
|
|
43
|
+
|
|
44
|
+
const recent$ = selectedEntityType$.pipe(
|
|
45
|
+
switchMap((et) =>
|
|
46
|
+
client.browse.resources({
|
|
47
|
+
limit: RECENT_LIMIT,
|
|
48
|
+
archived: false,
|
|
49
|
+
...(et ? { entityType: et } : {}),
|
|
50
|
+
}),
|
|
51
|
+
),
|
|
52
|
+
shareReplay({ bufferSize: 1, refCount: true }),
|
|
53
|
+
);
|
|
33
54
|
|
|
34
55
|
const recentResources$: Observable<ResourceDescriptor[]> = recent$.pipe(
|
|
35
56
|
map((r) => r ?? []),
|
|
@@ -43,12 +64,50 @@ export function createDiscoverStateUnit(
|
|
|
43
64
|
map((e) => e ?? []),
|
|
44
65
|
);
|
|
45
66
|
|
|
67
|
+
const debouncedQuery$ = queryInput$.pipe(
|
|
68
|
+
startWith(''),
|
|
69
|
+
debounceTime(DEBOUNCE_MS),
|
|
70
|
+
distinctUntilChanged(),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const state$: Observable<{ results: ResourceDescriptor[]; isSearching: boolean }> =
|
|
74
|
+
combineLatest([debouncedQuery$, selectedEntityType$]).pipe(
|
|
75
|
+
switchMap(([q, et]) => {
|
|
76
|
+
const trimmed = q.trim();
|
|
77
|
+
if (!trimmed) {
|
|
78
|
+
return of({ results: [] as ResourceDescriptor[], isSearching: false });
|
|
79
|
+
}
|
|
80
|
+
return client.browse
|
|
81
|
+
.resources({
|
|
82
|
+
search: trimmed,
|
|
83
|
+
limit: SEARCH_LIMIT,
|
|
84
|
+
...(et ? { entityType: et } : {}),
|
|
85
|
+
})
|
|
86
|
+
.pipe(
|
|
87
|
+
map((results) => ({
|
|
88
|
+
results: results ?? [],
|
|
89
|
+
isSearching: results === undefined,
|
|
90
|
+
})),
|
|
91
|
+
startWith({ results: [] as ResourceDescriptor[], isSearching: true }),
|
|
92
|
+
);
|
|
93
|
+
}),
|
|
94
|
+
shareReplay({ bufferSize: 1, refCount: true }),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const search: DiscoverSearchPipeline = {
|
|
98
|
+
query$: queryInput$.pipe(startWith('')),
|
|
99
|
+
state$,
|
|
100
|
+
setQuery: (value) => queryInput$.next(value),
|
|
101
|
+
};
|
|
102
|
+
|
|
46
103
|
return {
|
|
47
104
|
browse,
|
|
48
105
|
search,
|
|
49
106
|
recentResources$,
|
|
50
107
|
entityTypes$,
|
|
51
108
|
isLoadingRecent$,
|
|
109
|
+
selectedEntityType$: selectedEntityType$.asObservable(),
|
|
110
|
+
setSelectedEntityType: (value) => selectedEntityType$.next(value),
|
|
52
111
|
dispose: () => disposer.dispose(),
|
|
53
112
|
};
|
|
54
113
|
}
|
|
@@ -13,6 +13,7 @@ import type { ResourceViewerPageProps } from '../components/ResourceViewerPage';
|
|
|
13
13
|
import { ToastProvider } from '../../../components/Toast';
|
|
14
14
|
import { ThemeProvider } from '../../../contexts/ThemeContext';
|
|
15
15
|
import { createTestSemiontWrapper } from '../../../test-utils';
|
|
16
|
+
import type { ResourceId } from '@semiont/core';
|
|
16
17
|
|
|
17
18
|
// jsdom doesn't implement window.matchMedia — mock it for useTheme
|
|
18
19
|
Object.defineProperty(window, 'matchMedia', {
|
|
@@ -131,7 +132,7 @@ vi.mock('@/components/toolbar/ToolbarPanels', () => ({
|
|
|
131
132
|
const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): ResourceViewerPageProps => ({
|
|
132
133
|
resource: {
|
|
133
134
|
'@context': 'https://www.w3.org/ns/anno.jsonld',
|
|
134
|
-
'@id': 'test-123',
|
|
135
|
+
'@id': 'test-123' as ResourceId,
|
|
135
136
|
'@type': 'schema:DigitalDocument',
|
|
136
137
|
name: 'Test Resource',
|
|
137
138
|
description: 'A test resource for unit testing',
|
|
@@ -147,7 +148,6 @@ const createMockProps = (overrides?: Partial<ResourceViewerPageProps>): Resource
|
|
|
147
148
|
},
|
|
148
149
|
rUri: 'test-123' as any,
|
|
149
150
|
locale: 'en',
|
|
150
|
-
cacheManager: {},
|
|
151
151
|
Link: ({ children }: any) => <a>{children}</a>,
|
|
152
152
|
routes: {},
|
|
153
153
|
refetchDocument: vi.fn().mockResolvedValue(undefined),
|
|
@@ -9,8 +9,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
9
9
|
import type { components, ResourceDescriptor, ResourceId, GatheredContext, EventMap } from '@semiont/core';
|
|
10
10
|
import type { ConnectionState } from '@semiont/core';
|
|
11
11
|
import { annotationId } from '@semiont/core';
|
|
12
|
-
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType } from '@semiont/core';
|
|
13
|
-
import { getMimeCategory } from '@semiont/core';
|
|
12
|
+
import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, capabilitiesOf } from '@semiont/core';
|
|
14
13
|
import { ANNOTATORS } from '@semiont/react-ui';
|
|
15
14
|
import { ErrorBoundary } from '@semiont/react-ui';
|
|
16
15
|
import { AnnotationHistory } from '@semiont/react-ui';
|
|
@@ -141,9 +140,12 @@ export function ResourceViewerPage({
|
|
|
141
140
|
const { hoverDelayMs } = useHoverDelay();
|
|
142
141
|
const { triggerSparkleAnimation, clearNewAnnotationId } = useResourceAnnotations();
|
|
143
142
|
|
|
144
|
-
//
|
|
143
|
+
// Render mode chooses the content path: 'text' decodes inline; 'image'
|
|
144
|
+
// and 'pdf' go through the media-token (binary) path. 'none'/registry-miss
|
|
145
|
+
// fall to the text path harmlessly — the viewer shows metadata + download.
|
|
145
146
|
const resourceMediaType = getPrimaryMediaType(resource) || 'text/plain';
|
|
146
|
-
const
|
|
147
|
+
const renderMode = capabilitiesOf(resourceMediaType)?.render;
|
|
148
|
+
const isBinary = renderMode === 'image' || renderMode === 'pdf';
|
|
147
149
|
|
|
148
150
|
// Text path: fetch and decode representation (disabled for binary — mediaToken path handles those)
|
|
149
151
|
const { content: textContent, loading: textLoading } = useResourceContent(rUri, resource, !isBinary);
|
|
@@ -265,8 +267,9 @@ export function ResourceViewerPage({
|
|
|
265
267
|
|
|
266
268
|
// Domain events flow through the bus gateway (ActorStateUnit → local EventBus).
|
|
267
269
|
// BrowseNamespace cache invalidation handles annotation/resource updates.
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
+
// Resource-scoped freshness follows observation (#847): subscribing to the
|
|
271
|
+
// resource's `browse.*` live queries acquires its scope (which bridges scoped
|
|
272
|
+
// domain events into the local EventBus) and releases it on teardown.
|
|
270
273
|
|
|
271
274
|
const handleResourceArchive = useCallback(async () => {
|
|
272
275
|
if (!semiont) return;
|
|
@@ -576,7 +579,7 @@ export function ResourceViewerPage({
|
|
|
576
579
|
|
|
577
580
|
{/* JSON-LD Panel */}
|
|
578
581
|
{activePanel === 'jsonld' && (
|
|
579
|
-
<JsonLdPanel
|
|
582
|
+
<JsonLdPanel resourceId={rUri} />
|
|
580
583
|
)}
|
|
581
584
|
</ToolbarPanels>
|
|
582
585
|
|
package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts
CHANGED
|
@@ -56,7 +56,6 @@ function clientWithNamespaces(overrides: {
|
|
|
56
56
|
match: { search: vi.fn(() => new Observable(() => {})) },
|
|
57
57
|
yield: { fromAnnotation: vi.fn(() => new Observable(() => {})) },
|
|
58
58
|
bind: { body: vi.fn().mockResolvedValue(undefined) },
|
|
59
|
-
subscribeToResource: vi.fn().mockReturnValue(() => {}),
|
|
60
59
|
});
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -151,6 +150,43 @@ describe('createResourceViewerPageStateUnit', () => {
|
|
|
151
150
|
stateUnit.dispose();
|
|
152
151
|
});
|
|
153
152
|
|
|
153
|
+
// isBinaryType keys off textExtractionOf(...) !== 'decode', not render mode:
|
|
154
|
+
// storage-tier binary (ZIP, gif/webp) must be fetched as bytes, never
|
|
155
|
+
// decoded as text — the client-side twin of the Phase 3a serving-side fix.
|
|
156
|
+
it('fetches storage-tier binary (ZIP) as bytes, never the text-decode path', async () => {
|
|
157
|
+
const mediaToken = vi.fn().mockResolvedValue({ token: 'tok-zip' });
|
|
158
|
+
const resourceRepresentation = vi.fn();
|
|
159
|
+
tc = clientWithNamespaces({ mediaToken, resourceRepresentation });
|
|
160
|
+
const stateUnit = createResourceViewerPageStateUnit(
|
|
161
|
+
tc.client, RID, 'en', mockBrowse(),
|
|
162
|
+
{ mediaType: 'application/zip' },
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const token = await firstValueFrom(stateUnit.mediaToken$.pipe(filter((t) => t !== null)));
|
|
166
|
+
expect(token).toBe('tok-zip');
|
|
167
|
+
expect(resourceRepresentation).not.toHaveBeenCalled();
|
|
168
|
+
|
|
169
|
+
stateUnit.dispose();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('decodes a registry-miss text/* subtype via the text path (RFC 2046)', async () => {
|
|
173
|
+
const mediaToken = vi.fn();
|
|
174
|
+
const resourceRepresentation = vi.fn().mockResolvedValue({
|
|
175
|
+
data: new TextEncoder().encode('hi').buffer,
|
|
176
|
+
contentType: 'text/x-custom',
|
|
177
|
+
});
|
|
178
|
+
tc = clientWithNamespaces({ mediaToken, resourceRepresentation });
|
|
179
|
+
const stateUnit = createResourceViewerPageStateUnit(
|
|
180
|
+
tc.client, RID, 'en', mockBrowse(),
|
|
181
|
+
{ mediaType: 'text/x-custom' },
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
expect(resourceRepresentation).toHaveBeenCalled();
|
|
185
|
+
expect(mediaToken).not.toHaveBeenCalled();
|
|
186
|
+
|
|
187
|
+
stateUnit.dispose();
|
|
188
|
+
});
|
|
189
|
+
|
|
154
190
|
it('wizard initializes closed', async () => {
|
|
155
191
|
tc = clientWithNamespaces();
|
|
156
192
|
const stateUnit = createResourceViewerPageStateUnit(tc.client, RID, 'en', mockBrowse());
|
|
@@ -9,7 +9,7 @@ import { createGatherStateUnit, type GatherStateUnit } from '@semiont/sdk';
|
|
|
9
9
|
import { createMatchStateUnit } from '@semiont/sdk';
|
|
10
10
|
import { createYieldStateUnit, type YieldStateUnit } from '@semiont/sdk';
|
|
11
11
|
import type { SemiontClient } from '@semiont/sdk';
|
|
12
|
-
import { decodeWithCharset } from '@semiont/core';
|
|
12
|
+
import { decodeWithCharset, textExtractionOf } from '@semiont/core';
|
|
13
13
|
import { isHighlight, isComment, isAssessment, isReference, isTag } from '@semiont/core';
|
|
14
14
|
import type { ReferencedByEntry } from '@semiont/sdk';
|
|
15
15
|
|
|
@@ -113,13 +113,17 @@ export function createResourceViewerPageStateUnit(
|
|
|
113
113
|
const mediaToken$ = new BehaviorSubject<string | null>(null);
|
|
114
114
|
|
|
115
115
|
const mediaType = options?.mediaType || 'text/plain';
|
|
116
|
-
|
|
116
|
+
// "Fetch raw bytes or decode as text?" — binary iff the registry says this
|
|
117
|
+
// type does not decode to text. Storage-tier images (gif/webp) are
|
|
118
|
+
// render:'none' but still binary, and a ZIP must avoid the text path; a
|
|
119
|
+
// mechanical render-mode check would mis-route both into mojibake.
|
|
120
|
+
const isBinaryType = textExtractionOf(mediaType) !== 'decode';
|
|
117
121
|
|
|
118
122
|
if (!isBinaryType && mediaType) {
|
|
119
123
|
contentLoading$.next(true);
|
|
120
|
-
client.browse.resourceRepresentation(resourceId
|
|
121
|
-
.then(({ data }) => {
|
|
122
|
-
content$.next(decodeWithCharset(data,
|
|
124
|
+
client.browse.resourceRepresentation(resourceId)
|
|
125
|
+
.then(({ data, contentType }) => {
|
|
126
|
+
content$.next(decodeWithCharset(data, contentType));
|
|
123
127
|
contentLoading$.next(false);
|
|
124
128
|
})
|
|
125
129
|
.catch(() => { contentLoading$.next(false); });
|
|
@@ -133,8 +137,11 @@ export function createResourceViewerPageStateUnit(
|
|
|
133
137
|
|
|
134
138
|
const wizard$ = new BehaviorSubject<WizardState>(WIZARD_CLOSED);
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
// Resource-scoped freshness follows observation (#847): subscribing to the
|
|
141
|
+
// `browse.*(resourceId)` live queries exposed by this state unit
|
|
142
|
+
// (annotations$, events$, referencedBy$) acquires the resource scope for as
|
|
143
|
+
// long as they're observed and releases it on teardown — so no manual
|
|
144
|
+
// `subscribeToResource` call is needed.
|
|
138
145
|
|
|
139
146
|
const bindInitiateSub = client.bus.get('bind:initiate').subscribe((event) => {
|
|
140
147
|
wizard$.next({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import React from 'react';
|
|
3
2
|
import { render } from '@testing-library/react';
|
|
4
3
|
import {
|
|
5
4
|
createSemiontClassName,
|
|
@@ -96,7 +95,7 @@ describe('css-modules-helper', () => {
|
|
|
96
95
|
|
|
97
96
|
describe('mergeDataAttributes', () => {
|
|
98
97
|
it('merges data attributes into props', () => {
|
|
99
|
-
const props = { id: 'btn', className: 'foo' };
|
|
98
|
+
const props: Record<string, string> = { id: 'btn', className: 'foo' };
|
|
100
99
|
const dataAttrs = { 'data-variant': 'primary', 'data-size': 'md' };
|
|
101
100
|
const merged = mergeDataAttributes(props, dataAttrs);
|
|
102
101
|
|
|
@@ -107,7 +106,7 @@ describe('css-modules-helper', () => {
|
|
|
107
106
|
});
|
|
108
107
|
|
|
109
108
|
it('skips undefined data attributes', () => {
|
|
110
|
-
const props = { id: 'btn' };
|
|
109
|
+
const props: Record<string, string> = { id: 'btn' };
|
|
111
110
|
const dataAttrs = { 'data-variant': 'primary', 'data-size': undefined };
|
|
112
111
|
const merged = mergeDataAttributes(props, dataAttrs);
|
|
113
112
|
|
|
@@ -122,14 +122,12 @@ describe('styled-components-theme', () => {
|
|
|
122
122
|
|
|
123
123
|
it('calls styled.button.attrs', () => {
|
|
124
124
|
let attrsArg: any;
|
|
125
|
-
let templateArg: any;
|
|
126
125
|
|
|
127
126
|
const mockStyled = {
|
|
128
127
|
button: {
|
|
129
128
|
attrs: (arg: any) => {
|
|
130
129
|
attrsArg = arg;
|
|
131
|
-
return (
|
|
132
|
-
templateArg = { strings, exprs };
|
|
130
|
+
return (_strings: TemplateStringsArray) => {
|
|
133
131
|
return 'MockComponent';
|
|
134
132
|
};
|
|
135
133
|
},
|
|
@@ -362,36 +362,6 @@
|
|
|
362
362
|
color: var(--semiont-color-gray-400);
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
/* Hash chain badge */
|
|
366
|
-
.semiont-exchange__hash-badge {
|
|
367
|
-
display: inline-flex;
|
|
368
|
-
align-items: center;
|
|
369
|
-
padding: 0.25rem 0.75rem;
|
|
370
|
-
border-radius: 9999px;
|
|
371
|
-
font-size: 0.75rem;
|
|
372
|
-
font-weight: 500;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
.semiont-exchange__hash-badge--valid {
|
|
376
|
-
background-color: var(--semiont-color-green-100);
|
|
377
|
-
color: var(--semiont-color-green-800);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
[data-theme="dark"] .semiont-exchange__hash-badge--valid {
|
|
381
|
-
background-color: rgba(34, 197, 94, 0.15);
|
|
382
|
-
color: var(--semiont-color-green-300);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
.semiont-exchange__hash-badge--invalid {
|
|
386
|
-
background-color: var(--semiont-color-red-100);
|
|
387
|
-
color: var(--semiont-color-red-800);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
[data-theme="dark"] .semiont-exchange__hash-badge--invalid {
|
|
391
|
-
background-color: rgba(239, 68, 68, 0.15);
|
|
392
|
-
color: var(--semiont-color-red-300);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
365
|
/* Error message */
|
|
396
366
|
.semiont-exchange__error-message {
|
|
397
367
|
font-size: 0.875rem;
|
package/src/styles/index.css
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
@import '../components/annotation/references.css';
|
|
92
92
|
@import '../components/Toast.css';
|
|
93
93
|
@import '../components/StatusDisplay.css';
|
|
94
|
+
@import '../components/ProtectedErrorBoundary.css';
|
|
94
95
|
@import '../components/loading-states/loading.css';
|
|
95
96
|
@import '../components/error-states/errors.css';
|
|
96
97
|
@import '../features/auth/auth.css';
|
package/translations/ar.json
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"annotationRemoved": "تمت إزالة التعليق التوضيحي",
|
|
72
72
|
"annotationBodyUpdated": "تم تحديث التعليق التوضيحي",
|
|
73
73
|
"jobEvent": "حدث مهمة",
|
|
74
|
+
"embeddingComputed": "تم حساب التضمين",
|
|
74
75
|
"justNow": "الآن",
|
|
75
76
|
"minutesAgo": "منذ {{count}}د",
|
|
76
77
|
"hoursAgo": "منذ {{count}}س",
|
|
@@ -356,8 +357,5 @@
|
|
|
356
357
|
"semanticScoringHelp": "استخدام الذكاء الاصطناعي لتقييم النتائج حسب الصلة الدلالية",
|
|
357
358
|
"searching": "جاري البحث...",
|
|
358
359
|
"search": "بحث"
|
|
359
|
-
},
|
|
360
|
-
"History": {
|
|
361
|
-
"embeddingComputed": "تم حساب التضمين"
|
|
362
360
|
}
|
|
363
361
|
}
|