@semiont/react-ui 0.4.20 → 0.4.22
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 +8 -5
- package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-5QESNO5H.mjs} +13 -16
- package/dist/PdfAnnotationCanvas.client-5QESNO5H.mjs.map +1 -0
- package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
- package/dist/chunk-4NOUO3W6.mjs +7788 -0
- package/dist/chunk-4NOUO3W6.mjs.map +1 -0
- package/dist/index.d.mts +212 -1206
- package/dist/index.mjs +3332 -13712
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +48 -21
- package/dist/test-utils.mjs +2505 -87
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +2 -2
- package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
- package/src/components/CodeMirrorRenderer.tsx +12 -12
- package/src/components/LiveRegion.tsx +1 -2
- package/src/components/StatusDisplay.tsx +42 -16
- package/src/components/Toolbar.tsx +4 -4
- package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
- package/src/components/__tests__/StatusDisplay.test.tsx +50 -65
- package/src/components/__tests__/Toolbar.test.tsx +4 -4
- package/src/components/annotation/AnnotateToolbar.tsx +8 -9
- package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
- package/src/components/annotation-popups/JsonLdView.tsx +1 -2
- package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +1 -2
- package/src/components/image-annotation/AnnotationOverlay.tsx +15 -18
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +12 -17
- package/src/components/modals/ConfigureGenerationStep.tsx +1 -2
- package/src/components/modals/PermissionDeniedModal.tsx +11 -11
- package/src/components/modals/ReferenceWizardModal.tsx +14 -18
- package/src/components/modals/ResourceSearchModal.tsx +12 -8
- package/src/components/modals/SearchModal.tsx +11 -6
- package/src/components/modals/SearchResultsStep.tsx +1 -3
- package/src/components/modals/SessionExpiredModal.tsx +11 -11
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
- package/src/components/modals/__tests__/SearchModal.accessibility.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.basic.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.keyboard.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
- package/src/components/modals/__tests__/SearchModal.visual.test.tsx +6 -2
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
- package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
- package/src/components/navigation/ObservableLink.tsx +6 -6
- package/src/components/navigation/SimpleNavigation.tsx +4 -4
- package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
- package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +15 -18
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +1 -2
- package/src/components/resource/AnnotateView.tsx +8 -10
- package/src/components/resource/AnnotationHistory.tsx +9 -12
- package/src/components/resource/BrowseView.tsx +11 -8
- package/src/components/resource/ResourceViewer.tsx +22 -34
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
- package/src/components/resource/__tests__/BrowseView.test.tsx +38 -87
- package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +41 -31
- package/src/components/resource/__tests__/event-formatting.test.ts +6 -2
- package/src/components/resource/event-formatting.ts +2 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +7 -8
- package/src/components/resource/panels/AssessmentPanel.tsx +21 -17
- package/src/components/resource/panels/AssistSection.tsx +15 -21
- package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
- package/src/components/resource/panels/CommentEntry.tsx +7 -8
- package/src/components/resource/panels/CommentsPanel.tsx +11 -13
- package/src/components/resource/panels/HighlightEntry.tsx +7 -8
- package/src/components/resource/panels/HighlightPanel.tsx +12 -13
- package/src/components/resource/panels/ReferenceEntry.tsx +13 -15
- package/src/components/resource/panels/ReferencesPanel.tsx +17 -19
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -7
- package/src/components/resource/panels/StatisticsPanel.tsx +2 -3
- package/src/components/resource/panels/TagEntry.tsx +7 -8
- package/src/components/resource/panels/TaggingPanel.tsx +14 -23
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +4 -3
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +22 -57
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +22 -61
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +7 -8
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +28 -53
- package/src/components/resource/panels/__tests__/StatisticsPanel.test.tsx +3 -3
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +4 -4
- package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +19 -52
- package/src/components/settings/SettingsPanel.tsx +9 -9
- package/src/components/settings/__tests__/SettingsPanel.test.tsx +15 -15
- package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -2
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
- package/src/features/admin-exchange/components/ImportCard.tsx +2 -7
- package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -2
- package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
- package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -2
- package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -2
- package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
- package/src/features/resource-compose/components/ResourceComposePage.tsx +6 -22
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
- package/src/features/resource-discovery/components/ResourceCard.tsx +1 -2
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +3 -4
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +37 -45
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +129 -197
- package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
- package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
- package/dist/chunk-OZICDVH7.mjs +0 -62
- package/dist/chunk-OZICDVH7.mjs.map +0 -1
- package/dist/chunk-R4CCMFJH.mjs +0 -877
- package/dist/chunk-R4CCMFJH.mjs.map +0 -1
- package/dist/chunk-VN5NY4SN.mjs +0 -200
- package/dist/chunk-VN5NY4SN.mjs.map +0 -1
- package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
- package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
- package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
5
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Modal that surfaces when a 403 forbidden error is reported via
|
|
8
9
|
* `notifyPermissionDenied` (called from QueryCache.onError).
|
|
9
10
|
*
|
|
10
|
-
* Reads `permissionDeniedAt
|
|
11
|
-
*
|
|
12
|
-
* dismisses the modal.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Reads `permissionDeniedAt$` and `permissionDeniedMessage$` from the
|
|
12
|
+
* active `FrontendSessionSignals`. The signals instance clears the
|
|
13
|
+
* flag when the user dismisses the modal. Modal state lives on
|
|
14
|
+
* signals (not the session itself) so headless sessions
|
|
15
|
+
* (workers/CLIs) don't carry dead observables.
|
|
15
16
|
*/
|
|
16
17
|
export function PermissionDeniedModal() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} = useKnowledgeBaseSession();
|
|
18
|
+
const signals = useObservable(useSemiont().activeSignals$);
|
|
19
|
+
const permissionDeniedAt = useObservable(signals?.permissionDeniedAt$) ?? null;
|
|
20
|
+
const permissionDeniedMessage = useObservable(signals?.permissionDeniedMessage$) ?? null;
|
|
21
|
+
const acknowledgePermissionDenied = () => signals?.acknowledgePermissionDenied();
|
|
22
22
|
const showModal = permissionDeniedAt !== null;
|
|
23
23
|
const message = permissionDeniedMessage ?? 'You do not have permission to perform this action.';
|
|
24
24
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
|
-
import type { GatheredContext
|
|
5
|
+
import type { GatheredContext } from '@semiont/core';
|
|
6
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
7
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
8
|
+
import { useEventSubscription } from '../../contexts/useEventSubscription';
|
|
6
9
|
import { GatherContextStep } from './GatherContextStep';
|
|
7
10
|
import { ConfigureGenerationStep } from './ConfigureGenerationStep';
|
|
8
11
|
import type { GenerationConfig } from './ConfigureGenerationStep';
|
|
@@ -34,8 +37,6 @@ export interface ReferenceWizardModalProps {
|
|
|
34
37
|
context: GatheredContext | null;
|
|
35
38
|
contextLoading: boolean;
|
|
36
39
|
contextError: Error | null;
|
|
37
|
-
/** Event bus for emitting downstream events */
|
|
38
|
-
eventBus: EventBus;
|
|
39
40
|
/** Callbacks */
|
|
40
41
|
onGenerateSubmit: (referenceId: string, config: GenerationConfig) => void;
|
|
41
42
|
onLinkResource: (referenceId: string, targetResourceId: string) => void;
|
|
@@ -91,12 +92,12 @@ export function ReferenceWizardModal({
|
|
|
91
92
|
context,
|
|
92
93
|
contextLoading,
|
|
93
94
|
contextError,
|
|
94
|
-
eventBus,
|
|
95
95
|
onGenerateSubmit,
|
|
96
96
|
onLinkResource,
|
|
97
97
|
onComposeNavigate,
|
|
98
98
|
translations: t,
|
|
99
99
|
}: ReferenceWizardModalProps) {
|
|
100
|
+
const session = useObservable(useSemiont().activeSession$);
|
|
100
101
|
const [wizardStep, setWizardStep] = useState<WizardStep>({ step: 'gather' });
|
|
101
102
|
const [isSearching, setIsSearching] = useState(false);
|
|
102
103
|
const [userHint, setUserHint] = useState('');
|
|
@@ -110,19 +111,14 @@ export function ReferenceWizardModal({
|
|
|
110
111
|
}
|
|
111
112
|
}, [isOpen]);
|
|
112
113
|
|
|
113
|
-
// Subscribe to search results
|
|
114
|
-
|
|
114
|
+
// Subscribe to search results (only react while open and for the current annotation)
|
|
115
|
+
useEventSubscription('match:search-results', (event) => {
|
|
115
116
|
if (!isOpen) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return () => subscription.unsubscribe();
|
|
125
|
-
}, [isOpen, eventBus, annotationId]);
|
|
117
|
+
if (annotationId && event.referenceId === annotationId) {
|
|
118
|
+
setIsSearching(false);
|
|
119
|
+
setWizardStep({ step: 'search-results', results: event.response as ScoredResult[] });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
126
122
|
|
|
127
123
|
const handleBind = useCallback(() => {
|
|
128
124
|
setWizardStep({ step: 'configure-search' });
|
|
@@ -147,7 +143,7 @@ export function ReferenceWizardModal({
|
|
|
147
143
|
if (!annotationId || !context || !resourceId) return;
|
|
148
144
|
setIsSearching(true);
|
|
149
145
|
const contextWithHint = userHint ? { ...context, userHint } : context;
|
|
150
|
-
|
|
146
|
+
session?.client.match.requestSearch({
|
|
151
147
|
correlationId: crypto.randomUUID(),
|
|
152
148
|
resourceId,
|
|
153
149
|
referenceId: annotationId,
|
|
@@ -156,7 +152,7 @@ export function ReferenceWizardModal({
|
|
|
156
152
|
useSemanticScoring: config.useSemanticScoring,
|
|
157
153
|
});
|
|
158
154
|
// Stay on configure-search until results arrive (subscription above handles transition)
|
|
159
|
-
}, [annotationId, resourceId, context,
|
|
155
|
+
}, [annotationId, resourceId, context, session, userHint]);
|
|
160
156
|
|
|
161
157
|
const handleGenerateSubmit = useCallback((config: GenerationConfig) => {
|
|
162
158
|
if (!annotationId) return;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
5
5
|
import { map } from 'rxjs/operators';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { getResourceId, getPrimaryRepresentation } from '@semiont/core';
|
|
7
|
+
import { createSearchPipeline } from '@semiont/sdk';
|
|
8
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
9
9
|
import { useObservable } from '../../hooks/useObservable';
|
|
10
10
|
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
11
|
-
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
12
11
|
|
|
13
|
-
type ResourceDescriptor
|
|
12
|
+
import type { ResourceDescriptor } from '@semiont/core';
|
|
14
13
|
|
|
15
14
|
type SearchResult = {
|
|
16
15
|
id: string;
|
|
@@ -56,7 +55,12 @@ export function ResourceSearchModal({
|
|
|
56
55
|
translations = {}
|
|
57
56
|
}: ResourceSearchModalProps) {
|
|
58
57
|
const { announceSearchResults, announceSearching, announceNavigation } = useSearchAnnouncements();
|
|
59
|
-
const semiont =
|
|
58
|
+
const semiont = useObservable(useSemiont().activeSession$)?.client;
|
|
59
|
+
// Pipeline factory captures `semiont` once via useState; if semiont is
|
|
60
|
+
// still loading at first render the captured value would be undefined.
|
|
61
|
+
// Route through a ref so the fetch closure reads the latest client.
|
|
62
|
+
const semiontRef = useRef(semiont);
|
|
63
|
+
semiontRef.current = semiont;
|
|
60
64
|
|
|
61
65
|
const t = {
|
|
62
66
|
title: translations.title || 'Search Resources',
|
|
@@ -70,7 +74,7 @@ export function ResourceSearchModal({
|
|
|
70
74
|
const [pipeline] = useState(() =>
|
|
71
75
|
createSearchPipeline<SearchResult>(
|
|
72
76
|
(q) =>
|
|
73
|
-
|
|
77
|
+
semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
74
78
|
map((resources) => {
|
|
75
79
|
if (resources === undefined) return undefined;
|
|
76
80
|
return resources
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
|
|
5
5
|
import { map } from 'rxjs/operators';
|
|
6
|
-
import { getResourceId } from '@semiont/
|
|
6
|
+
import { getResourceId } from '@semiont/core';
|
|
7
|
+
import { createSearchPipeline } from '@semiont/sdk';
|
|
7
8
|
import { useSearchAnnouncements } from '../../hooks/useSearchAnnouncements';
|
|
8
|
-
import {
|
|
9
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
9
10
|
import { useObservable } from '../../hooks/useObservable';
|
|
10
|
-
import { createSearchPipeline } from '../../lib/search-pipeline';
|
|
11
11
|
import './SearchModal.css';
|
|
12
12
|
|
|
13
13
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
@@ -45,7 +45,12 @@ export function SearchModal({
|
|
|
45
45
|
translations = {}
|
|
46
46
|
}: SearchModalProps) {
|
|
47
47
|
const { announceSearchResults, announceSearching } = useSearchAnnouncements();
|
|
48
|
-
const semiont =
|
|
48
|
+
const semiont = useObservable(useSemiont().activeSession$)?.client;
|
|
49
|
+
// Pipeline factory captures `semiont` once via useState; if semiont is
|
|
50
|
+
// still loading at first render the captured value would be undefined.
|
|
51
|
+
// Route through a ref so the fetch closure reads the latest client.
|
|
52
|
+
const semiontRef = useRef(semiont);
|
|
53
|
+
semiontRef.current = semiont;
|
|
49
54
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
50
55
|
|
|
51
56
|
const t = {
|
|
@@ -66,7 +71,7 @@ export function SearchModal({
|
|
|
66
71
|
const [pipeline] = useState(() =>
|
|
67
72
|
createSearchPipeline<SearchResult>(
|
|
68
73
|
(q) =>
|
|
69
|
-
|
|
74
|
+
semiontRef.current!.browse.resources({ search: q, limit: SEARCH_LIMIT }).pipe(
|
|
70
75
|
map((resources) => {
|
|
71
76
|
if (resources === undefined) return undefined;
|
|
72
77
|
return resources
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { GatheredContext, ResourceDescriptor } from '@semiont/core';
|
|
4
4
|
import { ContextSummary } from './ContextSummary';
|
|
5
5
|
import type { ContextSummaryTranslations } from './ContextSummary';
|
|
6
6
|
|
|
7
|
-
type ResourceDescriptor = components['schemas']['ResourceDescriptor'];
|
|
8
|
-
|
|
9
7
|
export type ScoredResult = ResourceDescriptor & {
|
|
10
8
|
score?: number;
|
|
11
9
|
matchReason?: string;
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
5
|
+
import { useObservable } from '../../hooks/useObservable';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Modal that surfaces when the active KB's session expires (a 401 from
|
|
8
|
-
* either the
|
|
9
|
+
* either the session's own JWT validation or from any React Query call
|
|
9
10
|
* via the QueryCache.onError handler).
|
|
10
11
|
*
|
|
11
|
-
* Reads `sessionExpiredAt
|
|
12
|
-
* dismisses the modal, the
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Reads `sessionExpiredAt$` from the active `FrontendSessionSignals`.
|
|
13
|
+
* When the user dismisses the modal, the signals instance clears the
|
|
14
|
+
* flag. Modal state lives on signals (not the session itself) so
|
|
15
|
+
* headless sessions (workers/CLIs) don't carry dead observables.
|
|
15
16
|
*/
|
|
16
17
|
export function SessionExpiredModal() {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} = useKnowledgeBaseSession();
|
|
18
|
+
const signals = useObservable(useSemiont().activeSignals$);
|
|
19
|
+
const sessionExpiredAt = useObservable(signals?.sessionExpiredAt$) ?? null;
|
|
20
|
+
const sessionExpiredMessage = useObservable(signals?.sessionExpiredMessage$) ?? null;
|
|
21
|
+
const acknowledgeSessionExpired = () => signals?.acknowledgeSessionExpired();
|
|
22
22
|
const showModal = sessionExpiredAt !== null;
|
|
23
23
|
|
|
24
24
|
const handleSignIn = () => {
|
|
@@ -53,7 +53,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
53
53
|
describe('initial render', () => {
|
|
54
54
|
it('does not render modal content when permissionDeniedAt is null', () => {
|
|
55
55
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
56
|
-
|
|
56
|
+
browser: createMockKnowledgeBaseSession({
|
|
57
57
|
permissionDeniedAt: null,
|
|
58
58
|
}),
|
|
59
59
|
});
|
|
@@ -64,7 +64,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
64
64
|
describe('when permissionDeniedAt is set', () => {
|
|
65
65
|
it('shows modal with default message when no message provided', () => {
|
|
66
66
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
67
|
-
|
|
67
|
+
browser: createMockKnowledgeBaseSession({
|
|
68
68
|
permissionDeniedAt: Date.now(),
|
|
69
69
|
}),
|
|
70
70
|
});
|
|
@@ -75,7 +75,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
75
75
|
|
|
76
76
|
it('shows custom message from permissionDeniedMessage', () => {
|
|
77
77
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
78
|
-
|
|
78
|
+
browser: createMockKnowledgeBaseSession({
|
|
79
79
|
permissionDeniedAt: Date.now(),
|
|
80
80
|
permissionDeniedMessage: 'Admin access required for this resource',
|
|
81
81
|
}),
|
|
@@ -86,7 +86,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
86
86
|
|
|
87
87
|
it('renders all three action buttons', () => {
|
|
88
88
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
89
|
-
|
|
89
|
+
browser: createMockKnowledgeBaseSession({
|
|
90
90
|
permissionDeniedAt: Date.now(),
|
|
91
91
|
}),
|
|
92
92
|
});
|
|
@@ -101,7 +101,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
101
101
|
it('acknowledges and calls window.history.back on Go Back', () => {
|
|
102
102
|
const ack = vi.fn();
|
|
103
103
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
104
|
-
|
|
104
|
+
browser: createMockKnowledgeBaseSession({
|
|
105
105
|
permissionDeniedAt: Date.now(),
|
|
106
106
|
permissionDeniedMessage: 'denied',
|
|
107
107
|
acknowledgePermissionDenied: ack,
|
|
@@ -117,7 +117,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
117
117
|
it('acknowledges and navigates to / on Go to Home', () => {
|
|
118
118
|
const ack = vi.fn();
|
|
119
119
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
120
|
-
|
|
120
|
+
browser: createMockKnowledgeBaseSession({
|
|
121
121
|
permissionDeniedAt: Date.now(),
|
|
122
122
|
permissionDeniedMessage: 'denied',
|
|
123
123
|
acknowledgePermissionDenied: ack,
|
|
@@ -134,7 +134,7 @@ describe('PermissionDeniedModal', () => {
|
|
|
134
134
|
const ack = vi.fn();
|
|
135
135
|
mockLocation.pathname = '/admin/users';
|
|
136
136
|
renderWithProviders(<PermissionDeniedModal />, {
|
|
137
|
-
|
|
137
|
+
browser: createMockKnowledgeBaseSession({
|
|
138
138
|
permissionDeniedAt: Date.now(),
|
|
139
139
|
permissionDeniedMessage: 'denied',
|
|
140
140
|
acknowledgePermissionDenied: ack,
|
|
@@ -16,21 +16,23 @@ vi.mock('@headlessui/react', () => ({
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
// Mock the api-client Observable surface.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// value on each keystroke.
|
|
19
|
+
// The session-based useSemiont path: useObservable(useSemiont().activeSession$)?.client
|
|
20
|
+
// We mock useSemiont to return a stable browser whose activeSession$ emits a
|
|
21
|
+
// session-shaped object that carries the mock client.
|
|
23
22
|
const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
24
23
|
const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
|
|
25
24
|
const stableMockClient = { browse: { resources: browseResourcesMock } };
|
|
25
|
+
const stableMockSession = { client: stableMockClient };
|
|
26
|
+
const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
|
|
27
|
+
const stableMockBrowser = { activeSession$: stableActiveSession$ };
|
|
26
28
|
|
|
27
|
-
vi.mock('../../../
|
|
28
|
-
const actual = await vi.importActual<typeof import('../../../
|
|
29
|
-
'../../../
|
|
29
|
+
vi.mock('../../../session/SemiontProvider', async () => {
|
|
30
|
+
const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>(
|
|
31
|
+
'../../../session/SemiontProvider'
|
|
30
32
|
);
|
|
31
33
|
return {
|
|
32
34
|
...actual,
|
|
33
|
-
|
|
35
|
+
useSemiont: () => stableMockBrowser,
|
|
34
36
|
};
|
|
35
37
|
});
|
|
36
38
|
|
|
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
// Mock getResourceId
|
|
18
|
-
vi.mock('@semiont/
|
|
18
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
19
22
|
getResourceId: vi.fn((resource: any) => resource?.id)
|
|
20
|
-
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
describe.skip('SearchModal Component - Accessibility', () => {
|
|
23
27
|
// TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
|
|
@@ -16,9 +16,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
// Mock getResourceId
|
|
19
|
-
vi.mock('@semiont/
|
|
19
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
21
|
+
return {
|
|
22
|
+
...actual,
|
|
20
23
|
getResourceId: vi.fn((resource: any) => resource?.id)
|
|
21
|
-
}
|
|
24
|
+
};
|
|
25
|
+
});
|
|
22
26
|
|
|
23
27
|
describe('SearchModal Component - Basic Rendering', () => {
|
|
24
28
|
const defaultProps = {
|
|
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
// Mock getResourceId
|
|
18
|
-
vi.mock('@semiont/
|
|
18
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
19
22
|
getResourceId: vi.fn((resource: any) => resource?.id)
|
|
20
|
-
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
describe.skip('SearchModal Component - Keyboard Navigation', () => {
|
|
23
27
|
// TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
|
|
@@ -36,19 +36,22 @@ vi.mock('@headlessui/react', () => ({
|
|
|
36
36
|
const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
37
37
|
const browseResourcesMock = vi.fn(() => browseResourcesSubject.asObservable());
|
|
38
38
|
|
|
39
|
-
// Stable client reference —
|
|
39
|
+
// Stable client reference — useSemiont is called on every render, so a
|
|
40
40
|
// fresh object literal would invalidate useMemo deps and restart the RxJS
|
|
41
|
-
// pipeline on every keystroke. The real
|
|
42
|
-
//
|
|
41
|
+
// pipeline on every keystroke. The real SemiontBrowser holds a single
|
|
42
|
+
// activeSession$ BehaviorSubject; the mock must do the same.
|
|
43
43
|
const stableMockClient = { browse: { resources: browseResourcesMock } };
|
|
44
|
+
const stableMockSession = { client: stableMockClient };
|
|
45
|
+
const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
|
|
46
|
+
const stableMockBrowser = { activeSession$: stableActiveSession$ };
|
|
44
47
|
|
|
45
|
-
vi.mock('../../../
|
|
46
|
-
const actual = await vi.importActual<typeof import('../../../
|
|
47
|
-
'../../../
|
|
48
|
+
vi.mock('../../../session/SemiontProvider', async () => {
|
|
49
|
+
const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>(
|
|
50
|
+
'../../../session/SemiontProvider'
|
|
48
51
|
);
|
|
49
52
|
return {
|
|
50
53
|
...actual,
|
|
51
|
-
|
|
54
|
+
useSemiont: () => stableMockBrowser,
|
|
52
55
|
};
|
|
53
56
|
});
|
|
54
57
|
|
|
@@ -15,9 +15,13 @@ vi.mock('../../../hooks/useSearchAnnouncements', () => ({
|
|
|
15
15
|
}));
|
|
16
16
|
|
|
17
17
|
// Mock getResourceId
|
|
18
|
-
vi.mock('@semiont/
|
|
18
|
+
vi.mock('@semiont/core', async (importOriginal) => {
|
|
19
|
+
const actual = await importOriginal<typeof import('@semiont/core')>();
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
19
22
|
getResourceId: vi.fn((resource: any) => resource?.id)
|
|
20
|
-
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
21
25
|
|
|
22
26
|
describe.skip('SearchModal Component - Visual States', () => {
|
|
23
27
|
// TODO: All SearchModal tests skipped due to HeadlessUI Dialog + jsdom memory issues
|
|
@@ -48,7 +48,7 @@ describe('SessionExpiredModal', () => {
|
|
|
48
48
|
describe('initial render', () => {
|
|
49
49
|
it('does not render modal content when sessionExpiredAt is null', () => {
|
|
50
50
|
renderWithProviders(<SessionExpiredModal />, {
|
|
51
|
-
|
|
51
|
+
browser: createMockKnowledgeBaseSession({
|
|
52
52
|
sessionExpiredAt: null,
|
|
53
53
|
}),
|
|
54
54
|
});
|
|
@@ -59,7 +59,7 @@ describe('SessionExpiredModal', () => {
|
|
|
59
59
|
describe('when sessionExpiredAt is set', () => {
|
|
60
60
|
it('renders the modal with default message', () => {
|
|
61
61
|
renderWithProviders(<SessionExpiredModal />, {
|
|
62
|
-
|
|
62
|
+
browser: createMockKnowledgeBaseSession({
|
|
63
63
|
sessionExpiredAt: Date.now(),
|
|
64
64
|
}),
|
|
65
65
|
});
|
|
@@ -71,7 +71,7 @@ describe('SessionExpiredModal', () => {
|
|
|
71
71
|
|
|
72
72
|
it('renders the custom message from sessionExpiredMessage', () => {
|
|
73
73
|
renderWithProviders(<SessionExpiredModal />, {
|
|
74
|
-
|
|
74
|
+
browser: createMockKnowledgeBaseSession({
|
|
75
75
|
sessionExpiredAt: Date.now(),
|
|
76
76
|
sessionExpiredMessage: 'Your token expired at 5pm',
|
|
77
77
|
}),
|
|
@@ -85,7 +85,7 @@ describe('SessionExpiredModal', () => {
|
|
|
85
85
|
const ack = vi.fn();
|
|
86
86
|
mockLocation.pathname = '/know/discover';
|
|
87
87
|
renderWithProviders(<SessionExpiredModal />, {
|
|
88
|
-
|
|
88
|
+
browser: createMockKnowledgeBaseSession({
|
|
89
89
|
sessionExpiredAt: Date.now(),
|
|
90
90
|
acknowledgeSessionExpired: ack,
|
|
91
91
|
}),
|
|
@@ -100,7 +100,7 @@ describe('SessionExpiredModal', () => {
|
|
|
100
100
|
it('calls acknowledgeSessionExpired and navigates to / on Go to Home', () => {
|
|
101
101
|
const ack = vi.fn();
|
|
102
102
|
renderWithProviders(<SessionExpiredModal />, {
|
|
103
|
-
|
|
103
|
+
browser: createMockKnowledgeBaseSession({
|
|
104
104
|
sessionExpiredAt: Date.now(),
|
|
105
105
|
acknowledgeSessionExpired: ack,
|
|
106
106
|
}),
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
import { SortableResourceTab } from './SortableResourceTab';
|
|
20
20
|
import { useDragAnnouncements } from '../../hooks/useDragAnnouncements';
|
|
21
21
|
import { useTranslations } from '../../contexts/TranslationContext';
|
|
22
|
-
import {
|
|
22
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
23
23
|
import type { CollapsibleResourceNavigationProps } from '../../types/collapsible-navigation';
|
|
24
24
|
import './CollapsibleResourceNavigation.css';
|
|
25
25
|
|
|
@@ -28,9 +28,9 @@ import './CollapsibleResourceNavigation.css';
|
|
|
28
28
|
* Supports drag and drop for resource reordering when expanded.
|
|
29
29
|
* Platform-agnostic design for use across different React environments.
|
|
30
30
|
*
|
|
31
|
-
* @emits
|
|
32
|
-
* @emits
|
|
33
|
-
* @emits
|
|
31
|
+
* @emits tabs:reorder - Resource tab reordered. Payload: { oldIndex: number, newIndex: number }
|
|
32
|
+
* @emits tabs:close - Resource tab closed. Payload: { resourceId: string }
|
|
33
|
+
* @emits shell:sidebar-toggle - Toggle sidebar collapsed/expanded state. Payload: undefined
|
|
34
34
|
*/
|
|
35
35
|
export function CollapsibleResourceNavigation({
|
|
36
36
|
fixedItems,
|
|
@@ -53,7 +53,7 @@ export function CollapsibleResourceNavigation({
|
|
|
53
53
|
|
|
54
54
|
const { announcePickup, announceDrop, announceKeyboardReorder, announceCannotMove } = useDragAnnouncements();
|
|
55
55
|
const t = useTranslations('CollapsibleResourceNavigation');
|
|
56
|
-
const
|
|
56
|
+
const semiont = useSemiont();
|
|
57
57
|
|
|
58
58
|
// Use translations from context, with fallback to props for backward compatibility
|
|
59
59
|
const mergedTranslations = {
|
|
@@ -110,12 +110,12 @@ export function CollapsibleResourceNavigation({
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// Emit event
|
|
113
|
-
|
|
113
|
+
semiont.emit('tabs:reorder', { oldIndex: currentIndex, newIndex });
|
|
114
114
|
|
|
115
115
|
// Announce the change
|
|
116
116
|
const resource = resources[currentIndex];
|
|
117
117
|
announceKeyboardReorder(resource.name, direction, newIndex + 1, resources.length);
|
|
118
|
-
}, [resources]);
|
|
118
|
+
}, [resources, semiont]);
|
|
119
119
|
|
|
120
120
|
// Handle resource close
|
|
121
121
|
const handleResourceClose = (resourceId: string, e: React.MouseEvent) => {
|
|
@@ -123,7 +123,7 @@ export function CollapsibleResourceNavigation({
|
|
|
123
123
|
e.stopPropagation();
|
|
124
124
|
|
|
125
125
|
// Emit event
|
|
126
|
-
|
|
126
|
+
semiont.emit('tabs:close', { resourceId });
|
|
127
127
|
|
|
128
128
|
// If we're closing the currently viewed resource, navigate to first fixed item or trigger callback
|
|
129
129
|
const resourceHref = getResourceHref(resourceId);
|
|
@@ -151,7 +151,7 @@ export function CollapsibleResourceNavigation({
|
|
|
151
151
|
const newIndex = resources.findIndex((resource) => resource.id === over.id);
|
|
152
152
|
if (oldIndex !== -1 && newIndex !== -1) {
|
|
153
153
|
// Emit event
|
|
154
|
-
|
|
154
|
+
semiont.emit('tabs:reorder', { oldIndex, newIndex });
|
|
155
155
|
const resource = resources[oldIndex];
|
|
156
156
|
announceDrop(resource.name, newIndex + 1, resources.length);
|
|
157
157
|
}
|
|
@@ -189,7 +189,7 @@ export function CollapsibleResourceNavigation({
|
|
|
189
189
|
<span className="semiont-nav-section__header-text">{mergedTranslations.title}</span>
|
|
190
190
|
)}
|
|
191
191
|
<button
|
|
192
|
-
onClick={() =>
|
|
192
|
+
onClick={() => semiont.emit('shell:sidebar-toggle', undefined)}
|
|
193
193
|
className="semiont-nav-section__header-icon"
|
|
194
194
|
title={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
|
|
195
195
|
aria-label={isCollapsed ? mergedTranslations.expandSidebar : mergedTranslations.collapseSidebar}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useRef, useEffect } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useSemiont } from '../../session/SemiontProvider';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Props for ObservableLink component
|
|
@@ -27,7 +27,7 @@ export interface ObservableLinkProps extends React.AnchorHTMLAttributes<HTMLAnch
|
|
|
27
27
|
* - State coordination before navigation
|
|
28
28
|
* - Logging navigation flows
|
|
29
29
|
*
|
|
30
|
-
* The component emits '
|
|
30
|
+
* The component emits 'nav:link-clicked' event before allowing
|
|
31
31
|
* the browser to follow the link.
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
@@ -51,7 +51,7 @@ export interface ObservableLinkProps extends React.AnchorHTMLAttributes<HTMLAnch
|
|
|
51
51
|
* </Link>
|
|
52
52
|
* ```
|
|
53
53
|
*
|
|
54
|
-
* @emits
|
|
54
|
+
* @emits nav:link-clicked - Link clicked by user. Payload: { href: string, label?: string }
|
|
55
55
|
*/
|
|
56
56
|
export function ObservableLink({
|
|
57
57
|
href,
|
|
@@ -60,7 +60,7 @@ export function ObservableLink({
|
|
|
60
60
|
children,
|
|
61
61
|
...anchorProps
|
|
62
62
|
}: ObservableLinkProps) {
|
|
63
|
-
const
|
|
63
|
+
const semiont = useSemiont();
|
|
64
64
|
|
|
65
65
|
// Store callback in ref to avoid including in dependency arrays
|
|
66
66
|
const onClickRef = useRef(onClick);
|
|
@@ -70,14 +70,14 @@ export function ObservableLink({
|
|
|
70
70
|
|
|
71
71
|
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
72
72
|
// Emit event for observability
|
|
73
|
-
|
|
73
|
+
semiont.emit('nav:link-clicked', {
|
|
74
74
|
href,
|
|
75
75
|
label
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// Call original onClick if provided
|
|
79
79
|
onClickRef.current?.(e);
|
|
80
|
-
}, [href, label]);
|
|
80
|
+
}, [href, label, semiont]);
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<a
|