@addev-be/ui 0.17.4 → 0.18.1
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/package.json +1 -1
- package/src/components/search/QuickSearchBar.tsx +7 -16
- package/src/components/search/types.ts +3 -0
- package/src/providers/AuthenticationProvider/index.tsx +73 -13
- package/src/services/WebSocketService.ts +19 -1
- package/src/services/globalSearch.ts +5 -2
- package/src/services/hooks.ts +50 -2
package/package.json
CHANGED
|
@@ -10,22 +10,19 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { SearchResults, SearchTypeDefinitions } from './types';
|
|
12
12
|
|
|
13
|
-
import { useDebounce } from '@uidotdev/usehooks';
|
|
14
|
-
import { useGlobalSearchRequestHandler } from '../../services';
|
|
15
|
-
import { Input } from '../forms';
|
|
16
13
|
import { Dropdown } from '../layout';
|
|
14
|
+
import { Input } from '../forms';
|
|
17
15
|
import { QuickSearchResults } from './QuickSearchResults';
|
|
16
|
+
import { useDebounce } from '@uidotdev/usehooks';
|
|
17
|
+
import { useGlobalSearchRequestHandler } from '../../services';
|
|
18
18
|
|
|
19
19
|
type QuickSearchBarProps<R> = {
|
|
20
|
-
|
|
21
|
-
types?: string[];
|
|
22
|
-
conditions?: { field: string; operator: string }[];
|
|
20
|
+
types: SearchTypeDefinitions<R>;
|
|
23
21
|
placeholder?: string;
|
|
24
22
|
inputComponent?: FC<ComponentProps<typeof Input>>;
|
|
25
23
|
};
|
|
26
24
|
|
|
27
25
|
export const QuickSearchBar = <R,>({
|
|
28
|
-
definitions,
|
|
29
26
|
types,
|
|
30
27
|
placeholder,
|
|
31
28
|
inputComponent: InputComponent = Input,
|
|
@@ -45,21 +42,15 @@ export const QuickSearchBar = <R,>({
|
|
|
45
42
|
useEffect(() => {
|
|
46
43
|
if (debouncedTerm) {
|
|
47
44
|
globalSearch({
|
|
48
|
-
types
|
|
45
|
+
types,
|
|
49
46
|
searchTerm: debouncedTerm,
|
|
50
47
|
limit: 5,
|
|
51
|
-
conditions: [
|
|
52
|
-
{
|
|
53
|
-
field: 'DeletedAt',
|
|
54
|
-
operator: 'isNull',
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
48
|
}).then((response) => {
|
|
58
49
|
setResults(response.data as SearchResults<R>);
|
|
59
50
|
setDropdownVisible(true);
|
|
60
51
|
});
|
|
61
52
|
}
|
|
62
|
-
}, [globalSearch, debouncedTerm,
|
|
53
|
+
}, [globalSearch, debouncedTerm, types]);
|
|
63
54
|
|
|
64
55
|
const onFocus = useCallback(() => {
|
|
65
56
|
setDropdownVisible(true);
|
|
@@ -97,7 +88,7 @@ export const QuickSearchBar = <R,>({
|
|
|
97
88
|
{results && (
|
|
98
89
|
<QuickSearchResults
|
|
99
90
|
results={results}
|
|
100
|
-
definitions={
|
|
91
|
+
definitions={types}
|
|
101
92
|
term={debouncedTerm}
|
|
102
93
|
/>
|
|
103
94
|
)}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { FC, ReactNode } from 'react';
|
|
2
|
+
import { ConditionDTO, OrderByDTO } from '../../services';
|
|
2
3
|
|
|
3
4
|
export type SearchTypeDefinition<T> = {
|
|
4
5
|
title: string;
|
|
6
|
+
orderBy?: OrderByDTO[];
|
|
7
|
+
conditions?: ConditionDTO[];
|
|
5
8
|
quickRenderer: (result: T, term: string) => ReactNode;
|
|
6
9
|
titleRenderer: (result: T, term: string) => ReactNode;
|
|
7
10
|
detailsRenderer: (result: T, term: string) => ReactNode;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { basePermissions } from './helpers';
|
|
22
22
|
|
|
23
23
|
export type AuthenticationContextProps = {
|
|
24
|
+
status?: number;
|
|
24
25
|
token?: string;
|
|
25
26
|
user?: UserDTO;
|
|
26
27
|
isAuthenticated: boolean;
|
|
@@ -36,6 +37,7 @@ export type AuthenticationContextProps = {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export const AuthenticationContext = createContext<AuthenticationContextProps>({
|
|
40
|
+
status: undefined,
|
|
39
41
|
authenticate: () => {},
|
|
40
42
|
isAuthenticated: false,
|
|
41
43
|
login: async () => false,
|
|
@@ -64,11 +66,13 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
64
66
|
() => ({ ...basePermissions, ...permissions }),
|
|
65
67
|
[permissions]
|
|
66
68
|
);
|
|
69
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
67
70
|
const [, setWebSocketStatus] = useState<boolean | undefined>(false);
|
|
68
71
|
const [token, setToken] = useState<string | undefined>(
|
|
69
72
|
sessionStorage.getItem('authToken') || undefined
|
|
70
73
|
);
|
|
71
74
|
const [user, setUser] = useState<UserDTO | undefined>(undefined);
|
|
75
|
+
const [status, setStatus] = useState<number | undefined>(undefined);
|
|
72
76
|
|
|
73
77
|
const sendAuthenticateRequest =
|
|
74
78
|
useAuthenticateRequestHandler(requestHandlerName);
|
|
@@ -81,9 +85,12 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
81
85
|
|
|
82
86
|
const authenticate = useCallback(
|
|
83
87
|
(force = false) => {
|
|
84
|
-
if (token && (!user || force)) {
|
|
88
|
+
if (token && (!user || force) && !isLoading) {
|
|
89
|
+
setIsLoading(true);
|
|
85
90
|
sendAuthenticateRequest({ token }).then(
|
|
86
91
|
(response) => {
|
|
92
|
+
setIsLoading(false);
|
|
93
|
+
setStatus(response.status);
|
|
87
94
|
if (response.status === 0 && response.user) {
|
|
88
95
|
setUser(response.user);
|
|
89
96
|
} else if (response.status > 0) {
|
|
@@ -92,13 +99,14 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
92
99
|
}
|
|
93
100
|
},
|
|
94
101
|
() => {
|
|
102
|
+
setIsLoading(false);
|
|
95
103
|
setToken(undefined);
|
|
96
104
|
setUser(undefined);
|
|
97
105
|
}
|
|
98
106
|
);
|
|
99
107
|
}
|
|
100
108
|
},
|
|
101
|
-
[sendAuthenticateRequest, token, user]
|
|
109
|
+
[isLoading, sendAuthenticateRequest, token, user]
|
|
102
110
|
);
|
|
103
111
|
|
|
104
112
|
useEffect(() => {
|
|
@@ -110,10 +118,20 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
110
118
|
}
|
|
111
119
|
}, [authenticate, token]);
|
|
112
120
|
|
|
121
|
+
const [networkStatus, setNetworkStatus] = useState<boolean | undefined>(
|
|
122
|
+
false
|
|
123
|
+
);
|
|
124
|
+
|
|
113
125
|
const login = useCallback(
|
|
114
|
-
async (username: string, password: string) =>
|
|
115
|
-
|
|
126
|
+
async (username: string, password: string) => {
|
|
127
|
+
if (isLoading) {
|
|
128
|
+
throw new Error('Already loading');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setIsLoading(true);
|
|
132
|
+
return sendLoginRequest({ username, password })
|
|
116
133
|
.then((response) => {
|
|
134
|
+
setIsLoading(false);
|
|
117
135
|
if (response.status === 0 && response.user) {
|
|
118
136
|
setToken(response.token);
|
|
119
137
|
setUser(response.user);
|
|
@@ -122,11 +140,13 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
122
140
|
throw new Error(`Login failed with status ${response.status}`);
|
|
123
141
|
})
|
|
124
142
|
.catch(() => {
|
|
143
|
+
setIsLoading(false);
|
|
125
144
|
setToken(undefined);
|
|
126
145
|
setUser(undefined);
|
|
127
146
|
return false;
|
|
128
|
-
})
|
|
129
|
-
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
[isLoading, sendLoginRequest]
|
|
130
150
|
);
|
|
131
151
|
|
|
132
152
|
const logout = useCallback(
|
|
@@ -138,6 +158,7 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
138
158
|
.finally(() => {
|
|
139
159
|
setToken(undefined);
|
|
140
160
|
setUser(undefined);
|
|
161
|
+
setStatus(undefined);
|
|
141
162
|
}),
|
|
142
163
|
[sendLogoutRequest]
|
|
143
164
|
);
|
|
@@ -183,8 +204,47 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
183
204
|
]
|
|
184
205
|
);
|
|
185
206
|
|
|
207
|
+
const webSocketStatusChanged = useCallback(
|
|
208
|
+
(status: boolean | undefined) => {
|
|
209
|
+
if (!networkStatus && status) {
|
|
210
|
+
authenticate(true);
|
|
211
|
+
}
|
|
212
|
+
setNetworkStatus(status);
|
|
213
|
+
},
|
|
214
|
+
[authenticate, networkStatus]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const webSocketErrorReceived = useCallback(
|
|
218
|
+
({ status }: { status: number; message: string }) => {
|
|
219
|
+
// Handle logout when NotAuthenticated error is received
|
|
220
|
+
if (status === -4) {
|
|
221
|
+
logout();
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
[logout]
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const subscription = WebSocketService.getInstance().status$.subscribe(
|
|
229
|
+
webSocketStatusChanged
|
|
230
|
+
);
|
|
231
|
+
return () => {
|
|
232
|
+
subscription.unsubscribe();
|
|
233
|
+
};
|
|
234
|
+
}, [webSocketStatusChanged]);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const subscription = WebSocketService.getInstance().error$.subscribe(
|
|
238
|
+
webSocketErrorReceived
|
|
239
|
+
);
|
|
240
|
+
return () => {
|
|
241
|
+
subscription.unsubscribe();
|
|
242
|
+
};
|
|
243
|
+
}, [webSocketErrorReceived]);
|
|
244
|
+
|
|
186
245
|
const contextValue = useMemo(
|
|
187
246
|
() => ({
|
|
247
|
+
status,
|
|
188
248
|
token,
|
|
189
249
|
user,
|
|
190
250
|
isAuthenticated: !!user,
|
|
@@ -199,17 +259,17 @@ export const AuthenticationProvider: FC<AuthenticationProviderProps> = ({
|
|
|
199
259
|
setToken,
|
|
200
260
|
}),
|
|
201
261
|
[
|
|
202
|
-
|
|
262
|
+
status,
|
|
263
|
+
token,
|
|
264
|
+
user,
|
|
203
265
|
authenticate,
|
|
204
|
-
checkRecoveryKey,
|
|
205
|
-
hasPermission,
|
|
206
266
|
login,
|
|
207
267
|
logout,
|
|
208
|
-
resetPassword,
|
|
209
268
|
sendRecoveryKey,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
269
|
+
checkRecoveryKey,
|
|
270
|
+
resetPassword,
|
|
271
|
+
allPermissions,
|
|
272
|
+
hasPermission,
|
|
213
273
|
]
|
|
214
274
|
);
|
|
215
275
|
|
|
@@ -3,6 +3,16 @@ import { Request } from './base';
|
|
|
3
3
|
import { Subject } from 'rxjs';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
5
|
|
|
6
|
+
export class WebSocketError extends Error {
|
|
7
|
+
public status: number;
|
|
8
|
+
|
|
9
|
+
constructor(status: number, message: string) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.name = 'WebSocketError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export class WebSocketService {
|
|
7
17
|
private static instance: WebSocketService;
|
|
8
18
|
private config: Config;
|
|
@@ -10,6 +20,7 @@ export class WebSocketService {
|
|
|
10
20
|
|
|
11
21
|
public status: boolean | undefined = undefined;
|
|
12
22
|
public status$ = new Subject<boolean | undefined>();
|
|
23
|
+
public error$ = new Subject<{ status: number; message: string }>();
|
|
13
24
|
|
|
14
25
|
private queue: Request[] = [];
|
|
15
26
|
private promises: {
|
|
@@ -99,6 +110,7 @@ export class WebSocketService {
|
|
|
99
110
|
const request: Request = { id, name, data };
|
|
100
111
|
this.promises[id] = { resolve, reject };
|
|
101
112
|
if (!this.socket || !this.status) {
|
|
113
|
+
console.log('[WS] Queuing request:', request);
|
|
102
114
|
this.queue.push(request);
|
|
103
115
|
return;
|
|
104
116
|
}
|
|
@@ -120,7 +132,13 @@ export class WebSocketService {
|
|
|
120
132
|
typeof response.data?.status === 'number' &&
|
|
121
133
|
response.data.status < 0
|
|
122
134
|
) {
|
|
123
|
-
this.promises[response.id].reject(
|
|
135
|
+
this.promises[response.id].reject(
|
|
136
|
+
new WebSocketError(response.data.status, response.data.message)
|
|
137
|
+
);
|
|
138
|
+
this.error$.next({
|
|
139
|
+
status: response.data.status,
|
|
140
|
+
message: response.data.message || 'Unknown error',
|
|
141
|
+
});
|
|
124
142
|
} else {
|
|
125
143
|
this.promises[response.id].resolve(response.data);
|
|
126
144
|
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ConditionDTO, SqlRequestRow } from './sqlRequests';
|
|
2
2
|
|
|
3
|
+
import { OrderByDTO } from './sqlRequests';
|
|
3
4
|
import { useWebSocketRequestHandler } from './hooks';
|
|
4
5
|
|
|
5
6
|
export type GlobalSearchRequestDTO = {
|
|
6
|
-
types:
|
|
7
|
+
types: Record<
|
|
8
|
+
string,
|
|
9
|
+
{ conditions?: ConditionDTO[]; orderBy?: OrderByDTO[] }
|
|
10
|
+
>;
|
|
7
11
|
searchTerm: string;
|
|
8
12
|
limit?: number;
|
|
9
|
-
conditions?: ConditionDTO[];
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
package/src/services/hooks.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { WebSocketError, WebSocketService } from './WebSocketService';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
|
|
1
4
|
import { HttpService } from './HttpService';
|
|
2
|
-
import { WebSocketService } from './WebSocketService';
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
5
|
import { useLoading } from '../providers';
|
|
5
6
|
|
|
6
7
|
export const useLoadingRequestHandler = <TReq, TRes>(
|
|
@@ -42,3 +43,50 @@ export const useHttpRequestHandler = <TReq, TRes>(
|
|
|
42
43
|
},
|
|
43
44
|
[name]
|
|
44
45
|
);
|
|
46
|
+
|
|
47
|
+
export const useRequestState = <TReq, TRes>(name: string, loading = false) => {
|
|
48
|
+
const [state, setState] = useState<
|
|
49
|
+
'initial' | 'loading' | 'success' | 'error'
|
|
50
|
+
>('initial');
|
|
51
|
+
const [status, setStatus] = useState<number | undefined>(undefined);
|
|
52
|
+
const [response, setResponse] = useState<TRes | null>(null);
|
|
53
|
+
|
|
54
|
+
const handleSendRequest = useWebSocketRequestHandler<TReq, TRes>(name);
|
|
55
|
+
const handleSendLoadingRequest = useLoadingRequestHandler<TReq, TRes>(name);
|
|
56
|
+
|
|
57
|
+
const sendRequest = useCallback(
|
|
58
|
+
async (data: TReq) => {
|
|
59
|
+
setState('loading');
|
|
60
|
+
try {
|
|
61
|
+
const res = await (loading
|
|
62
|
+
? handleSendLoadingRequest
|
|
63
|
+
: handleSendRequest)(data);
|
|
64
|
+
setStatus(0);
|
|
65
|
+
setResponse(res);
|
|
66
|
+
setState('success');
|
|
67
|
+
return res;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
setStatus(error instanceof WebSocketError ? error.status : -1);
|
|
70
|
+
setState('error');
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[handleSendLoadingRequest, handleSendRequest, loading]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const reset = useCallback(() => {
|
|
77
|
+
setState('initial');
|
|
78
|
+
setStatus(undefined);
|
|
79
|
+
setResponse(null);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
isInitial: state === 'initial',
|
|
85
|
+
isLoading: state === 'loading',
|
|
86
|
+
isSuccess: state === 'success',
|
|
87
|
+
isError: state === 'error',
|
|
88
|
+
response,
|
|
89
|
+
sendRequest,
|
|
90
|
+
reset,
|
|
91
|
+
};
|
|
92
|
+
};
|