@elevasis/ui 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/hooks/useApiClient.d.ts +54 -0
- package/dist/api/hooks/useApiClient.d.ts.map +1 -0
- package/dist/api/hooks/useApiClient.js +185 -0
- package/dist/api/index.d.ts +49 -11
- package/dist/api/index.js +3 -2
- package/dist/auth/index.d.ts +88 -2
- package/dist/auth/index.js +6 -2
- package/dist/{chunk-5UWFGBFM.js → chunk-4KAG5U7A.js} +18 -4
- package/dist/chunk-4VGWQ5AN.js +91 -0
- package/dist/{chunk-JKERRYVS.js → chunk-BLO4SISK.js} +7 -3
- package/dist/chunk-BWCC6ZJC.js +202 -0
- package/dist/{chunk-WNWKOCGJ.js → chunk-BZTA7IIL.js} +1 -1
- package/dist/chunk-DD3CCMCZ.js +15 -0
- package/dist/{chunk-GEFB5YIR.js → chunk-EZMRFWZQ.js} +1 -1
- package/dist/chunk-FDCVFCOQ.js +105 -0
- package/dist/chunk-FLJXZ7YC.js +150 -0
- package/dist/{chunk-7AI5ZYJ4.js → chunk-JVAZHVNV.js} +2 -94
- package/dist/{chunk-ZGHDPDTF.js → chunk-JYSYHVLU.js} +3 -3
- package/dist/{chunk-J3FALDQE.js → chunk-NXHL23JW.js} +7 -13
- package/dist/{chunk-OUHGHTE7.js → chunk-O3PY6B6E.js} +3 -2
- package/dist/chunk-OLD3NQLI.js +91 -0
- package/dist/{chunk-XVPEDNIM.js → chunk-PCBXNHKY.js} +325 -152
- package/dist/chunk-QQOLC46E.js +75 -0
- package/dist/{chunk-KSG4C5DD.js → chunk-QSVZP2NU.js} +2 -1
- package/dist/chunk-RNP5R5I3.js +1 -0
- package/dist/{chunk-YULUKCS6.js → chunk-SITSZUFW.js} +1 -1
- package/dist/chunk-TIRMFDM4.js +33 -0
- package/dist/{chunk-PYL4XW6H.js → chunk-TMFCNFLW.js} +1 -1
- package/dist/{chunk-S66I2PYB.js → chunk-TN3PU2WK.js} +1 -1
- package/dist/components/command-queue/index.js +6 -4
- package/dist/components/index.js +9 -7
- package/dist/components/notifications/index.js +4 -3
- package/dist/display/index.js +3 -2
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/index.js +5 -4
- package/dist/hooks/published.d.ts +1 -1
- package/dist/hooks/published.js +4 -3
- package/dist/index.d.ts +458 -120
- package/dist/index.js +23 -17
- package/dist/initialization/index.d.ts +49 -1
- package/dist/initialization/index.js +5 -2
- package/dist/organization/index.d.ts +61 -2
- package/dist/organization/index.js +5 -2
- package/dist/profile/index.d.ts +30 -2
- package/dist/profile/index.js +2 -1
- package/dist/provider/index.d.ts +123 -30
- package/dist/provider/index.js +11 -6
- package/dist/provider/published.d.ts +96 -16
- package/dist/provider/published.js +10 -4
- package/dist/utils/index.js +2 -1
- package/package.json +17 -4
- package/dist/chunk-GDV44UWF.js +0 -138
- package/dist/chunk-HBRMWW6V.js +0 -43
- package/dist/chunk-QGEFP2EU.js +0 -399
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return type of useOrganizations hook (subset needed by the factory)
|
|
3
|
+
*/
|
|
4
|
+
interface UseOrganizationsReturn {
|
|
5
|
+
isInitializing: boolean;
|
|
6
|
+
isOrgRefreshing: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Hook that returns apiRequest and deferredApiRequest bound to the current
|
|
10
|
+
* ApiClientContext. apiUrl is the only parameter because org ID is resolved
|
|
11
|
+
* at call time via getOrganizationId() from context.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```typescript
|
|
15
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
16
|
+
*
|
|
17
|
+
* const API_URL = import.meta.env.VITE_API_SERVER || 'http://localhost:5170'
|
|
18
|
+
*
|
|
19
|
+
* function useMyClient() {
|
|
20
|
+
* return useApiClient(API_URL)
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function useApiClient(apiUrl: string): {
|
|
25
|
+
apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
26
|
+
deferredApiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
27
|
+
isOrganizationReady: boolean;
|
|
28
|
+
isInitializing: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Factory function to create a useApiClient hook for your app.
|
|
32
|
+
*
|
|
33
|
+
* @deprecated Use the plain `useApiClient(apiUrl)` hook instead.
|
|
34
|
+
* The factory pattern is no longer needed because org ID is resolved at
|
|
35
|
+
* call time via getOrganizationId() from ApiClientContext.
|
|
36
|
+
*
|
|
37
|
+
* Migration:
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // Before
|
|
40
|
+
* export const useApiClient = createUseApiClient(useOrganizations, API_URL)
|
|
41
|
+
*
|
|
42
|
+
* // After
|
|
43
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
44
|
+
* // call useApiClient(API_URL) directly in your hook/component
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function createUseApiClient(useOrganizations: () => UseOrganizationsReturn, apiUrl: string): () => {
|
|
48
|
+
apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
49
|
+
deferredApiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
50
|
+
isOrganizationReady: boolean;
|
|
51
|
+
isInitializing: boolean;
|
|
52
|
+
};
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=useApiClient.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApiClient.d.ts","sourceRoot":"","sources":["../../../src/api/hooks/useApiClient.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,UAAU,sBAAsB;IAC9B,cAAc,EAAE,OAAO,CAAA;IACvB,eAAe,EAAE,OAAO,CAAA;CACzB;AA+FD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM;iBA+BtC,CAAC,YAAY,MAAM,YAAW,WAAW,KAAQ,OAAO,CAAC,CAAC,CAAC;yBAMrD,CAAC,YAAY,MAAM,YAAW,WAAW,KAAQ,OAAO,CAAC,CAAC,CAAC;;;EAerE;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,sBAAsB,EAAE,MAAM,EAAE,MAAM;iBAqC1F,CAAC,YAAY,MAAM,YAAW,WAAW,KAAQ,OAAO,CAAC,CAAC,CAAC;yBAMrD,CAAC,YAAY,MAAM,YAAW,WAAW,KAAQ,OAAO,CAAC,CAAC,CAAC;;;EAqBvE"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { HTTP_HEADERS } from '@repo/core';
|
|
3
|
+
import { APIClientError } from '../../utils';
|
|
4
|
+
import { useApiClientContext } from '../context/ApiClientProvider';
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Shared request logic builder — used by both the plain hook and the factory.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
function buildApiRequest(getAccessToken, getOrganizationId, apiUrl, handleResponseError) {
|
|
10
|
+
return async function apiRequest(endpoint, options = {}) {
|
|
11
|
+
try {
|
|
12
|
+
// Get the access token from context
|
|
13
|
+
const token = await getAccessToken();
|
|
14
|
+
// Read org ID at call time — reflects org switches without re-render
|
|
15
|
+
const organizationId = getOrganizationId();
|
|
16
|
+
// DEBUG
|
|
17
|
+
if (organizationId)
|
|
18
|
+
console.log('[apiRequest]', endpoint, 'org:', organizationId);
|
|
19
|
+
// Only set Content-Type if there's a body
|
|
20
|
+
const headers = {
|
|
21
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
22
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
23
|
+
...(organizationId && { [HTTP_HEADERS.WORKOS_ORGANIZATION_ID]: organizationId }),
|
|
24
|
+
...options.headers
|
|
25
|
+
};
|
|
26
|
+
const timeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
27
|
+
const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;
|
|
28
|
+
const response = await fetch(`${apiUrl}/api${endpoint}`, {
|
|
29
|
+
...options,
|
|
30
|
+
headers,
|
|
31
|
+
signal
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
await handleResponseError(endpoint, response, options);
|
|
35
|
+
}
|
|
36
|
+
// Handle 204 No Content responses
|
|
37
|
+
if (response.status === 204) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
// DEBUG
|
|
42
|
+
if (organizationId && endpoint.includes('dashboard'))
|
|
43
|
+
console.log('[apiResponse] dashboard:', data);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// If getAccessToken fails, still make the request without auth
|
|
48
|
+
// This allows public endpoints to work
|
|
49
|
+
if (error instanceof Error && error.message.includes('No access token')) {
|
|
50
|
+
// Only set Content-Type if there's a body
|
|
51
|
+
const headers = {
|
|
52
|
+
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
53
|
+
...options.headers
|
|
54
|
+
};
|
|
55
|
+
const fallbackTimeoutSignal = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
56
|
+
const fallbackSignal = options.signal
|
|
57
|
+
? AbortSignal.any([options.signal, fallbackTimeoutSignal])
|
|
58
|
+
: fallbackTimeoutSignal;
|
|
59
|
+
const response = await fetch(`${apiUrl}/api${endpoint}`, {
|
|
60
|
+
...options,
|
|
61
|
+
headers,
|
|
62
|
+
signal: fallbackSignal
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
await handleResponseError(endpoint, response, options);
|
|
66
|
+
}
|
|
67
|
+
if (response.status === 204) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
return response.json();
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Plain hook — reads everything from ApiClientContext directly.
|
|
78
|
+
// apiUrl must be provided because the context does not store it.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
/**
|
|
81
|
+
* Hook that returns apiRequest and deferredApiRequest bound to the current
|
|
82
|
+
* ApiClientContext. apiUrl is the only parameter because org ID is resolved
|
|
83
|
+
* at call time via getOrganizationId() from context.
|
|
84
|
+
*
|
|
85
|
+
* Usage:
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
88
|
+
*
|
|
89
|
+
* const API_URL = import.meta.env.VITE_API_SERVER || 'http://localhost:5170'
|
|
90
|
+
*
|
|
91
|
+
* function useMyClient() {
|
|
92
|
+
* return useApiClient(API_URL)
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
export function useApiClient(apiUrl) {
|
|
97
|
+
const { getAccessToken, getOrganizationId, isOrganizationReady, onError } = useApiClientContext();
|
|
98
|
+
const handleResponseError = useCallback(async (endpoint, response, options) => {
|
|
99
|
+
const errorData = await response.json().catch(() => ({
|
|
100
|
+
error: `HTTP ${response.status}`,
|
|
101
|
+
code: 'INTERNAL_SERVER_ERROR'
|
|
102
|
+
}));
|
|
103
|
+
if (response.status >= 500 && onError) {
|
|
104
|
+
onError(endpoint, new Error(errorData.error), {
|
|
105
|
+
method: options.method || 'GET',
|
|
106
|
+
statusCode: response.status,
|
|
107
|
+
requestId: errorData.requestId
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
throw new APIClientError(errorData.error, errorData.code, response.status, errorData.requestId, errorData.fields, errorData.retryAfter);
|
|
111
|
+
}, [onError]);
|
|
112
|
+
const apiRequest = useCallback((endpoint, options = {}) => buildApiRequest(getAccessToken, getOrganizationId, apiUrl, handleResponseError)(endpoint, options), [getAccessToken, getOrganizationId, apiUrl, handleResponseError]);
|
|
113
|
+
const deferredApiRequest = useCallback(async (endpoint, options = {}) => {
|
|
114
|
+
if (!isOrganizationReady) {
|
|
115
|
+
throw new Error('No organization selected. Please select an organization.');
|
|
116
|
+
}
|
|
117
|
+
return apiRequest(endpoint, options);
|
|
118
|
+
}, [apiRequest, isOrganizationReady]);
|
|
119
|
+
return {
|
|
120
|
+
apiRequest,
|
|
121
|
+
deferredApiRequest,
|
|
122
|
+
isOrganizationReady,
|
|
123
|
+
isInitializing: false
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Factory — kept for backwards compatibility during migration.
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
/**
|
|
130
|
+
* Factory function to create a useApiClient hook for your app.
|
|
131
|
+
*
|
|
132
|
+
* @deprecated Use the plain `useApiClient(apiUrl)` hook instead.
|
|
133
|
+
* The factory pattern is no longer needed because org ID is resolved at
|
|
134
|
+
* call time via getOrganizationId() from ApiClientContext.
|
|
135
|
+
*
|
|
136
|
+
* Migration:
|
|
137
|
+
* ```typescript
|
|
138
|
+
* // Before
|
|
139
|
+
* export const useApiClient = createUseApiClient(useOrganizations, API_URL)
|
|
140
|
+
*
|
|
141
|
+
* // After
|
|
142
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
143
|
+
* // call useApiClient(API_URL) directly in your hook/component
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
export function createUseApiClient(useOrganizations, apiUrl) {
|
|
147
|
+
return function useApiClientCompat() {
|
|
148
|
+
const { getAccessToken, getOrganizationId, isOrganizationReady, onError } = useApiClientContext();
|
|
149
|
+
const { isInitializing, isOrgRefreshing } = useOrganizations();
|
|
150
|
+
/**
|
|
151
|
+
* Handle API response errors
|
|
152
|
+
* Logs 5xx errors via onError callback and throws APIClientError
|
|
153
|
+
*/
|
|
154
|
+
const handleResponseError = useCallback(async (endpoint, response, options) => {
|
|
155
|
+
const errorData = await response.json().catch(() => ({
|
|
156
|
+
error: `HTTP ${response.status}`,
|
|
157
|
+
code: 'INTERNAL_SERVER_ERROR'
|
|
158
|
+
}));
|
|
159
|
+
if (response.status >= 500 && onError) {
|
|
160
|
+
onError(endpoint, new Error(errorData.error), {
|
|
161
|
+
method: options.method || 'GET',
|
|
162
|
+
statusCode: response.status,
|
|
163
|
+
requestId: errorData.requestId
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
throw new APIClientError(errorData.error, errorData.code, response.status, errorData.requestId, errorData.fields, errorData.retryAfter);
|
|
167
|
+
}, [onError]);
|
|
168
|
+
const apiRequest = useCallback((endpoint, options = {}) => buildApiRequest(getAccessToken, getOrganizationId, apiUrl, handleResponseError)(endpoint, options), [getAccessToken, getOrganizationId, handleResponseError]);
|
|
169
|
+
const deferredApiRequest = useCallback(async (endpoint, options = {}) => {
|
|
170
|
+
if (isInitializing || isOrgRefreshing) {
|
|
171
|
+
throw new Error('Organization context is still initializing. Please wait.');
|
|
172
|
+
}
|
|
173
|
+
if (!isOrganizationReady) {
|
|
174
|
+
throw new Error('No organization selected. Please select an organization.');
|
|
175
|
+
}
|
|
176
|
+
return apiRequest(endpoint, options);
|
|
177
|
+
}, [apiRequest, isInitializing, isOrgRefreshing, isOrganizationReady]);
|
|
178
|
+
return {
|
|
179
|
+
apiRequest,
|
|
180
|
+
deferredApiRequest,
|
|
181
|
+
isOrganizationReady,
|
|
182
|
+
isInitializing: isInitializing || isOrgRefreshing
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
}
|
package/dist/api/index.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
5
5
|
*/
|
|
6
6
|
interface ApiClientContextValue {
|
|
7
7
|
getAccessToken: () => Promise<string | undefined>;
|
|
8
|
+
/** @deprecated Read via getOrganizationId() instead. Kept for downstream context consumers during migration. */
|
|
8
9
|
organizationId: string | null;
|
|
9
10
|
isOrganizationReady: boolean;
|
|
11
|
+
getOrganizationId: () => string | null;
|
|
10
12
|
onError?: (endpoint: string, error: Error, details?: ApiErrorDetails) => void;
|
|
11
13
|
}
|
|
12
14
|
interface ApiErrorDetails {
|
|
@@ -17,7 +19,19 @@ interface ApiErrorDetails {
|
|
|
17
19
|
interface ApiClientProviderProps {
|
|
18
20
|
children: React.ReactNode;
|
|
19
21
|
getAccessToken: () => Promise<string | undefined>;
|
|
20
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Static org ID. Used when org ID is stable at render time.
|
|
24
|
+
* Cannot reflect org switches without re-rendering the provider.
|
|
25
|
+
* Prefer getOrganizationId for dynamic org context.
|
|
26
|
+
* @deprecated Pass getOrganizationId instead for dynamic org resolution.
|
|
27
|
+
*/
|
|
28
|
+
organizationId?: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Callback invoked on every request to read the current org ID.
|
|
31
|
+
* Takes precedence over organizationId when provided.
|
|
32
|
+
* Allows org switching without re-rendering the provider tree.
|
|
33
|
+
*/
|
|
34
|
+
getOrganizationId?: () => string | null;
|
|
21
35
|
isOrganizationReady: boolean;
|
|
22
36
|
onError?: (endpoint: string, error: Error, details?: ApiErrorDetails) => void;
|
|
23
37
|
}
|
|
@@ -52,28 +66,52 @@ declare function useApiClientContext(): ApiClientContextValue;
|
|
|
52
66
|
* }
|
|
53
67
|
* ```
|
|
54
68
|
*/
|
|
55
|
-
declare function ApiClientProvider({ children, getAccessToken, organizationId, isOrganizationReady, onError }: ApiClientProviderProps): react_jsx_runtime.JSX.Element;
|
|
69
|
+
declare function ApiClientProvider({ children, getAccessToken, organizationId, getOrganizationId: getOrganizationIdProp, isOrganizationReady, onError }: ApiClientProviderProps): react_jsx_runtime.JSX.Element;
|
|
56
70
|
|
|
57
71
|
/**
|
|
58
|
-
* Return type of useOrganizations hook (subset needed by
|
|
72
|
+
* Return type of useOrganizations hook (subset needed by the factory)
|
|
59
73
|
*/
|
|
60
74
|
interface UseOrganizationsReturn {
|
|
61
75
|
isInitializing: boolean;
|
|
62
76
|
isOrgRefreshing: boolean;
|
|
63
77
|
}
|
|
64
78
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* while each app provides its own useOrganizations hook and API URL.
|
|
79
|
+
* Hook that returns apiRequest and deferredApiRequest bound to the current
|
|
80
|
+
* ApiClientContext. apiUrl is the only parameter because org ID is resolved
|
|
81
|
+
* at call time via getOrganizationId() from context.
|
|
69
82
|
*
|
|
70
|
-
* Usage
|
|
83
|
+
* Usage:
|
|
71
84
|
* ```typescript
|
|
72
|
-
* import {
|
|
73
|
-
* import { useOrganizations } from './organization/hooks/useOrganizations'
|
|
85
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
74
86
|
*
|
|
75
87
|
* const API_URL = import.meta.env.VITE_API_SERVER || 'http://localhost:5170'
|
|
88
|
+
*
|
|
89
|
+
* function useMyClient() {
|
|
90
|
+
* return useApiClient(API_URL)
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
declare function useApiClient(apiUrl: string): {
|
|
95
|
+
apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
96
|
+
deferredApiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
97
|
+
isOrganizationReady: boolean;
|
|
98
|
+
isInitializing: boolean;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Factory function to create a useApiClient hook for your app.
|
|
102
|
+
*
|
|
103
|
+
* @deprecated Use the plain `useApiClient(apiUrl)` hook instead.
|
|
104
|
+
* The factory pattern is no longer needed because org ID is resolved at
|
|
105
|
+
* call time via getOrganizationId() from ApiClientContext.
|
|
106
|
+
*
|
|
107
|
+
* Migration:
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // Before
|
|
76
110
|
* export const useApiClient = createUseApiClient(useOrganizations, API_URL)
|
|
111
|
+
*
|
|
112
|
+
* // After
|
|
113
|
+
* import { useApiClient } from '@repo/ui/api'
|
|
114
|
+
* // call useApiClient(API_URL) directly in your hook/component
|
|
77
115
|
* ```
|
|
78
116
|
*/
|
|
79
117
|
declare function createUseApiClient(useOrganizations: () => UseOrganizationsReturn, apiUrl: string): () => {
|
|
@@ -83,5 +121,5 @@ declare function createUseApiClient(useOrganizations: () => UseOrganizationsRetu
|
|
|
83
121
|
isInitializing: boolean;
|
|
84
122
|
};
|
|
85
123
|
|
|
86
|
-
export { ApiClientProvider, createUseApiClient, useApiClientContext };
|
|
124
|
+
export { ApiClientProvider, createUseApiClient, useApiClient, useApiClientContext };
|
|
87
125
|
export type { ApiClientContextValue, ApiClientProviderProps, ApiErrorDetails };
|
package/dist/api/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import '../chunk-XCYKC6OZ.js';
|
|
2
|
-
export { ApiClientProvider, createUseApiClient, useApiClientContext } from '../chunk-
|
|
3
|
-
import '../chunk-
|
|
2
|
+
export { ApiClientProvider, createUseApiClient, useApiClient, useApiClientContext } from '../chunk-BWCC6ZJC.js';
|
|
3
|
+
import '../chunk-JVAZHVNV.js';
|
|
4
|
+
import '../chunk-4VGWQ5AN.js';
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -83,5 +83,91 @@ declare function useStableAccessToken(): () => Promise<string>;
|
|
|
83
83
|
*/
|
|
84
84
|
declare function useSessionCheck(): void;
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
interface InitializationError {
|
|
87
|
+
layer: 'auth' | 'profile' | 'organization';
|
|
88
|
+
message: string;
|
|
89
|
+
originalError?: Error;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ProtectedRouteProps {
|
|
93
|
+
children: ReactNode;
|
|
94
|
+
/**
|
|
95
|
+
* Path to redirect to when user is unauthenticated.
|
|
96
|
+
* @default '/login'
|
|
97
|
+
*/
|
|
98
|
+
redirectTo?: string;
|
|
99
|
+
/**
|
|
100
|
+
* Rendered while initialization is in progress.
|
|
101
|
+
* When not provided, nothing is rendered during initialization.
|
|
102
|
+
*/
|
|
103
|
+
fallback?: ReactNode;
|
|
104
|
+
/**
|
|
105
|
+
* Rendered when initialization fails with a non-organization error.
|
|
106
|
+
* Receives the error and a retry callback.
|
|
107
|
+
* When not provided, the error is silently swallowed (children not rendered).
|
|
108
|
+
*/
|
|
109
|
+
errorFallback?: (error: InitializationError, retry: () => void) => ReactNode;
|
|
110
|
+
/**
|
|
111
|
+
* When true (default), waits for both user AND organization to be ready
|
|
112
|
+
* before rendering children. When false, only waits for user readiness.
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
115
|
+
waitForOrganization?: boolean;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Headless route guard for authenticated pages.
|
|
119
|
+
*
|
|
120
|
+
* Reads initialization state from the nearest InitializationProvider.
|
|
121
|
+
* Redirects unauthenticated users to `redirectTo` (default: '/login')
|
|
122
|
+
* with a `returnTo` search param preserving the current location.
|
|
123
|
+
*
|
|
124
|
+
* Organization-layer errors are allowed through — routes like /invitations
|
|
125
|
+
* must be accessible even when the user has no org membership.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* // With custom fallback (your Mantine loader):
|
|
129
|
+
* <ProtectedRoute fallback={<AppShellLoader />}>
|
|
130
|
+
* <DashboardPage />
|
|
131
|
+
* </ProtectedRoute>
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* // Wait only for user, not org (e.g. /invitations route):
|
|
135
|
+
* <ProtectedRoute waitForOrganization={false}>
|
|
136
|
+
* <InvitationsPage />
|
|
137
|
+
* </ProtectedRoute>
|
|
138
|
+
*/
|
|
139
|
+
declare function ProtectedRoute({ children, redirectTo, fallback, errorFallback, waitForOrganization }: ProtectedRouteProps): react_jsx_runtime.JSX.Element | null;
|
|
140
|
+
|
|
141
|
+
interface AdminGuardProps {
|
|
142
|
+
children: ReactNode;
|
|
143
|
+
/**
|
|
144
|
+
* Path to redirect non-admin users to.
|
|
145
|
+
* @default '/'
|
|
146
|
+
*/
|
|
147
|
+
redirectTo?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Rendered while user readiness is being determined.
|
|
150
|
+
* When not provided, nothing is rendered during initialization.
|
|
151
|
+
*/
|
|
152
|
+
fallback?: ReactNode;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Headless route guard for platform admin pages.
|
|
156
|
+
*
|
|
157
|
+
* Reads `profile.is_platform_admin` from the nearest InitializationProvider.
|
|
158
|
+
* Non-admin users are redirected to `redirectTo` (default: '/').
|
|
159
|
+
*
|
|
160
|
+
* Must be nested inside a ProtectedRoute (or equivalent) so that
|
|
161
|
+
* `userReady` is guaranteed to be true when this guard runs.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* <ProtectedRoute>
|
|
165
|
+
* <AdminGuard fallback={<AppShellLoader />}>
|
|
166
|
+
* <AdminDashboard />
|
|
167
|
+
* </AdminGuard>
|
|
168
|
+
* </ProtectedRoute>
|
|
169
|
+
*/
|
|
170
|
+
declare function AdminGuard({ children, redirectTo, fallback }: AdminGuardProps): react_jsx_runtime.JSX.Element | null;
|
|
171
|
+
|
|
172
|
+
export { AdminGuard, AuthProvider, ProtectedRoute, WorkOSAuthBridge, useAuthContext, useOAuthContext, useSessionCheck as useRefocusSessionCheck, useSessionCheck, useStableAccessToken };
|
|
173
|
+
export type { AdminGuardProps, AuthAdapter, OAuthContextValue, ProtectedRouteProps };
|
package/dist/auth/index.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export { useSessionCheck as useRefocusSessionCheck, useSessionCheck, useStableAccessToken } from '../chunk-
|
|
1
|
+
export { AdminGuard, ProtectedRoute, useSessionCheck as useRefocusSessionCheck, useSessionCheck, useStableAccessToken } from '../chunk-FDCVFCOQ.js';
|
|
2
2
|
export { WorkOSAuthBridge } from '../chunk-NIAVTSMB.js';
|
|
3
|
-
export { useOAuthContext } from '../chunk-
|
|
3
|
+
export { useOAuthContext } from '../chunk-QSVZP2NU.js';
|
|
4
|
+
import '../chunk-QQOLC46E.js';
|
|
5
|
+
import '../chunk-DD3CCMCZ.js';
|
|
6
|
+
import '../chunk-4KAG5U7A.js';
|
|
7
|
+
import '../chunk-KA7LO7U5.js';
|
|
4
8
|
export { AuthProvider, useAuthContext } from '../chunk-7PLEQFHO.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useElevasisServices } from './chunk-KA7LO7U5.js';
|
|
2
2
|
import { useAuthContext } from './chunk-7PLEQFHO.js';
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { createContext, useState, useEffect, useContext, createElement } from 'react';
|
|
4
4
|
|
|
5
5
|
// src/profile/services/UserProfileService.ts
|
|
6
6
|
var UserProfileService = class {
|
|
@@ -65,8 +65,6 @@ var UserProfileService = class {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
};
|
|
68
|
-
|
|
69
|
-
// src/profile/hooks/useUserProfile.ts
|
|
70
68
|
var useUserProfile = (options) => {
|
|
71
69
|
const { user, isLoading: authLoading } = useAuthContext();
|
|
72
70
|
const { apiRequest } = useElevasisServices();
|
|
@@ -125,5 +123,21 @@ var useUserProfile = (options) => {
|
|
|
125
123
|
refetch
|
|
126
124
|
};
|
|
127
125
|
};
|
|
126
|
+
var ProfileContext = createContext(null);
|
|
127
|
+
function useProfile() {
|
|
128
|
+
const ctx = useContext(ProfileContext);
|
|
129
|
+
if (!ctx) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"useProfile must be used within a ProfileProvider. Wrap your app (or the relevant subtree) with <ProfileProvider>."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return ctx;
|
|
135
|
+
}
|
|
136
|
+
function ProfileProvider({ children }) {
|
|
137
|
+
const value = useUserProfile();
|
|
138
|
+
if (!value.loading && value.profile) console.log("[ProfileProvider] ready, profile:", value.profile.id);
|
|
139
|
+
if (!value.loading && value.error) console.log("[ProfileProvider] error:", value.error.message);
|
|
140
|
+
return createElement(ProfileContext.Provider, { value }, children);
|
|
141
|
+
}
|
|
128
142
|
|
|
129
|
-
export { UserProfileService, useUserProfile };
|
|
143
|
+
export { ProfileProvider, UserProfileService, useProfile, useUserProfile };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// src/utils/error-utils.ts
|
|
2
|
+
var APIClientError = class extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
code;
|
|
5
|
+
requestId;
|
|
6
|
+
fields;
|
|
7
|
+
retryAfter;
|
|
8
|
+
constructor(message, code, statusCode, requestId, fields, retryAfter) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "APIClientError";
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.requestId = requestId;
|
|
14
|
+
this.fields = fields;
|
|
15
|
+
this.retryAfter = retryAfter;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function isAPIClientError(error) {
|
|
19
|
+
return error instanceof APIClientError;
|
|
20
|
+
}
|
|
21
|
+
function getErrorInfo(error) {
|
|
22
|
+
if (isAPIClientError(error)) {
|
|
23
|
+
return {
|
|
24
|
+
message: error.message,
|
|
25
|
+
code: error.code,
|
|
26
|
+
requestId: error.requestId,
|
|
27
|
+
statusCode: error.statusCode,
|
|
28
|
+
fields: error.fields,
|
|
29
|
+
retryAfter: error.retryAfter
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
return {
|
|
34
|
+
message: error.message
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
message: String(error)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function getErrorTitle(code) {
|
|
42
|
+
if (!code) return "Error";
|
|
43
|
+
switch (code) {
|
|
44
|
+
case "VALIDATION_ERROR":
|
|
45
|
+
return "Validation Error";
|
|
46
|
+
case "AUTHENTICATION_FAILED":
|
|
47
|
+
return "Authentication Required";
|
|
48
|
+
case "FORBIDDEN":
|
|
49
|
+
return "Access Denied";
|
|
50
|
+
case "NOT_FOUND":
|
|
51
|
+
return "Not Found";
|
|
52
|
+
case "CONFLICT":
|
|
53
|
+
return "Conflict";
|
|
54
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
55
|
+
return "Rate Limit Exceeded";
|
|
56
|
+
case "INTERNAL_SERVER_ERROR":
|
|
57
|
+
return "Server Error";
|
|
58
|
+
case "SERVICE_UNAVAILABLE":
|
|
59
|
+
return "Service Temporarily Unavailable";
|
|
60
|
+
default:
|
|
61
|
+
return "Error";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function formatErrorMessage(message, requestId, fields, retryAfter) {
|
|
65
|
+
let formatted = message;
|
|
66
|
+
if (fields && Object.keys(fields).length > 0) {
|
|
67
|
+
const fieldErrors = [];
|
|
68
|
+
for (const [field, errors] of Object.entries(fields)) {
|
|
69
|
+
const fieldName = field === "root" || field === "_root" ? "Request" : field;
|
|
70
|
+
errors.forEach((error) => {
|
|
71
|
+
fieldErrors.push(`\u2022 ${fieldName}: ${error}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (fieldErrors.length > 0) {
|
|
75
|
+
formatted = fieldErrors.join("\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (retryAfter) {
|
|
79
|
+
formatted += `
|
|
80
|
+
|
|
81
|
+
Please wait ${retryAfter} seconds before retrying.`;
|
|
82
|
+
}
|
|
83
|
+
if (requestId) {
|
|
84
|
+
formatted += `
|
|
85
|
+
|
|
86
|
+
Request ID: ${requestId}`;
|
|
87
|
+
}
|
|
88
|
+
return formatted;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { APIClientError, formatErrorMessage, getErrorInfo, getErrorTitle, isAPIClientError };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { useNotificationAdapter } from './chunk-TIRMFDM4.js';
|
|
1
2
|
import { useElevasisServices } from './chunk-KA7LO7U5.js';
|
|
2
|
-
import { showApiErrorNotification } from './chunk-7AI5ZYJ4.js';
|
|
3
3
|
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
@@ -26,6 +26,8 @@ z.object({
|
|
|
26
26
|
startDate: z.string().datetime(),
|
|
27
27
|
endDate: z.string().datetime()
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
// ../core/src/operations/notifications/api-schemas.ts
|
|
29
31
|
var NotificationCategorySchema = z.enum(["info", "queue", "alert", "error", "system"]);
|
|
30
32
|
var GetNotificationsQuerySchema = z.object({
|
|
31
33
|
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
@@ -47,6 +49,7 @@ z.object({
|
|
|
47
49
|
function useMarkAsRead() {
|
|
48
50
|
const queryClient = useQueryClient();
|
|
49
51
|
const { apiRequest } = useElevasisServices();
|
|
52
|
+
const notify = useNotificationAdapter();
|
|
50
53
|
return useMutation({
|
|
51
54
|
mutationFn: async (notificationId) => {
|
|
52
55
|
MarkAsReadParamsSchema.parse({ id: notificationId });
|
|
@@ -58,13 +61,14 @@ function useMarkAsRead() {
|
|
|
58
61
|
queryClient.invalidateQueries({ queryKey: ["notifications"] });
|
|
59
62
|
},
|
|
60
63
|
onError: (error) => {
|
|
61
|
-
|
|
64
|
+
notify.apiError(error);
|
|
62
65
|
}
|
|
63
66
|
});
|
|
64
67
|
}
|
|
65
68
|
function useMarkAllAsRead() {
|
|
66
69
|
const queryClient = useQueryClient();
|
|
67
70
|
const { apiRequest } = useElevasisServices();
|
|
71
|
+
const notify = useNotificationAdapter();
|
|
68
72
|
return useMutation({
|
|
69
73
|
mutationFn: async () => {
|
|
70
74
|
await apiRequest("/notifications/mark-all-read", {
|
|
@@ -75,7 +79,7 @@ function useMarkAllAsRead() {
|
|
|
75
79
|
queryClient.invalidateQueries({ queryKey: ["notifications"] });
|
|
76
80
|
},
|
|
77
81
|
onError: (error) => {
|
|
78
|
-
|
|
82
|
+
notify.apiError(error);
|
|
79
83
|
}
|
|
80
84
|
});
|
|
81
85
|
}
|