@peassoft/mnr-web-topline 0.1.0 → 0.2.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/dist/css/index.css +17 -0
- package/dist/en/modules/logger/index.d.ts +1 -0
- package/dist/en/modules/logger/index.js +3 -0
- package/dist/en/modules/ui-lang/index.d.ts +8 -0
- package/dist/en/modules/ui-lang/index.js +20 -0
- package/dist/en/modules/user-data-change-beacon/index.d.ts +14 -0
- package/dist/en/modules/user-data-change-beacon/index.js +25 -0
- package/dist/en/modules/websocket/index.js +29 -14
- package/dist/en/parts/profile/actions/save-user-data/index.d.ts +9 -0
- package/dist/en/parts/profile/actions/save-user-data/index.js +53 -0
- package/dist/en/parts/profile/index.d.ts +1 -0
- package/dist/en/parts/profile/index.js +1 -0
- package/dist/en/parts/profile/ui/index.d.ts +6 -0
- package/dist/en/parts/profile/ui/index.js +47 -0
- package/dist/en/parts/profile/ui/root/get-initial-ui-lang.d.ts +2 -0
- package/dist/en/parts/profile/ui/root/get-initial-ui-lang.js +4 -0
- package/dist/en/parts/profile/ui/root/get-initial-user-name.d.ts +2 -0
- package/dist/en/parts/profile/ui/root/get-initial-user-name.js +3 -0
- package/dist/en/parts/profile/ui/root/has-changes.d.ts +7 -0
- package/dist/en/parts/profile/ui/root/has-changes.js +11 -0
- package/dist/en/parts/profile/ui/root/index.d.ts +9 -0
- package/dist/en/parts/profile/ui/root/index.js +95 -0
- package/dist/en/parts/shell/ui/shell/index.js +31 -11
- package/dist/ru/modules/logger/index.d.ts +1 -0
- package/dist/ru/modules/logger/index.js +3 -0
- package/dist/ru/modules/ui-lang/index.d.ts +8 -0
- package/dist/ru/modules/ui-lang/index.js +20 -0
- package/dist/ru/modules/user-data-change-beacon/index.d.ts +14 -0
- package/dist/ru/modules/user-data-change-beacon/index.js +25 -0
- package/dist/ru/modules/websocket/index.js +29 -14
- package/dist/ru/parts/profile/actions/save-user-data/index.d.ts +9 -0
- package/dist/ru/parts/profile/actions/save-user-data/index.js +53 -0
- package/dist/ru/parts/profile/index.d.ts +1 -0
- package/dist/ru/parts/profile/index.js +1 -0
- package/dist/ru/parts/profile/ui/index.d.ts +6 -0
- package/dist/ru/parts/profile/ui/index.js +47 -0
- package/dist/ru/parts/profile/ui/root/get-initial-ui-lang.d.ts +2 -0
- package/dist/ru/parts/profile/ui/root/get-initial-ui-lang.js +4 -0
- package/dist/ru/parts/profile/ui/root/get-initial-user-name.d.ts +2 -0
- package/dist/ru/parts/profile/ui/root/get-initial-user-name.js +3 -0
- package/dist/ru/parts/profile/ui/root/has-changes.d.ts +7 -0
- package/dist/ru/parts/profile/ui/root/has-changes.js +11 -0
- package/dist/ru/parts/profile/ui/root/index.d.ts +9 -0
- package/dist/ru/parts/profile/ui/root/index.js +95 -0
- package/dist/ru/parts/shell/ui/shell/index.js +31 -11
- package/package.json +4 -3
package/dist/css/index.css
CHANGED
|
@@ -155,6 +155,23 @@
|
|
|
155
155
|
font-weight: bolder;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
.topline_profile_grid {
|
|
159
|
+
display: grid;
|
|
160
|
+
grid-template-columns: 1fr;
|
|
161
|
+
row-gap: 2rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.topline_profile_fieldCont {
|
|
165
|
+
margin-top: 1.5rem;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.topline_profile_buttonsCont {
|
|
169
|
+
margin-top: 2rem;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: row-reverse;
|
|
172
|
+
gap: 1rem;
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
.topline_c_modal_overlay {
|
|
159
176
|
position: fixed;
|
|
160
177
|
z-index: 1;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NonEmptyArray } from '../../types/helpers.js';
|
|
2
|
+
type UiLangDescriptor = {
|
|
3
|
+
code: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function getSupportedUiLangs(): NonEmptyArray<UiLangDescriptor>;
|
|
7
|
+
export declare function getDefaultUiLangForUser(): UiLangDescriptor;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const supportedUiLangs = [{
|
|
2
|
+
code: 'en',
|
|
3
|
+
name: 'English'
|
|
4
|
+
}, {
|
|
5
|
+
code: 'ru',
|
|
6
|
+
name: 'Русский'
|
|
7
|
+
}];
|
|
8
|
+
export function getSupportedUiLangs() {
|
|
9
|
+
return window.structuredClone(supportedUiLangs);
|
|
10
|
+
}
|
|
11
|
+
export function getDefaultUiLangForUser() {
|
|
12
|
+
for (const lang of window.navigator.languages) {
|
|
13
|
+
const normalizedLangCode = lang.slice(0, 2).toLowerCase();
|
|
14
|
+
const matchedDescriptor = supportedUiLangs.find(x => x.code === normalizedLangCode);
|
|
15
|
+
if (matchedDescriptor) {
|
|
16
|
+
return window.structuredClone(matchedDescriptor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return supportedUiLangs[0];
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create new beacon for a user data change operation.
|
|
3
|
+
*
|
|
4
|
+
* Saves the created beacon in the local store.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createUserDataChangeBeacon(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check whether given beacon exists in the local store.
|
|
9
|
+
*/
|
|
10
|
+
export declare function userDataChangeBeaconExists(beacon: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Remove given beacon from the local store
|
|
13
|
+
*/
|
|
14
|
+
export declare function removeUserDataChangeBeacon(beacon: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
// Local store
|
|
3
|
+
const userDataChangeBeacons = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Create new beacon for a user data change operation.
|
|
6
|
+
*
|
|
7
|
+
* Saves the created beacon in the local store.
|
|
8
|
+
*/
|
|
9
|
+
export function createUserDataChangeBeacon() {
|
|
10
|
+
const beacon = v4();
|
|
11
|
+
userDataChangeBeacons.add(beacon);
|
|
12
|
+
return beacon;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check whether given beacon exists in the local store.
|
|
16
|
+
*/
|
|
17
|
+
export function userDataChangeBeaconExists(beacon) {
|
|
18
|
+
return userDataChangeBeacons.has(beacon);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove given beacon from the local store
|
|
22
|
+
*/
|
|
23
|
+
export function removeUserDataChangeBeacon(beacon) {
|
|
24
|
+
userDataChangeBeacons.delete(beacon);
|
|
25
|
+
}
|
|
@@ -4,6 +4,9 @@ import { getGrantToken } from '../tokens/index.js';
|
|
|
4
4
|
import { logError } from '../logger/index.js';
|
|
5
5
|
import { innerToplineService, ToplineEventName } from '../topline-service/index.js';
|
|
6
6
|
import { isObject } from '../../types/helpers.js';
|
|
7
|
+
import { isToplineUser } from '../../types/data.js';
|
|
8
|
+
import { userDataChangeBeaconExists, removeUserDataChangeBeacon } from '../user-data-change-beacon/index.js';
|
|
9
|
+
import { updateUser } from '../local-db/index.js';
|
|
7
10
|
let activeSocket = null;
|
|
8
11
|
let currConnectionRetries = 0;
|
|
9
12
|
const MAX_RECONNECTION_DELAY_MS = 30000;
|
|
@@ -22,26 +25,37 @@ const closeCodes = {
|
|
|
22
25
|
};
|
|
23
26
|
/** Handle incoming message from websocket */
|
|
24
27
|
function handleSocketMessage(e) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const message = JSON.parse(e.data);
|
|
29
|
+
if (isObject(message) && 'type' in message && typeof message.type === 'string') {
|
|
30
|
+
switch (message.type) {
|
|
31
|
+
case 'logout':
|
|
32
|
+
innerToplineService.processLogout();
|
|
33
|
+
closeWebsocket();
|
|
34
|
+
break;
|
|
35
|
+
case 'sync':
|
|
36
|
+
if ('syncId' in message && typeof message.syncId === 'string') {
|
|
37
|
+
innerToplineService.emit(ToplineEventName.SyncNotification, message.syncId);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case 'user_data_change':
|
|
41
|
+
if ('beacon' in message && typeof message.beacon === 'string' && 'user' in message && isToplineUser(message.user)) {
|
|
42
|
+
if (!userDataChangeBeaconExists(message.beacon)) {
|
|
43
|
+
removeUserDataChangeBeacon(message.beacon);
|
|
44
|
+
void updateUser(message.user);
|
|
45
|
+
innerToplineService.emit(ToplineEventName.UserChange, message.user);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
/** Open websocket */
|
|
36
52
|
export function openWebsocket() {
|
|
37
|
-
|
|
53
|
+
if (activeSocket) return;
|
|
38
54
|
const wsUrl = getWebsocketUrl();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
activeSocket = socket;
|
|
44
|
-
currConnectionRetries = 0;
|
|
55
|
+
activeSocket = new window.WebSocket(wsUrl);
|
|
56
|
+
activeSocket.addEventListener('open', handleSocketOpen);
|
|
57
|
+
activeSocket.addEventListener('close', e => void handleSocketClose(e));
|
|
58
|
+
activeSocket.addEventListener('message', handleSocketMessage);
|
|
45
59
|
}
|
|
46
60
|
/** Close websocket */
|
|
47
61
|
export function closeWebsocket() {
|
|
@@ -51,6 +65,7 @@ export function closeWebsocket() {
|
|
|
51
65
|
}
|
|
52
66
|
/** Handle for websocket "open" event */
|
|
53
67
|
function handleSocketOpen() {
|
|
68
|
+
currConnectionRetries = 0;
|
|
54
69
|
this.send(JSON.stringify({
|
|
55
70
|
type: 'connection',
|
|
56
71
|
data: {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save new user data on backend.
|
|
3
|
+
*
|
|
4
|
+
* Returned Promise is resolved with:
|
|
5
|
+
*
|
|
6
|
+
* - true - if the operation succeeds;
|
|
7
|
+
* - false - if the operation fails due to any reason.
|
|
8
|
+
*/
|
|
9
|
+
export default function saveUserData(beacon: string, userName: string, uiLang: string): Promise<boolean>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getGrantToken } from '../../../../modules/tokens/index.js';
|
|
2
|
+
import { logInvariant } from '../../../../modules/logger/index.js';
|
|
3
|
+
import { getApiBaseUrl, getRequestCredentialsMode } from '../../../../modules/env/index.js';
|
|
4
|
+
import request from '../../../../modules/request/index.js';
|
|
5
|
+
import { innerToplineService } from '../../../../modules/topline-service/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Save new user data on backend.
|
|
8
|
+
*
|
|
9
|
+
* Returned Promise is resolved with:
|
|
10
|
+
*
|
|
11
|
+
* - true - if the operation succeeds;
|
|
12
|
+
* - false - if the operation fails due to any reason.
|
|
13
|
+
*/
|
|
14
|
+
export default async function saveUserData(beacon, userName, uiLang) {
|
|
15
|
+
const grantToken = getGrantToken();
|
|
16
|
+
if (!grantToken) {
|
|
17
|
+
// This situation is impossible: if a logout event is received on a websocket,
|
|
18
|
+
// this modal window is closed automatically; thus the user has no chance to start
|
|
19
|
+
// a saving operation.
|
|
20
|
+
logInvariant('Invariant: grant token must exist while changing user data');
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const url = `${getApiBaseUrl()}/private/user/update-data`;
|
|
24
|
+
const options = {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${grantToken}`,
|
|
28
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
beacon,
|
|
32
|
+
userName,
|
|
33
|
+
uiLang
|
|
34
|
+
}),
|
|
35
|
+
credentials: getRequestCredentialsMode(),
|
|
36
|
+
cache: 'no-store',
|
|
37
|
+
redirect: 'error'
|
|
38
|
+
};
|
|
39
|
+
const response = await request(url, options, {
|
|
40
|
+
maxRetries: 1
|
|
41
|
+
});
|
|
42
|
+
if (!response) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
const upgradedGrantToken = await innerToplineService.upgradeGrantToken();
|
|
47
|
+
if (!upgradedGrantToken) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return await saveUserData(beacon, userName, uiLang);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Profile } from './ui/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Profile } from './ui/index.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useState, useCallback } from 'react';
|
|
3
|
+
import Modal from '../../../shared/components/modal/index.js';
|
|
4
|
+
import Root from './root/index.js';
|
|
5
|
+
import { ToplineContext } from '../../shell/index.js';
|
|
6
|
+
var LocalStep;
|
|
7
|
+
(function (LocalStep) {
|
|
8
|
+
LocalStep["Root"] = "root";
|
|
9
|
+
LocalStep["PasswordChange"] = "password_change";
|
|
10
|
+
})(LocalStep || (LocalStep = {}));
|
|
11
|
+
export default function Profile(props) {
|
|
12
|
+
const {
|
|
13
|
+
onClose
|
|
14
|
+
} = props;
|
|
15
|
+
const {
|
|
16
|
+
user
|
|
17
|
+
} = useContext(ToplineContext);
|
|
18
|
+
const [localStep, setLocalStep] = useState(LocalStep.Root);
|
|
19
|
+
const handleSwitchToPasswordChange = useCallback(() => setLocalStep(LocalStep.PasswordChange), []);
|
|
20
|
+
const getContents = user => {
|
|
21
|
+
switch (localStep) {
|
|
22
|
+
case LocalStep.Root:
|
|
23
|
+
return _jsx(Root, {
|
|
24
|
+
user: user,
|
|
25
|
+
onChangePassword: handleSwitchToPasswordChange,
|
|
26
|
+
onCancel: onClose
|
|
27
|
+
});
|
|
28
|
+
case LocalStep.PasswordChange:
|
|
29
|
+
return _jsx("div", {
|
|
30
|
+
children: "PasswordChange"
|
|
31
|
+
});
|
|
32
|
+
default:
|
|
33
|
+
{
|
|
34
|
+
const _exhaustiveCheck = localStep;
|
|
35
|
+
return _exhaustiveCheck;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (user == null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return _jsx(Modal, {
|
|
43
|
+
title: "My profile",
|
|
44
|
+
onClose: onClose,
|
|
45
|
+
children: getContents(user)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import getInitialUserName from './get-initial-user-name.js';
|
|
2
|
+
import getInitialUiLang from './get-initial-ui-lang.js';
|
|
3
|
+
export default function hasChanges(user, currData) {
|
|
4
|
+
if (getInitialUserName(user) !== currData.userName) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (getInitialUiLang(user) !== currData.uiLang) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { ToplineUser } from '../../../../types/data.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
user: ToplineUser;
|
|
5
|
+
onChangePassword: () => unknown;
|
|
6
|
+
onCancel: () => unknown;
|
|
7
|
+
};
|
|
8
|
+
export default function Root(props: Props): JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
3
|
+
import { InputField } from '@peassoft/mnr-web-ui-kit/input-field/index.js';
|
|
4
|
+
import { Select } from '@peassoft/mnr-web-ui-kit/select/index.js';
|
|
5
|
+
import { TextButton } from '@peassoft/mnr-web-ui-kit/text-button/index.js';
|
|
6
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
7
|
+
import { ErrorMessage } from '@peassoft/mnr-web-ui-kit/error-message/index.js';
|
|
8
|
+
import { createUserDataChangeBeacon } from '../../../../modules/user-data-change-beacon/index.js';
|
|
9
|
+
import { getSupportedUiLangs } from '../../../../modules/ui-lang/index.js';
|
|
10
|
+
import { updateUser } from '../../../../modules/local-db/index.js';
|
|
11
|
+
import { innerToplineService, ToplineEventName } from '../../../../modules/topline-service/index.js';
|
|
12
|
+
import saveUserData from '../../actions/save-user-data/index.js';
|
|
13
|
+
import getInitialUserName from './get-initial-user-name.js';
|
|
14
|
+
import getInitialUiLang from './get-initial-ui-lang.js';
|
|
15
|
+
import hasChanges from './has-changes.js';
|
|
16
|
+
export default function Root(props) {
|
|
17
|
+
const {
|
|
18
|
+
user,
|
|
19
|
+
onChangePassword,
|
|
20
|
+
onCancel
|
|
21
|
+
} = props;
|
|
22
|
+
const [userName, setUserName] = useState(getInitialUserName(user));
|
|
23
|
+
const [uiLang, setUiLang] = useState(getInitialUiLang(user));
|
|
24
|
+
const [isInProgress, setIsInProgress] = useState(false);
|
|
25
|
+
const [hasSavingError, setHasSavingError] = useState(false);
|
|
26
|
+
const uiLangOptions = useMemo(() => getSupportedUiLangs().map(langDescriptor => ({
|
|
27
|
+
id: langDescriptor.code,
|
|
28
|
+
value: langDescriptor.name
|
|
29
|
+
})), []);
|
|
30
|
+
const noop = useCallback(() => {}, []);
|
|
31
|
+
const handleUserNameChange = useCallback(val => setUserName(val), []);
|
|
32
|
+
const handleUiLangChange = useCallback(val => setUiLang(val), []);
|
|
33
|
+
const handleChangePasswordClick = useCallback(() => onChangePassword(), [onChangePassword]);
|
|
34
|
+
const handleSave = useCallback(async () => {
|
|
35
|
+
if (hasChanges(user, {
|
|
36
|
+
userName,
|
|
37
|
+
uiLang
|
|
38
|
+
})) {
|
|
39
|
+
setIsInProgress(true);
|
|
40
|
+
setHasSavingError(false);
|
|
41
|
+
const beacon = createUserDataChangeBeacon();
|
|
42
|
+
const savingResult = await saveUserData(beacon, userName, uiLang);
|
|
43
|
+
if (!savingResult) {
|
|
44
|
+
setIsInProgress(false);
|
|
45
|
+
setHasSavingError(true);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const updatedUser = {
|
|
49
|
+
...user,
|
|
50
|
+
userName,
|
|
51
|
+
uiLang
|
|
52
|
+
};
|
|
53
|
+
void updateUser(updatedUser);
|
|
54
|
+
innerToplineService.emit(ToplineEventName.UserChange, updatedUser);
|
|
55
|
+
onCancel();
|
|
56
|
+
}
|
|
57
|
+
}, [user, userName, uiLang, onCancel]);
|
|
58
|
+
return _jsxs(_Fragment, {
|
|
59
|
+
children: [_jsx(InputField, {
|
|
60
|
+
label: "Your email address. Cannot be changed.",
|
|
61
|
+
value: user.id,
|
|
62
|
+
disabled: true,
|
|
63
|
+
onChange: noop
|
|
64
|
+
}), _jsx(InputField, {
|
|
65
|
+
label: "Your name",
|
|
66
|
+
value: userName,
|
|
67
|
+
onChange: handleUserNameChange
|
|
68
|
+
}), _jsx(Select, {
|
|
69
|
+
label: "Select your preferred language for user interface",
|
|
70
|
+
items: uiLangOptions,
|
|
71
|
+
selectedItemId: uiLang,
|
|
72
|
+
onChange: handleUiLangChange
|
|
73
|
+
}), _jsx("div", {
|
|
74
|
+
className: 'topline_profile_fieldCont',
|
|
75
|
+
children: _jsx(TextButton, {
|
|
76
|
+
label: "Change my password",
|
|
77
|
+
onClick: handleChangePasswordClick
|
|
78
|
+
})
|
|
79
|
+
}), !!hasSavingError && _jsx(ErrorMessage, {
|
|
80
|
+
variant: 'standalone',
|
|
81
|
+
children: "An error occurred while saving changes. Check your internet connection and/or try again."
|
|
82
|
+
}), _jsxs("div", {
|
|
83
|
+
className: 'topline_profile_buttonsCont',
|
|
84
|
+
children: [_jsx(Button, {
|
|
85
|
+
label: "Save",
|
|
86
|
+
isInProgress: isInProgress,
|
|
87
|
+
onClick: handleSave
|
|
88
|
+
}), _jsx(Button, {
|
|
89
|
+
label: "Cancel",
|
|
90
|
+
variant: 'secondary',
|
|
91
|
+
onClick: onCancel
|
|
92
|
+
})]
|
|
93
|
+
})]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -8,6 +8,7 @@ import Alternative from '../../../../shared/components/alternative/index.js';
|
|
|
8
8
|
import { Login } from '../../../login/index.js';
|
|
9
9
|
import { Signup } from '../../../signup/index.js';
|
|
10
10
|
import { PasswordRecovery } from '../../../password-recovery/index.js';
|
|
11
|
+
import { Profile } from '../../../profile/index.js';
|
|
11
12
|
import ToplineContext from '../../context.js';
|
|
12
13
|
import { globalRefs } from '../../../../modules/focus-marshal/index.js';
|
|
13
14
|
import { initDb, getAllData } from '../../../../modules/local-db/index.js';
|
|
@@ -47,7 +48,7 @@ export default function Shell() {
|
|
|
47
48
|
setModalWindowOpen('signup');
|
|
48
49
|
break;
|
|
49
50
|
case 'profile':
|
|
50
|
-
|
|
51
|
+
setModalWindowOpen('profile');
|
|
51
52
|
break;
|
|
52
53
|
case 'logout':
|
|
53
54
|
handleUserChange(null);
|
|
@@ -93,7 +94,10 @@ export default function Shell() {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
void initData();
|
|
96
|
-
},
|
|
97
|
+
},
|
|
98
|
+
// We need this effect to run only once - on initial mount
|
|
99
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
100
|
+
[]);
|
|
97
101
|
useEffect(() => {
|
|
98
102
|
if (isUserMenuOpen) {
|
|
99
103
|
globalRefs.userMenuFirstItem.current?.focus();
|
|
@@ -114,28 +118,30 @@ export default function Shell() {
|
|
|
114
118
|
};
|
|
115
119
|
}, []);
|
|
116
120
|
useEffect(() => {
|
|
117
|
-
let websocketClosed = false;
|
|
118
121
|
let timer = null;
|
|
119
|
-
function
|
|
122
|
+
async function handler() {
|
|
120
123
|
if (timer) clearTimeout(timer);
|
|
121
124
|
if (document.visibilityState === 'visible') {
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const {
|
|
126
|
+
user
|
|
127
|
+
} = await getAllData();
|
|
128
|
+
if (user) {
|
|
129
|
+
await refreshUser(user);
|
|
124
130
|
openWebsocket();
|
|
125
131
|
}
|
|
126
132
|
} else {
|
|
127
|
-
timer = setTimeout(
|
|
128
|
-
websocketClosed = true;
|
|
129
|
-
closeWebsocket();
|
|
130
|
-
}, 30000);
|
|
133
|
+
timer = setTimeout(closeWebsocket, 30000);
|
|
131
134
|
}
|
|
132
135
|
}
|
|
136
|
+
function handleVisibilityChange() {
|
|
137
|
+
void handler();
|
|
138
|
+
}
|
|
133
139
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
134
140
|
return () => {
|
|
135
141
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
136
142
|
};
|
|
137
143
|
}, []);
|
|
138
|
-
let ModalWindow
|
|
144
|
+
let ModalWindow;
|
|
139
145
|
switch (modalWindowActive) {
|
|
140
146
|
case 'login':
|
|
141
147
|
ModalWindow = _jsx(Login, {
|
|
@@ -155,6 +161,20 @@ export default function Shell() {
|
|
|
155
161
|
ModalWindow = _jsx(PasswordRecovery, {
|
|
156
162
|
onClose: handleModalWindowClose
|
|
157
163
|
});
|
|
164
|
+
break;
|
|
165
|
+
case 'profile':
|
|
166
|
+
ModalWindow = _jsx(Profile, {
|
|
167
|
+
onClose: handleModalWindowClose
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
case null:
|
|
171
|
+
ModalWindow = null;
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
{
|
|
175
|
+
const _exhaustiveCheck = modalWindowActive;
|
|
176
|
+
return _exhaustiveCheck;
|
|
177
|
+
}
|
|
158
178
|
}
|
|
159
179
|
return _jsxs(_Fragment, {
|
|
160
180
|
children: [_jsxs("div", {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NonEmptyArray } from '../../types/helpers.js';
|
|
2
|
+
type UiLangDescriptor = {
|
|
3
|
+
code: string;
|
|
4
|
+
name: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function getSupportedUiLangs(): NonEmptyArray<UiLangDescriptor>;
|
|
7
|
+
export declare function getDefaultUiLangForUser(): UiLangDescriptor;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const supportedUiLangs = [{
|
|
2
|
+
code: 'en',
|
|
3
|
+
name: 'English'
|
|
4
|
+
}, {
|
|
5
|
+
code: 'ru',
|
|
6
|
+
name: 'Русский'
|
|
7
|
+
}];
|
|
8
|
+
export function getSupportedUiLangs() {
|
|
9
|
+
return window.structuredClone(supportedUiLangs);
|
|
10
|
+
}
|
|
11
|
+
export function getDefaultUiLangForUser() {
|
|
12
|
+
for (const lang of window.navigator.languages) {
|
|
13
|
+
const normalizedLangCode = lang.slice(0, 2).toLowerCase();
|
|
14
|
+
const matchedDescriptor = supportedUiLangs.find(x => x.code === normalizedLangCode);
|
|
15
|
+
if (matchedDescriptor) {
|
|
16
|
+
return window.structuredClone(matchedDescriptor);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return supportedUiLangs[0];
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create new beacon for a user data change operation.
|
|
3
|
+
*
|
|
4
|
+
* Saves the created beacon in the local store.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createUserDataChangeBeacon(): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check whether given beacon exists in the local store.
|
|
9
|
+
*/
|
|
10
|
+
export declare function userDataChangeBeaconExists(beacon: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Remove given beacon from the local store
|
|
13
|
+
*/
|
|
14
|
+
export declare function removeUserDataChangeBeacon(beacon: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
// Local store
|
|
3
|
+
const userDataChangeBeacons = new Set();
|
|
4
|
+
/**
|
|
5
|
+
* Create new beacon for a user data change operation.
|
|
6
|
+
*
|
|
7
|
+
* Saves the created beacon in the local store.
|
|
8
|
+
*/
|
|
9
|
+
export function createUserDataChangeBeacon() {
|
|
10
|
+
const beacon = v4();
|
|
11
|
+
userDataChangeBeacons.add(beacon);
|
|
12
|
+
return beacon;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check whether given beacon exists in the local store.
|
|
16
|
+
*/
|
|
17
|
+
export function userDataChangeBeaconExists(beacon) {
|
|
18
|
+
return userDataChangeBeacons.has(beacon);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Remove given beacon from the local store
|
|
22
|
+
*/
|
|
23
|
+
export function removeUserDataChangeBeacon(beacon) {
|
|
24
|
+
userDataChangeBeacons.delete(beacon);
|
|
25
|
+
}
|
|
@@ -4,6 +4,9 @@ import { getGrantToken } from '../tokens/index.js';
|
|
|
4
4
|
import { logError } from '../logger/index.js';
|
|
5
5
|
import { innerToplineService, ToplineEventName } from '../topline-service/index.js';
|
|
6
6
|
import { isObject } from '../../types/helpers.js';
|
|
7
|
+
import { isToplineUser } from '../../types/data.js';
|
|
8
|
+
import { userDataChangeBeaconExists, removeUserDataChangeBeacon } from '../user-data-change-beacon/index.js';
|
|
9
|
+
import { updateUser } from '../local-db/index.js';
|
|
7
10
|
let activeSocket = null;
|
|
8
11
|
let currConnectionRetries = 0;
|
|
9
12
|
const MAX_RECONNECTION_DELAY_MS = 30000;
|
|
@@ -22,26 +25,37 @@ const closeCodes = {
|
|
|
22
25
|
};
|
|
23
26
|
/** Handle incoming message from websocket */
|
|
24
27
|
function handleSocketMessage(e) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const message = JSON.parse(e.data);
|
|
29
|
+
if (isObject(message) && 'type' in message && typeof message.type === 'string') {
|
|
30
|
+
switch (message.type) {
|
|
31
|
+
case 'logout':
|
|
32
|
+
innerToplineService.processLogout();
|
|
33
|
+
closeWebsocket();
|
|
34
|
+
break;
|
|
35
|
+
case 'sync':
|
|
36
|
+
if ('syncId' in message && typeof message.syncId === 'string') {
|
|
37
|
+
innerToplineService.emit(ToplineEventName.SyncNotification, message.syncId);
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case 'user_data_change':
|
|
41
|
+
if ('beacon' in message && typeof message.beacon === 'string' && 'user' in message && isToplineUser(message.user)) {
|
|
42
|
+
if (!userDataChangeBeaconExists(message.beacon)) {
|
|
43
|
+
removeUserDataChangeBeacon(message.beacon);
|
|
44
|
+
void updateUser(message.user);
|
|
45
|
+
innerToplineService.emit(ToplineEventName.UserChange, message.user);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
/** Open websocket */
|
|
36
52
|
export function openWebsocket() {
|
|
37
|
-
|
|
53
|
+
if (activeSocket) return;
|
|
38
54
|
const wsUrl = getWebsocketUrl();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
activeSocket = socket;
|
|
44
|
-
currConnectionRetries = 0;
|
|
55
|
+
activeSocket = new window.WebSocket(wsUrl);
|
|
56
|
+
activeSocket.addEventListener('open', handleSocketOpen);
|
|
57
|
+
activeSocket.addEventListener('close', e => void handleSocketClose(e));
|
|
58
|
+
activeSocket.addEventListener('message', handleSocketMessage);
|
|
45
59
|
}
|
|
46
60
|
/** Close websocket */
|
|
47
61
|
export function closeWebsocket() {
|
|
@@ -51,6 +65,7 @@ export function closeWebsocket() {
|
|
|
51
65
|
}
|
|
52
66
|
/** Handle for websocket "open" event */
|
|
53
67
|
function handleSocketOpen() {
|
|
68
|
+
currConnectionRetries = 0;
|
|
54
69
|
this.send(JSON.stringify({
|
|
55
70
|
type: 'connection',
|
|
56
71
|
data: {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Save new user data on backend.
|
|
3
|
+
*
|
|
4
|
+
* Returned Promise is resolved with:
|
|
5
|
+
*
|
|
6
|
+
* - true - if the operation succeeds;
|
|
7
|
+
* - false - if the operation fails due to any reason.
|
|
8
|
+
*/
|
|
9
|
+
export default function saveUserData(beacon: string, userName: string, uiLang: string): Promise<boolean>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getGrantToken } from '../../../../modules/tokens/index.js';
|
|
2
|
+
import { logInvariant } from '../../../../modules/logger/index.js';
|
|
3
|
+
import { getApiBaseUrl, getRequestCredentialsMode } from '../../../../modules/env/index.js';
|
|
4
|
+
import request from '../../../../modules/request/index.js';
|
|
5
|
+
import { innerToplineService } from '../../../../modules/topline-service/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Save new user data on backend.
|
|
8
|
+
*
|
|
9
|
+
* Returned Promise is resolved with:
|
|
10
|
+
*
|
|
11
|
+
* - true - if the operation succeeds;
|
|
12
|
+
* - false - if the operation fails due to any reason.
|
|
13
|
+
*/
|
|
14
|
+
export default async function saveUserData(beacon, userName, uiLang) {
|
|
15
|
+
const grantToken = getGrantToken();
|
|
16
|
+
if (!grantToken) {
|
|
17
|
+
// This situation is impossible: if a logout event is received on a websocket,
|
|
18
|
+
// this modal window is closed automatically; thus the user has no chance to start
|
|
19
|
+
// a saving operation.
|
|
20
|
+
logInvariant('Invariant: grant token must exist while changing user data');
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const url = `${getApiBaseUrl()}/private/user/update-data`;
|
|
24
|
+
const options = {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${grantToken}`,
|
|
28
|
+
'Content-Type': 'application/json; charset=utf-8'
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
beacon,
|
|
32
|
+
userName,
|
|
33
|
+
uiLang
|
|
34
|
+
}),
|
|
35
|
+
credentials: getRequestCredentialsMode(),
|
|
36
|
+
cache: 'no-store',
|
|
37
|
+
redirect: 'error'
|
|
38
|
+
};
|
|
39
|
+
const response = await request(url, options, {
|
|
40
|
+
maxRetries: 1
|
|
41
|
+
});
|
|
42
|
+
if (!response) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (response.status === 401) {
|
|
46
|
+
const upgradedGrantToken = await innerToplineService.upgradeGrantToken();
|
|
47
|
+
if (!upgradedGrantToken) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return await saveUserData(beacon, userName, uiLang);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Profile } from './ui/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Profile } from './ui/index.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useState, useCallback } from 'react';
|
|
3
|
+
import Modal from '../../../shared/components/modal/index.js';
|
|
4
|
+
import Root from './root/index.js';
|
|
5
|
+
import { ToplineContext } from '../../shell/index.js';
|
|
6
|
+
var LocalStep;
|
|
7
|
+
(function (LocalStep) {
|
|
8
|
+
LocalStep["Root"] = "root";
|
|
9
|
+
LocalStep["PasswordChange"] = "password_change";
|
|
10
|
+
})(LocalStep || (LocalStep = {}));
|
|
11
|
+
export default function Profile(props) {
|
|
12
|
+
const {
|
|
13
|
+
onClose
|
|
14
|
+
} = props;
|
|
15
|
+
const {
|
|
16
|
+
user
|
|
17
|
+
} = useContext(ToplineContext);
|
|
18
|
+
const [localStep, setLocalStep] = useState(LocalStep.Root);
|
|
19
|
+
const handleSwitchToPasswordChange = useCallback(() => setLocalStep(LocalStep.PasswordChange), []);
|
|
20
|
+
const getContents = user => {
|
|
21
|
+
switch (localStep) {
|
|
22
|
+
case LocalStep.Root:
|
|
23
|
+
return _jsx(Root, {
|
|
24
|
+
user: user,
|
|
25
|
+
onChangePassword: handleSwitchToPasswordChange,
|
|
26
|
+
onCancel: onClose
|
|
27
|
+
});
|
|
28
|
+
case LocalStep.PasswordChange:
|
|
29
|
+
return _jsx("div", {
|
|
30
|
+
children: "PasswordChange"
|
|
31
|
+
});
|
|
32
|
+
default:
|
|
33
|
+
{
|
|
34
|
+
const _exhaustiveCheck = localStep;
|
|
35
|
+
return _exhaustiveCheck;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
if (user == null) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return _jsx(Modal, {
|
|
43
|
+
title: "Мой профиль",
|
|
44
|
+
onClose: onClose,
|
|
45
|
+
children: getContents(user)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import getInitialUserName from './get-initial-user-name.js';
|
|
2
|
+
import getInitialUiLang from './get-initial-ui-lang.js';
|
|
3
|
+
export default function hasChanges(user, currData) {
|
|
4
|
+
if (getInitialUserName(user) !== currData.userName) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
if (getInitialUiLang(user) !== currData.uiLang) {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
import type { ToplineUser } from '../../../../types/data.js';
|
|
3
|
+
type Props = {
|
|
4
|
+
user: ToplineUser;
|
|
5
|
+
onChangePassword: () => unknown;
|
|
6
|
+
onCancel: () => unknown;
|
|
7
|
+
};
|
|
8
|
+
export default function Root(props: Props): JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
3
|
+
import { InputField } from '@peassoft/mnr-web-ui-kit/input-field/index.js';
|
|
4
|
+
import { Select } from '@peassoft/mnr-web-ui-kit/select/index.js';
|
|
5
|
+
import { TextButton } from '@peassoft/mnr-web-ui-kit/text-button/index.js';
|
|
6
|
+
import { Button } from '@peassoft/mnr-web-ui-kit/button/index.js';
|
|
7
|
+
import { ErrorMessage } from '@peassoft/mnr-web-ui-kit/error-message/index.js';
|
|
8
|
+
import { createUserDataChangeBeacon } from '../../../../modules/user-data-change-beacon/index.js';
|
|
9
|
+
import { getSupportedUiLangs } from '../../../../modules/ui-lang/index.js';
|
|
10
|
+
import { updateUser } from '../../../../modules/local-db/index.js';
|
|
11
|
+
import { innerToplineService, ToplineEventName } from '../../../../modules/topline-service/index.js';
|
|
12
|
+
import saveUserData from '../../actions/save-user-data/index.js';
|
|
13
|
+
import getInitialUserName from './get-initial-user-name.js';
|
|
14
|
+
import getInitialUiLang from './get-initial-ui-lang.js';
|
|
15
|
+
import hasChanges from './has-changes.js';
|
|
16
|
+
export default function Root(props) {
|
|
17
|
+
const {
|
|
18
|
+
user,
|
|
19
|
+
onChangePassword,
|
|
20
|
+
onCancel
|
|
21
|
+
} = props;
|
|
22
|
+
const [userName, setUserName] = useState(getInitialUserName(user));
|
|
23
|
+
const [uiLang, setUiLang] = useState(getInitialUiLang(user));
|
|
24
|
+
const [isInProgress, setIsInProgress] = useState(false);
|
|
25
|
+
const [hasSavingError, setHasSavingError] = useState(false);
|
|
26
|
+
const uiLangOptions = useMemo(() => getSupportedUiLangs().map(langDescriptor => ({
|
|
27
|
+
id: langDescriptor.code,
|
|
28
|
+
value: langDescriptor.name
|
|
29
|
+
})), []);
|
|
30
|
+
const noop = useCallback(() => {}, []);
|
|
31
|
+
const handleUserNameChange = useCallback(val => setUserName(val), []);
|
|
32
|
+
const handleUiLangChange = useCallback(val => setUiLang(val), []);
|
|
33
|
+
const handleChangePasswordClick = useCallback(() => onChangePassword(), [onChangePassword]);
|
|
34
|
+
const handleSave = useCallback(async () => {
|
|
35
|
+
if (hasChanges(user, {
|
|
36
|
+
userName,
|
|
37
|
+
uiLang
|
|
38
|
+
})) {
|
|
39
|
+
setIsInProgress(true);
|
|
40
|
+
setHasSavingError(false);
|
|
41
|
+
const beacon = createUserDataChangeBeacon();
|
|
42
|
+
const savingResult = await saveUserData(beacon, userName, uiLang);
|
|
43
|
+
if (!savingResult) {
|
|
44
|
+
setIsInProgress(false);
|
|
45
|
+
setHasSavingError(true);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const updatedUser = {
|
|
49
|
+
...user,
|
|
50
|
+
userName,
|
|
51
|
+
uiLang
|
|
52
|
+
};
|
|
53
|
+
void updateUser(updatedUser);
|
|
54
|
+
innerToplineService.emit(ToplineEventName.UserChange, updatedUser);
|
|
55
|
+
onCancel();
|
|
56
|
+
}
|
|
57
|
+
}, [user, userName, uiLang, onCancel]);
|
|
58
|
+
return _jsxs(_Fragment, {
|
|
59
|
+
children: [_jsx(InputField, {
|
|
60
|
+
label: "Ваш email. Не может быть изменён.",
|
|
61
|
+
value: user.id,
|
|
62
|
+
disabled: true,
|
|
63
|
+
onChange: noop
|
|
64
|
+
}), _jsx(InputField, {
|
|
65
|
+
label: "Ваше имя",
|
|
66
|
+
value: userName,
|
|
67
|
+
onChange: handleUserNameChange
|
|
68
|
+
}), _jsx(Select, {
|
|
69
|
+
label: "Выберите предпочитаемый язык для пользовательского интерфейса",
|
|
70
|
+
items: uiLangOptions,
|
|
71
|
+
selectedItemId: uiLang,
|
|
72
|
+
onChange: handleUiLangChange
|
|
73
|
+
}), _jsx("div", {
|
|
74
|
+
className: 'topline_profile_fieldCont',
|
|
75
|
+
children: _jsx(TextButton, {
|
|
76
|
+
label: "Изменить мой пароль",
|
|
77
|
+
onClick: handleChangePasswordClick
|
|
78
|
+
})
|
|
79
|
+
}), !!hasSavingError && _jsx(ErrorMessage, {
|
|
80
|
+
variant: 'standalone',
|
|
81
|
+
children: "При сохранении изменений произошла ошибка. Проверьте ваше подключение к интернету и/или попробуйте ещё раз."
|
|
82
|
+
}), _jsxs("div", {
|
|
83
|
+
className: 'topline_profile_buttonsCont',
|
|
84
|
+
children: [_jsx(Button, {
|
|
85
|
+
label: "Сохранить",
|
|
86
|
+
isInProgress: isInProgress,
|
|
87
|
+
onClick: handleSave
|
|
88
|
+
}), _jsx(Button, {
|
|
89
|
+
label: "Отменить",
|
|
90
|
+
variant: 'secondary',
|
|
91
|
+
onClick: onCancel
|
|
92
|
+
})]
|
|
93
|
+
})]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -8,6 +8,7 @@ import Alternative from '../../../../shared/components/alternative/index.js';
|
|
|
8
8
|
import { Login } from '../../../login/index.js';
|
|
9
9
|
import { Signup } from '../../../signup/index.js';
|
|
10
10
|
import { PasswordRecovery } from '../../../password-recovery/index.js';
|
|
11
|
+
import { Profile } from '../../../profile/index.js';
|
|
11
12
|
import ToplineContext from '../../context.js';
|
|
12
13
|
import { globalRefs } from '../../../../modules/focus-marshal/index.js';
|
|
13
14
|
import { initDb, getAllData } from '../../../../modules/local-db/index.js';
|
|
@@ -47,7 +48,7 @@ export default function Shell() {
|
|
|
47
48
|
setModalWindowOpen('signup');
|
|
48
49
|
break;
|
|
49
50
|
case 'profile':
|
|
50
|
-
|
|
51
|
+
setModalWindowOpen('profile');
|
|
51
52
|
break;
|
|
52
53
|
case 'logout':
|
|
53
54
|
handleUserChange(null);
|
|
@@ -93,7 +94,10 @@ export default function Shell() {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
void initData();
|
|
96
|
-
},
|
|
97
|
+
},
|
|
98
|
+
// We need this effect to run only once - on initial mount
|
|
99
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
100
|
+
[]);
|
|
97
101
|
useEffect(() => {
|
|
98
102
|
if (isUserMenuOpen) {
|
|
99
103
|
globalRefs.userMenuFirstItem.current?.focus();
|
|
@@ -114,28 +118,30 @@ export default function Shell() {
|
|
|
114
118
|
};
|
|
115
119
|
}, []);
|
|
116
120
|
useEffect(() => {
|
|
117
|
-
let websocketClosed = false;
|
|
118
121
|
let timer = null;
|
|
119
|
-
function
|
|
122
|
+
async function handler() {
|
|
120
123
|
if (timer) clearTimeout(timer);
|
|
121
124
|
if (document.visibilityState === 'visible') {
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const {
|
|
126
|
+
user
|
|
127
|
+
} = await getAllData();
|
|
128
|
+
if (user) {
|
|
129
|
+
await refreshUser(user);
|
|
124
130
|
openWebsocket();
|
|
125
131
|
}
|
|
126
132
|
} else {
|
|
127
|
-
timer = setTimeout(
|
|
128
|
-
websocketClosed = true;
|
|
129
|
-
closeWebsocket();
|
|
130
|
-
}, 30000);
|
|
133
|
+
timer = setTimeout(closeWebsocket, 30000);
|
|
131
134
|
}
|
|
132
135
|
}
|
|
136
|
+
function handleVisibilityChange() {
|
|
137
|
+
void handler();
|
|
138
|
+
}
|
|
133
139
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
134
140
|
return () => {
|
|
135
141
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
136
142
|
};
|
|
137
143
|
}, []);
|
|
138
|
-
let ModalWindow
|
|
144
|
+
let ModalWindow;
|
|
139
145
|
switch (modalWindowActive) {
|
|
140
146
|
case 'login':
|
|
141
147
|
ModalWindow = _jsx(Login, {
|
|
@@ -155,6 +161,20 @@ export default function Shell() {
|
|
|
155
161
|
ModalWindow = _jsx(PasswordRecovery, {
|
|
156
162
|
onClose: handleModalWindowClose
|
|
157
163
|
});
|
|
164
|
+
break;
|
|
165
|
+
case 'profile':
|
|
166
|
+
ModalWindow = _jsx(Profile, {
|
|
167
|
+
onClose: handleModalWindowClose
|
|
168
|
+
});
|
|
169
|
+
break;
|
|
170
|
+
case null:
|
|
171
|
+
ModalWindow = null;
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
{
|
|
175
|
+
const _exhaustiveCheck = modalWindowActive;
|
|
176
|
+
return _exhaustiveCheck;
|
|
177
|
+
}
|
|
158
178
|
}
|
|
159
179
|
return _jsxs(_Fragment, {
|
|
160
180
|
children: [_jsxs("div", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peassoft/mnr-web-topline",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Peassoft Topline widget for mem'n'rev web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@memnrev/dict-replacer": "^0.1.0",
|
|
31
31
|
"@memnrev/eslint-v9-config": "^0.1.1",
|
|
32
32
|
"@microsoft/api-extractor": "^7.38.0",
|
|
33
|
-
"@peassoft/mnr-web-ui-kit": "^0.1.
|
|
33
|
+
"@peassoft/mnr-web-ui-kit": "^0.1.2",
|
|
34
34
|
"@types/jest": "^29.5.4",
|
|
35
35
|
"@types/md5": "^2.3.3",
|
|
36
36
|
"@types/react": "^19.0.2",
|
|
@@ -66,7 +66,8 @@
|
|
|
66
66
|
"email-validator": "^2.0.4",
|
|
67
67
|
"md5": "^2.3.0",
|
|
68
68
|
"react-error-boundary": "^5.0.0",
|
|
69
|
-
"stampit": "^4.3.2"
|
|
69
|
+
"stampit": "^4.3.2",
|
|
70
|
+
"uuid": "^11.1.0"
|
|
70
71
|
},
|
|
71
72
|
"peerDependencies": {
|
|
72
73
|
"react": ">= 19 < 20",
|