@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addev-be/ui",
3
- "version": "0.17.4",
3
+ "version": "0.18.1",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "watch": "tsc -b --watch",
@@ -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
- definitions: SearchTypeDefinitions<R>;
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: types ?? Object.keys(definitions),
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, definitions, types]);
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={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
- sendLoginRequest({ username, password })
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
- [sendLoginRequest]
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
- allPermissions,
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
- token,
211
- user,
212
- setToken,
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(new Error(response.data.message));
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: string[];
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
@@ -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
+ };