@semiont/react-ui 0.4.14 → 0.4.15
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 +18 -12
- package/dist/KnowledgeBaseSessionContext-CpYaCbnC.d.mts +174 -0
- package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs → PdfAnnotationCanvas.client-CHDCGQBR.mjs} +3 -3
- package/dist/{chunk-HNZOXH4L.mjs → chunk-OZICDVH7.mjs} +5 -3
- package/dist/chunk-OZICDVH7.mjs.map +1 -0
- package/dist/chunk-R2U7P4TK.mjs +865 -0
- package/dist/chunk-R2U7P4TK.mjs.map +1 -0
- package/dist/{chunk-BQJWOK4C.mjs → chunk-VN5NY4SN.mjs} +9 -8
- package/dist/chunk-VN5NY4SN.mjs.map +1 -0
- package/dist/index.d.mts +139 -169
- package/dist/index.mjs +2197 -1947
- package/dist/index.mjs.map +1 -1
- package/dist/test-utils.d.mts +13 -62
- package/dist/test-utils.mjs +40 -21
- package/dist/test-utils.mjs.map +1 -1
- package/package.json +5 -3
- package/src/components/ProtectedErrorBoundary.tsx +95 -0
- package/src/components/__tests__/ProtectedErrorBoundary.test.tsx +197 -0
- package/src/components/modals/PermissionDeniedModal.tsx +140 -0
- package/src/components/modals/ReferenceWizardModal.tsx +3 -2
- package/src/components/modals/SessionExpiredModal.tsx +101 -0
- package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +150 -0
- package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +115 -0
- package/src/components/resource/AnnotationHistory.tsx +5 -6
- package/src/components/resource/HistoryEvent.tsx +7 -7
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +33 -34
- package/src/components/resource/__tests__/HistoryEvent.test.tsx +17 -19
- package/src/components/resource/__tests__/event-formatting.test.ts +70 -94
- package/src/components/resource/event-formatting.ts +56 -56
- package/src/components/resource/panels/ReferenceEntry.tsx +7 -5
- package/src/components/resource/panels/ResourceInfoPanel.tsx +8 -6
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +12 -12
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +1 -0
- package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +4 -4
- package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +5 -10
- package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +23 -54
- package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +6 -6
- package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +7 -19
- package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +1 -1
- package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +18 -44
- package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +6 -6
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +24 -26
- package/dist/TranslationManager-CudgH3gw.d.mts +0 -107
- package/dist/chunk-BQJWOK4C.mjs.map +0 -1
- package/dist/chunk-HNZOXH4L.mjs.map +0 -1
- package/dist/chunk-OL5UST25.mjs +0 -413
- package/dist/chunk-OL5UST25.mjs.map +0 -1
- /package/dist/{PdfAnnotationCanvas.client-CW6SKH2U.mjs.map → PdfAnnotationCanvas.client-CHDCGQBR.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@semiont/react-ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.15",
|
|
4
4
|
"description": "React components and hooks for Semiont",
|
|
5
5
|
"main": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.mts",
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
"test:components:modals:keyboard": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/components/modals/__tests__/SearchModal.keyboard.test.tsx",
|
|
65
65
|
"test:components:modals:accessibility": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/components/modals/__tests__/SearchModal.accessibility.test.tsx",
|
|
66
66
|
"test:components:modals:visual": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/components/modals/__tests__/SearchModal.visual.test.tsx",
|
|
67
|
-
"test:components:modals": "
|
|
67
|
+
"test:components:modals:auth": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/components/modals/__tests__/SessionExpiredModal.test.tsx src/components/modals/__tests__/PermissionDeniedModal.test.tsx",
|
|
68
|
+
"test:components:modals": "npm run test:components:modals:basic && npm run test:components:modals:keyboard && npm run test:components:modals:accessibility && npm run test:components:modals:visual && npm run test:components:modals:auth",
|
|
68
69
|
"test:components:other": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/components/annotation src/components/Button src/components/resource/__tests__ src/components/__tests__",
|
|
69
70
|
"test:features": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/features",
|
|
70
71
|
"test:other": "NODE_OPTIONS=--max-old-space-size=8192 vitest run src/contexts src/hooks src/lib src/assets",
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
},
|
|
104
105
|
"dependencies": {
|
|
105
106
|
"@semiont/api-client": "*",
|
|
106
|
-
"@semiont/core": "*"
|
|
107
|
+
"@semiont/core": "*",
|
|
108
|
+
"react-error-boundary": "^4.1.2"
|
|
107
109
|
}
|
|
108
110
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ErrorBoundary, type FallbackProps } from 'react-error-boundary';
|
|
3
|
+
|
|
4
|
+
interface ProtectedErrorBoundaryProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
/**
|
|
7
|
+
* Values that, when any change, reset the boundary back to its non-error
|
|
8
|
+
* state. Apps typically pass `[location.pathname]` so navigating away from
|
|
9
|
+
* a crashed page automatically recovers.
|
|
10
|
+
*/
|
|
11
|
+
resetKeys?: unknown[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Error boundary for protected (authenticated) routes.
|
|
16
|
+
*
|
|
17
|
+
* Catches unexpected render-time crashes inside the protected tree and
|
|
18
|
+
* shows a generic "something went wrong" fallback with a refresh option.
|
|
19
|
+
*
|
|
20
|
+
* NOT auth-specific. Auth state changes (sign-in, sign-out, expiry) flow
|
|
21
|
+
* through the KnowledgeBaseSession context, not exceptions — so this
|
|
22
|
+
* boundary will never catch an "auth error" in normal operation. Its job
|
|
23
|
+
* is purely to keep a render bug from blanking the screen.
|
|
24
|
+
*
|
|
25
|
+
* The optional `resetKeys` prop lets callers wire automatic recovery on
|
|
26
|
+
* navigation (e.g. `resetKeys={[location.pathname]}`).
|
|
27
|
+
*/
|
|
28
|
+
export function ProtectedErrorBoundary({
|
|
29
|
+
children,
|
|
30
|
+
resetKeys,
|
|
31
|
+
}: ProtectedErrorBoundaryProps) {
|
|
32
|
+
return (
|
|
33
|
+
<ErrorBoundary
|
|
34
|
+
FallbackComponent={ProtectedErrorFallback}
|
|
35
|
+
onError={(error, info) => {
|
|
36
|
+
if (process.env.NODE_ENV === 'development') {
|
|
37
|
+
console.error('ProtectedErrorBoundary caught:', error, info);
|
|
38
|
+
}
|
|
39
|
+
}}
|
|
40
|
+
{...(resetKeys && { resetKeys })}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</ErrorBoundary>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ProtectedErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="min-h-[400px] flex items-center justify-center p-4">
|
|
50
|
+
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
|
51
|
+
<div className="flex items-center gap-3 mb-4">
|
|
52
|
+
<div className="flex-shrink-0 w-10 h-10 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center">
|
|
53
|
+
<svg className="w-6 h-6 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
55
|
+
</svg>
|
|
56
|
+
</div>
|
|
57
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
58
|
+
Something went wrong
|
|
59
|
+
</h2>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<p className="text-gray-600 dark:text-gray-300 mb-6">
|
|
63
|
+
An unexpected error occurred. Try again, or refresh the page.
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
{process.env.NODE_ENV === 'development' && (
|
|
67
|
+
<details className="mb-4">
|
|
68
|
+
<summary className="text-sm text-gray-500 cursor-pointer hover:text-gray-700 dark:hover:text-gray-300">
|
|
69
|
+
Error details (development only)
|
|
70
|
+
</summary>
|
|
71
|
+
<pre className="mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded-sm overflow-auto">
|
|
72
|
+
{error.message}
|
|
73
|
+
{error.stack}
|
|
74
|
+
</pre>
|
|
75
|
+
</details>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
<div className="flex gap-3">
|
|
79
|
+
<button
|
|
80
|
+
onClick={resetErrorBoundary}
|
|
81
|
+
className="flex-1 px-4 py-2 text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
|
82
|
+
>
|
|
83
|
+
Try Again
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => window.location.reload()}
|
|
87
|
+
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
88
|
+
>
|
|
89
|
+
Refresh Page
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProtectedErrorBoundary Tests
|
|
3
|
+
*
|
|
4
|
+
* The boundary catches render-time crashes inside the protected tree and
|
|
5
|
+
* shows a generic "something went wrong" fallback. It is NOT auth-specific.
|
|
6
|
+
* Auth state changes flow through context, not exceptions.
|
|
7
|
+
*
|
|
8
|
+
* Implementation: thin wrapper around `react-error-boundary`'s `ErrorBoundary`,
|
|
9
|
+
* with a custom fallback and an optional `resetKeys` prop for navigation-based
|
|
10
|
+
* auto-recovery.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
|
|
18
|
+
import { ProtectedErrorBoundary } from '../ProtectedErrorBoundary';
|
|
19
|
+
|
|
20
|
+
function ThrowOnRender({ message }: { message: string }): React.ReactElement {
|
|
21
|
+
throw new Error(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A child that throws on first render but stops throwing if `shouldThrow`
|
|
26
|
+
* becomes false. Used to test boundary reset behavior.
|
|
27
|
+
*/
|
|
28
|
+
function ThrowOnce({ shouldThrow }: { shouldThrow: boolean }): React.ReactElement {
|
|
29
|
+
if (shouldThrow) throw new Error('boom');
|
|
30
|
+
return <div data-testid="recovered">recovered</div>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('ProtectedErrorBoundary', () => {
|
|
34
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// React logs caught render errors to console.error in development;
|
|
38
|
+
// suppress to keep test output clean.
|
|
39
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
consoleErrorSpy.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('happy path', () => {
|
|
47
|
+
it('renders children unchanged when no error', () => {
|
|
48
|
+
render(
|
|
49
|
+
<ProtectedErrorBoundary>
|
|
50
|
+
<div data-testid="child">protected content</div>
|
|
51
|
+
</ProtectedErrorBoundary>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText('protected content')).toBeInTheDocument();
|
|
56
|
+
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('on render error', () => {
|
|
61
|
+
it('shows the generic fallback heading', () => {
|
|
62
|
+
render(
|
|
63
|
+
<ProtectedErrorBoundary>
|
|
64
|
+
<ThrowOnRender message="boom" />
|
|
65
|
+
</ProtectedErrorBoundary>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('does NOT show "Authentication Error" — the boundary is not auth-themed', () => {
|
|
72
|
+
// Throw an error whose message contains the word "session" — the old
|
|
73
|
+
// AuthErrorBoundary substring-matched on this and switched its UI to
|
|
74
|
+
// an auth-flavored fallback. The new boundary must not.
|
|
75
|
+
render(
|
|
76
|
+
<ProtectedErrorBoundary>
|
|
77
|
+
<ThrowOnRender message="session blew up" />
|
|
78
|
+
</ProtectedErrorBoundary>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(screen.queryByText(/Authentication Error/i)).not.toBeInTheDocument();
|
|
82
|
+
expect(screen.queryByRole('button', { name: /sign in/i })).not.toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('renders both a Try Again and a Refresh Page button', () => {
|
|
87
|
+
render(
|
|
88
|
+
<ProtectedErrorBoundary>
|
|
89
|
+
<ThrowOnRender message="boom" />
|
|
90
|
+
</ProtectedErrorBoundary>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(screen.getByRole('button', { name: /try again/i })).toBeInTheDocument();
|
|
94
|
+
expect(screen.getByRole('button', { name: /refresh page/i })).toBeInTheDocument();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('calls window.location.reload when the Refresh Page button is clicked', () => {
|
|
98
|
+
const originalLocation = window.location;
|
|
99
|
+
const reload = vi.fn();
|
|
100
|
+
Object.defineProperty(window, 'location', {
|
|
101
|
+
value: { ...originalLocation, reload },
|
|
102
|
+
writable: true,
|
|
103
|
+
configurable: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
render(
|
|
108
|
+
<ProtectedErrorBoundary>
|
|
109
|
+
<ThrowOnRender message="boom" />
|
|
110
|
+
</ProtectedErrorBoundary>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
fireEvent.click(screen.getByRole('button', { name: /refresh page/i }));
|
|
114
|
+
expect(reload).toHaveBeenCalled();
|
|
115
|
+
} finally {
|
|
116
|
+
Object.defineProperty(window, 'location', {
|
|
117
|
+
value: originalLocation,
|
|
118
|
+
writable: true,
|
|
119
|
+
configurable: true,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('Try Again resets the boundary so children re-render', () => {
|
|
125
|
+
// The child reads `shouldThrow` from a closed-over variable that we
|
|
126
|
+
// flip between the initial mount and the click. Using an external
|
|
127
|
+
// flag (rather than counting renders) sidesteps React 18's
|
|
128
|
+
// strict-mode double-invocation of component bodies.
|
|
129
|
+
let shouldThrow = true;
|
|
130
|
+
function MaybeThrow(): React.ReactElement {
|
|
131
|
+
if (shouldThrow) throw new Error('boom');
|
|
132
|
+
return <div data-testid="recovered">recovered</div>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
render(
|
|
136
|
+
<ProtectedErrorBoundary>
|
|
137
|
+
<MaybeThrow />
|
|
138
|
+
</ProtectedErrorBoundary>
|
|
139
|
+
);
|
|
140
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
141
|
+
|
|
142
|
+
// Stop throwing so the next render succeeds
|
|
143
|
+
shouldThrow = false;
|
|
144
|
+
fireEvent.click(screen.getByRole('button', { name: /try again/i }));
|
|
145
|
+
|
|
146
|
+
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
|
|
147
|
+
expect(screen.getByTestId('recovered')).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('resetKeys', () => {
|
|
152
|
+
it('auto-resets when a resetKey changes (simulating navigation)', () => {
|
|
153
|
+
function Harness({ pathname, shouldThrow }: { pathname: string; shouldThrow: boolean }) {
|
|
154
|
+
return (
|
|
155
|
+
<ProtectedErrorBoundary resetKeys={[pathname]}>
|
|
156
|
+
<ThrowOnce shouldThrow={shouldThrow} />
|
|
157
|
+
</ProtectedErrorBoundary>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { rerender } = render(<Harness pathname="/know" shouldThrow={true} />);
|
|
162
|
+
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
|
163
|
+
|
|
164
|
+
// Simulate navigation to a different path AND a render that no longer throws
|
|
165
|
+
rerender(<Harness pathname="/admin" shouldThrow={false} />);
|
|
166
|
+
|
|
167
|
+
// Boundary auto-recovers because the resetKey changed
|
|
168
|
+
expect(screen.queryByText('Something went wrong')).not.toBeInTheDocument();
|
|
169
|
+
expect(screen.getByTestId('recovered')).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('logs the error to console.error in development', () => {
|
|
173
|
+
// Vitest's NODE_ENV defaults to "test", but the boundary's
|
|
174
|
+
// componentDidCatch logs only when NODE_ENV === 'development'.
|
|
175
|
+
// Force it for this test.
|
|
176
|
+
const originalEnv = process.env.NODE_ENV;
|
|
177
|
+
process.env.NODE_ENV = 'development';
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
render(
|
|
181
|
+
<ProtectedErrorBoundary>
|
|
182
|
+
<ThrowOnRender message="instrumented boom" />
|
|
183
|
+
</ProtectedErrorBoundary>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Multiple console.error calls happen (React's own logs, plus ours).
|
|
187
|
+
// Look for the boundary's prefix specifically.
|
|
188
|
+
const ourCall = consoleErrorSpy.mock.calls.find(
|
|
189
|
+
call => typeof call[0] === 'string' && call[0].includes('ProtectedErrorBoundary caught:')
|
|
190
|
+
);
|
|
191
|
+
expect(ourCall).toBeDefined();
|
|
192
|
+
} finally {
|
|
193
|
+
process.env.NODE_ENV = originalEnv;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
+
import { useKnowledgeBaseSession } from '../../contexts/KnowledgeBaseSessionContext';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Modal that surfaces when a 403 forbidden error is reported via
|
|
8
|
+
* `notifyPermissionDenied` (called from QueryCache.onError).
|
|
9
|
+
*
|
|
10
|
+
* Reads `permissionDeniedAt` and `permissionDeniedMessage` from
|
|
11
|
+
* KnowledgeBaseSessionContext. The provider clears the flag when the user
|
|
12
|
+
* dismisses the modal.
|
|
13
|
+
*
|
|
14
|
+
* Must be mounted inside KnowledgeBaseSessionProvider.
|
|
15
|
+
*/
|
|
16
|
+
export function PermissionDeniedModal() {
|
|
17
|
+
const {
|
|
18
|
+
permissionDeniedAt,
|
|
19
|
+
permissionDeniedMessage,
|
|
20
|
+
acknowledgePermissionDenied,
|
|
21
|
+
} = useKnowledgeBaseSession();
|
|
22
|
+
const showModal = permissionDeniedAt !== null;
|
|
23
|
+
const message = permissionDeniedMessage ?? 'You do not have permission to perform this action.';
|
|
24
|
+
|
|
25
|
+
const handleGoBack = () => {
|
|
26
|
+
acknowledgePermissionDenied();
|
|
27
|
+
window.history.back();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const handleGoHome = () => {
|
|
31
|
+
acknowledgePermissionDenied();
|
|
32
|
+
window.location.href = '/';
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleSwitchAccount = () => {
|
|
36
|
+
acknowledgePermissionDenied();
|
|
37
|
+
window.location.href = `/auth/connect?callbackUrl=${encodeURIComponent(window.location.pathname)}`;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Transition appear show={showModal}>
|
|
42
|
+
<Dialog as="div" className="semiont-modal" onClose={handleGoBack}>
|
|
43
|
+
<TransitionChild
|
|
44
|
+
enter="ease-out duration-200"
|
|
45
|
+
enterFrom="opacity-0"
|
|
46
|
+
enterTo="opacity-100"
|
|
47
|
+
leave="ease-in duration-150"
|
|
48
|
+
leaveFrom="opacity-100"
|
|
49
|
+
leaveTo="opacity-0"
|
|
50
|
+
>
|
|
51
|
+
<div className="semiont-modal__backdrop" />
|
|
52
|
+
</TransitionChild>
|
|
53
|
+
|
|
54
|
+
<div className="semiont-modal__container">
|
|
55
|
+
<div className="semiont-modal__wrapper">
|
|
56
|
+
<TransitionChild
|
|
57
|
+
enter="ease-out duration-200"
|
|
58
|
+
enterFrom="opacity-0 scale-95"
|
|
59
|
+
enterTo="opacity-100 scale-100"
|
|
60
|
+
leave="ease-in duration-150"
|
|
61
|
+
leaveFrom="opacity-100 scale-100"
|
|
62
|
+
leaveTo="opacity-0 scale-95"
|
|
63
|
+
>
|
|
64
|
+
<DialogPanel className="semiont-modal__panel semiont-modal__panel--medium">
|
|
65
|
+
<div className="semiont-modal__icon-wrapper">
|
|
66
|
+
<div className="semiont-modal__icon">
|
|
67
|
+
<svg style={{ width: '1.5rem', height: '1.5rem', color: 'var(--semiont-color-amber-600, #d97706)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
68
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
69
|
+
</svg>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="semiont-modal__content">
|
|
74
|
+
<DialogTitle className="semiont-modal__title semiont-modal__title--centered">
|
|
75
|
+
Access Denied
|
|
76
|
+
</DialogTitle>
|
|
77
|
+
<p className="semiont-modal__description">
|
|
78
|
+
{message}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div style={{
|
|
83
|
+
padding: '0.75rem',
|
|
84
|
+
backgroundColor: 'var(--semiont-bg-secondary)',
|
|
85
|
+
borderRadius: 'var(--semiont-radius-md, 0.375rem)',
|
|
86
|
+
border: '1px solid var(--semiont-border-primary)',
|
|
87
|
+
fontSize: 'var(--semiont-text-sm, 0.875rem)',
|
|
88
|
+
marginBottom: '1rem',
|
|
89
|
+
}}>
|
|
90
|
+
<p style={{ color: 'var(--semiont-text-secondary)', marginBottom: '0.5rem' }}>
|
|
91
|
+
This could be because:
|
|
92
|
+
</p>
|
|
93
|
+
<ul style={{
|
|
94
|
+
listStyle: 'disc',
|
|
95
|
+
listStylePosition: 'inside',
|
|
96
|
+
color: 'var(--semiont-text-tertiary)',
|
|
97
|
+
display: 'flex',
|
|
98
|
+
flexDirection: 'column',
|
|
99
|
+
gap: '0.25rem',
|
|
100
|
+
}}>
|
|
101
|
+
<li>You don't have the required permissions</li>
|
|
102
|
+
<li>The resource is restricted to specific users or teams</li>
|
|
103
|
+
<li>Your account type doesn't include this feature</li>
|
|
104
|
+
</ul>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="semiont-modal__actions">
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={handleGoBack}
|
|
111
|
+
className="semiont-button--primary semiont-button--flex"
|
|
112
|
+
>
|
|
113
|
+
Go Back
|
|
114
|
+
</button>
|
|
115
|
+
<button
|
|
116
|
+
type="button"
|
|
117
|
+
onClick={handleGoHome}
|
|
118
|
+
className="semiont-button--secondary semiont-button--flex"
|
|
119
|
+
>
|
|
120
|
+
Go to Home
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="semiont-modal__actions" style={{ marginTop: '0.5rem', paddingTop: '0.5rem', borderTop: '1px solid var(--semiont-border-primary)' }}>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={handleSwitchAccount}
|
|
127
|
+
className="semiont-button--secondary semiont-button--flex"
|
|
128
|
+
style={{ fontSize: 'var(--semiont-text-sm, 0.875rem)' }}
|
|
129
|
+
>
|
|
130
|
+
Switch Account
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</DialogPanel>
|
|
134
|
+
</TransitionChild>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</Dialog>
|
|
138
|
+
</Transition>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -144,18 +144,19 @@ export function ReferenceWizardModal({
|
|
|
144
144
|
}, []);
|
|
145
145
|
|
|
146
146
|
const handleSearchSubmit = useCallback((config: SearchConfig) => {
|
|
147
|
-
if (!annotationId || !context) return;
|
|
147
|
+
if (!annotationId || !context || !resourceId) return;
|
|
148
148
|
setIsSearching(true);
|
|
149
149
|
const contextWithHint = userHint ? { ...context, userHint } : context;
|
|
150
150
|
eventBus.get('match:search-requested').next({
|
|
151
151
|
correlationId: crypto.randomUUID(),
|
|
152
|
+
resourceId,
|
|
152
153
|
referenceId: annotationId,
|
|
153
154
|
context: contextWithHint,
|
|
154
155
|
limit: config.limit,
|
|
155
156
|
useSemanticScoring: config.useSemanticScoring,
|
|
156
157
|
});
|
|
157
158
|
// Stay on configure-search until results arrive (subscription above handles transition)
|
|
158
|
-
}, [annotationId, context, eventBus, userHint]);
|
|
159
|
+
}, [annotationId, resourceId, context, eventBus, userHint]);
|
|
159
160
|
|
|
160
161
|
const handleGenerateSubmit = useCallback((config: GenerationConfig) => {
|
|
161
162
|
if (!annotationId) return;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
|
|
4
|
+
import { useKnowledgeBaseSession } from '../../contexts/KnowledgeBaseSessionContext';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Modal that surfaces when the active KB's session expires (a 401 from
|
|
8
|
+
* either the provider's own JWT validation or from any React Query call
|
|
9
|
+
* via the QueryCache.onError handler).
|
|
10
|
+
*
|
|
11
|
+
* Reads `sessionExpiredAt` from KnowledgeBaseSessionContext. When the user
|
|
12
|
+
* dismisses the modal, the provider clears the flag.
|
|
13
|
+
*
|
|
14
|
+
* Must be mounted inside KnowledgeBaseSessionProvider.
|
|
15
|
+
*/
|
|
16
|
+
export function SessionExpiredModal() {
|
|
17
|
+
const {
|
|
18
|
+
sessionExpiredAt,
|
|
19
|
+
sessionExpiredMessage,
|
|
20
|
+
acknowledgeSessionExpired,
|
|
21
|
+
} = useKnowledgeBaseSession();
|
|
22
|
+
const showModal = sessionExpiredAt !== null;
|
|
23
|
+
|
|
24
|
+
const handleSignIn = () => {
|
|
25
|
+
acknowledgeSessionExpired();
|
|
26
|
+
window.location.href = `/auth/connect?callbackUrl=${encodeURIComponent(window.location.pathname)}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleClose = () => {
|
|
30
|
+
acknowledgeSessionExpired();
|
|
31
|
+
window.location.href = '/';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Transition appear show={showModal}>
|
|
36
|
+
<Dialog as="div" className="semiont-modal" onClose={handleClose}>
|
|
37
|
+
<TransitionChild
|
|
38
|
+
enter="ease-out duration-200"
|
|
39
|
+
enterFrom="opacity-0"
|
|
40
|
+
enterTo="opacity-100"
|
|
41
|
+
leave="ease-in duration-150"
|
|
42
|
+
leaveFrom="opacity-100"
|
|
43
|
+
leaveTo="opacity-0"
|
|
44
|
+
>
|
|
45
|
+
<div className="semiont-modal__backdrop" />
|
|
46
|
+
</TransitionChild>
|
|
47
|
+
|
|
48
|
+
<div className="semiont-modal__container">
|
|
49
|
+
<div className="semiont-modal__wrapper">
|
|
50
|
+
<TransitionChild
|
|
51
|
+
enter="ease-out duration-200"
|
|
52
|
+
enterFrom="opacity-0 scale-95"
|
|
53
|
+
enterTo="opacity-100 scale-100"
|
|
54
|
+
leave="ease-in duration-150"
|
|
55
|
+
leaveFrom="opacity-100 scale-100"
|
|
56
|
+
leaveTo="opacity-0 scale-95"
|
|
57
|
+
>
|
|
58
|
+
<DialogPanel className="semiont-modal__panel semiont-modal__panel--medium">
|
|
59
|
+
<div className="semiont-modal__icon-wrapper">
|
|
60
|
+
<div className="semiont-modal__icon" style={{
|
|
61
|
+
background: 'linear-gradient(to bottom right, var(--semiont-color-red-100, #fee2e2), var(--semiont-color-red-300, #fca5a5))',
|
|
62
|
+
}}>
|
|
63
|
+
<svg style={{ width: '1.5rem', height: '1.5rem', color: 'var(--semiont-color-red-600, #dc2626)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
64
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
65
|
+
</svg>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="semiont-modal__content">
|
|
70
|
+
<DialogTitle className="semiont-modal__title semiont-modal__title--centered">
|
|
71
|
+
Session Expired
|
|
72
|
+
</DialogTitle>
|
|
73
|
+
<p className="semiont-modal__description">
|
|
74
|
+
{sessionExpiredMessage ?? 'Your session has expired for security reasons. Please sign in again to continue working.'}
|
|
75
|
+
</p>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="semiont-modal__actions">
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
onClick={handleClose}
|
|
82
|
+
className="semiont-button--secondary semiont-button--flex"
|
|
83
|
+
>
|
|
84
|
+
Go to Home
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={handleSignIn}
|
|
89
|
+
className="semiont-button--primary semiont-button--flex"
|
|
90
|
+
>
|
|
91
|
+
Sign In Again
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</DialogPanel>
|
|
95
|
+
</TransitionChild>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</Dialog>
|
|
99
|
+
</Transition>
|
|
100
|
+
);
|
|
101
|
+
}
|