Merge branch 'master' into refactoring/di

# Conflicts:
#	common/ASC.Api.Core/Auth/ConfirmAuthHandler.cs
#	common/ASC.Core.Common/Security/EmailValidationKeyProvider.cs
#	web/ASC.Web.Api/Controllers/PortalController.cs
#	web/ASC.Web.Api/Controllers/SettingsController.cs
#	web/ASC.Web.Core/Notify/StudioNotifyService.cs
This commit is contained in:
pavelbannov 2019-10-30 11:45:55 +03:00
commit 0bbc383266
200 changed files with 9967 additions and 3009 deletions

View File

@ -0,0 +1,127 @@
FROM ubuntu:18.04
ARG RELEASE_DATE="2016-06-21"
ARG RELEASE_DATE_SIGN=""
ARG VERSION="8.9.0.190"
ARG SOURCE_REPO_URL="deb http://static.teamlab.com.s3.amazonaws.com/repo/debian squeeze main"
ARG DEBIAN_FRONTEND=noninteractive
LABEL onlyoffice.community.release-date="${RELEASE_DATE}" \
onlyoffice.community.version="${VERSION}" \
maintainer="Ascensio System SIA <support@onlyoffice.com>"
ENV LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8
RUN apt-get -y update && \
apt-get -yq install gnupg2 ca-certificates && \
apt-get install -yq sudo locales && \
addgroup --system --gid 107 onlyoffice && \
adduser -uid 104 --quiet --home /var/www/onlyoffice --system --gid 107 onlyoffice && \
addgroup --system --gid 104 elasticsearch && \
adduser -uid 103 --quiet --home /nonexistent --system --gid 104 elasticsearch && \
locale-gen en_US.UTF-8 && \
apt-get -y update && \
apt-get install -yq software-properties-common wget curl cron rsyslog && \
wget http://nginx.org/keys/nginx_signing.key && \
apt-key add nginx_signing.key && \
echo "deb http://nginx.org/packages/mainline/ubuntu/ bionic nginx" >> /etc/apt/sources.list.d/nginx.list && \
echo "deb-src http://nginx.org/packages/mainline/ubuntu/ bionic nginx" >> /etc/apt/sources.list.d/nginx.list && \
apt-get install -yq openjdk-8-jre-headless && \
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | apt-key add - && \
apt-get install -yq apt-transport-https && \
echo "deb https://artifacts.elastic.co/packages/6.x/apt stable main" | tee -a /etc/apt/sources.list.d/elastic-6.x.list && \
apt-get -y update && \
apt-get install -yq elasticsearch=6.5.0 && \
add-apt-repository -y ppa:certbot/certbot && \
add-apt-repository -y ppa:jonathonf/ffmpeg-4 && \
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - && \
apt-get install -y nodejs && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \
wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb && \
dpkg -i packages-microsoft-prod.deb && \
apt-get -y update && \
apt-get install -yq nginx && \
cd ~ && \
wget http://www-us.apache.org/dist/kafka/2.2.1/kafka_2.12-2.2.1.tgz && \
tar xzf kafka_2.12-2.2.1.tgz && \
rm kafka_2.12-2.2.1.tgz && \
echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d && \
apt-get install -yq dumb-init python-certbot-nginx htop nano dnsutils python3-pip multiarch-support iproute2 ffmpeg jq git yarn dotnet-sdk-3.0 supervisor mysql-client mysql-server
RUN git clone https://github.com/ONLYOFFICE/CommunityServer-AspNetCore.git /app/onlyoffice/src/
RUN cd /app/onlyoffice/src/ && \
yarn install --cwd web/ASC.Web.Components --frozen-lockfile > build/ASC.Web.Components.log && \
npm run build --prefix web/ASC.Web.Components && \
yarn pack --cwd web/ASC.Web.Components
RUN cd /app/onlyoffice/src/ && \
npm run build:storybook --prefix web/ASC.Web.Components && \
mkdir -p /var/www/story/ && \
cp -Rf web/ASC.Web.Components/storybook-static/* /var/www/story/
RUN cd /app/onlyoffice/src/ && \
component=$(ls web/ASC.Web.Components/asc-web-components-v1.*.tgz) && \
yarn add ../../$component --cwd web/ASC.Web.Client --cache-folder ../../yarn && \
yarn install --cwd web/ASC.Web.Client --frozen-lockfile || (cd web/ASC.Web.Client && npm i && cd ../../) && \
npm run build --prefix web/ASC.Web.Client && \
rm -rf /var/www/studio/client/* && \
mkdir -p /var/www/studio/client && \
cp -rf web/ASC.Web.Client/build/* /var/www/studio/client
RUN cd /app/onlyoffice/src/ && \
component=$(ls web/ASC.Web.Components/asc-web-components-v1.*.tgz) && \
yarn add ../../../$component --cwd products/ASC.People/Client --cache-folder ../../../yarn && \
yarn install --cwd products/ASC.People/Client --frozen-lockfile || (cd products/ASC.People/Client && npm i && cd ../../../) && \
npm run build --prefix products/ASC.People/Client && \
mkdir -p /var/www/products/ASC.People/client && \
cp -Rf products/ASC.People/Client/build/* /var/www/products/ASC.People/client && \
mkdir -p /var/www/products/ASC.People/client/products/people
RUN cd /app/onlyoffice/src/ && \
rm -f /etc/nginx/conf.d/* && \
cp -rf config/nginx/onlyoffice*.conf /etc/nginx/conf.d/ && \
mkdir -p /app/onlyoffice/config/ && cp -rf config/* /app/onlyoffice/config/ && \
sed -e 's/#//' -i /etc/nginx/conf.d/onlyoffice.conf && \
cd products/ASC.People/Server && \
dotnet -d publish -o /var/www/products/ASC.People/server && \
cd ../../../ && \
cd web/ASC.Web.Api && \
dotnet -d publish -o /var/www/studio/api && \
cd ../../ && \
cd web/ASC.Web.Studio && \
dotnet -d publish -o /var/www/studio/server && \
cd ../../ && \
cd common/services/ASC.Notify && \
dotnet -d publish -o /var/www/services/notify && \
cd ../../../ && \
cd common/services/ASC.Studio.Notify && \
dotnet -d publish -o /var/www/services/studio.notify
COPY config/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf
COPY config/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
RUN sed -i 's/172.18.0.5/localhost/' /app/onlyoffice/config/appsettings.test.json
RUN mkdir -p /var/mysqld/ && \
chown -R mysql:mysql /var/lib/mysql /var/run/mysqld /var/mysqld/ && \
service mysql start && \
mysql -e "CREATE DATABASE IF NOT EXISTS onlyoffice CHARACTER SET utf8 COLLATE 'utf8_general_ci'" && \
mysql -D "onlyoffice" < /app/onlyoffice/src/sql/app.sql && \
mysql -D "onlyoffice" < /app/onlyoffice/src/sql/app.data.sql && \
mysql -D "onlyoffice" -e 'CREATE USER IF NOT EXISTS "onlyoffice_user"@"localhost" IDENTIFIED WITH mysql_native_password BY "onlyoffice_pass";' && \
mysql -D "onlyoffice" -e 'GRANT ALL PRIVILEGES ON *.* TO 'onlyoffice_user'@'localhost' IDENTIFIED BY "onlyoffice_pass";' && \
mysql -D "onlyoffice" -e 'UPDATE core_user SET email = "paul.bannov@gmail.com";' && \
mysql -D "onlyoffice" -e 'UPDATE core_usersecurity SET pwdhash = "vLFfghR5tNV3K9DKhmwArV+SbjWAcgZZzIDTnJ0JgCo=", pwdhashsha512 = "USubvPlB+ogq0Q1trcSupg==";' && \
service mysql stop
RUN rm -rf /var/lib/apt/lists/*
VOLUME /var/lib/mysql
EXPOSE 80 443 8092 8081
ENTRYPOINT ["/usr/bin/supervisord", "--"]

View File

@ -0,0 +1,7 @@
[mysqld]
sql_mode = 'NO_ENGINE_SUBSTITUTION'
max_connections = 1000
max_allowed_packet = 1048576000
group_concat_max_len = 2048
log-error = /var/log/mysql/error.log

View File

@ -0,0 +1,64 @@
[supervisord]
nodaemon=true
[program:mysqld]
command=/usr/bin/pidproxy /var/mysqld/mysqld.pid /usr/bin/mysqld_safe --pid-file=/var/mysqld/mysqld.pid
autostart=true
autorestart=true
environment=PATH=/usr/local/sbin:/usr/bin:/bin:/usr/local/bin
user=mysql
[program:kafka]
directory=/root/kafka_2.12-2.2.1/
command=/root/kafka_2.12-2.2.1/bin/kafka-server-start.sh /root/kafka_2.12-2.2.1/config/server.properties
autostart=true
autorestart=true
[program:kafka_zookeeper]
directory=/root/kafka_2.12-2.2.1/
command=/root/kafka_2.12-2.2.1/bin/zookeeper-server-start.sh /root/kafka_2.12-2.2.1/config/zookeeper.properties
autostart=true
autorestart=true
[program:api]
directory=/var/www/studio/api/
command=dotnet ASC.Web.Api.dll --urls=http://0.0.0.0:5000 --pathToConf=/app/onlyoffice/config/ --$STORAGE_ROOT=/app/onlyoffice/data/ --ENVIRONMENT=test
autostart=true
autorestart=true
[program:notify]
directory=/var/www/services/notify/
command=dotnet ASC.Notify.dll --urls=http://0.0.0.0:5005 --pathToConf=/app/onlyoffice/config/ --$STORAGE_ROOT=/app/onlyoffice/data/ --ENVIRONMENT=test
autostart=true
autorestart=true
[program:studio_notify]
directory=/var/www/services/studio.notify/
command=dotnet ASC.Studio.Notify.dll --urls=http://0.0.0.0:5006 --pathToConf=/app/onlyoffice/config/ --$STORAGE_ROOT=/app/onlyoffice/data/ --ENVIRONMENT=test
autostart=true
autorestart=true
[program:people]
directory=/var/www/products/ASC.People/server/
command=dotnet ASC.People.dll --urls=http://0.0.0.0:5004 --pathToConf=/app/onlyoffice/config/ --$STORAGE_ROOT=/app/onlyoffice/data/ --ENVIRONMENT=test
autostart=true
autorestart=true
[program:studio]
directory=/var/www/studio/server/
command=dotnet ASC.Web.Studio.dll --urls=http://0.0.0.0:5003 --pathToConf=/app/onlyoffice/config/ --$STORAGE_ROOT=/app/onlyoffice/data/ --ENVIRONMENT=test
autostart=true
autorestart=true
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
startretries=5
numprocs=1
startsecs=0
process_name=%(program_name)s_%(process_num)02d
stderr_logfile=/var/log/supervisor/%(program_name)s_stderr.log
stderr_logfile_maxbytes=10MB
stdout_logfile=/var/log/supervisor/%(program_name)s_stdout.log
stdout_logfile_maxbytes=10MB

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
@ -8,8 +9,6 @@ using System.Threading.Tasks;
using ASC.Core;
using ASC.Security.Cryptography;
using ASC.Web.Studio.Core;
using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -56,33 +55,46 @@ namespace ASC.Api.Core.Auth
{
var emailValidationKeyModel = EmailValidationKeyModel.FromRequest(Context.Request);
if (SecurityContext.IsAuthenticated && emailValidationKeyModel.Type != ConfirmType.EmailChange)
if (!emailValidationKeyModel.Type.HasValue)
{
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)));
return SecurityContext.IsAuthenticated
? Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)))
: Task.FromResult(AuthenticateResult.Fail(new AuthenticationException(HttpStatusCode.Unauthorized.ToString())));
}
var checkKeyResult = emailValidationKeyModel.Validate(EmailValidationKeyProvider, AuthContext, TenantManager, AuthManager);
EmailValidationKeyProvider.ValidationResult checkKeyResult;
try
{
checkKeyResult = emailValidationKeyModel.Validate(EmailValidationKeyProvider, AuthContext, TenantManager, UserManager, AuthManager);
}
catch (ArgumentNullException)
{
checkKeyResult = EmailValidationKeyProvider.ValidationResult.Invalid;
}
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Role, emailValidationKeyModel.Type.ToString())
};
if (!SecurityContext.IsAuthenticated)
if (checkKeyResult == EmailValidationKeyProvider.ValidationResult.Ok)
{
if (emailValidationKeyModel.UiD.HasValue)
if (!SecurityContext.IsAuthenticated)
{
SecurityContext.AuthenticateMe(emailValidationKeyModel.UiD.Value, claims);
if (emailValidationKeyModel.UiD.HasValue && !emailValidationKeyModel.UiD.Equals(Guid.Empty))
{
SecurityContext.AuthenticateMe(emailValidationKeyModel.UiD.Value, claims);
}
else
{
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem, claims);
}
}
else
{
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem, claims);
SecurityContext.AuthenticateMe(SecurityContext.CurrentAccount, claims);
}
}
else
{
SecurityContext.AuthenticateMe(SecurityContext.CurrentAccount, claims);
}
var result = checkKeyResult switch
{

View File

@ -151,20 +151,20 @@ namespace ASC.Security.Cryptography
public EmployeeType? EmplType { get; set; }
public string Email { get; set; }
public Guid? UiD { get; set; }
public ConfirmType Type { get; set; }
public ConfirmType? Type { get; set; }
public int? P { get; set; }
public ValidationResult Validate(EmailValidationKeyProvider provider, AuthContext authContext, TenantManager tenantManager, AuthManager authentication)
public ValidationResult Validate(EmailValidationKeyProvider provider, AuthContext authContext, TenantManager tenantManager, UserManager userManager, AuthManager authentication)
{
ValidationResult checkKeyResult;
switch (Type)
{
case ConfirmType.EmpInvite:
checkKeyResult = provider.ValidateEmailKey(Email + Type + EmplType, Key, provider.ValidInterval);
checkKeyResult = provider.ValidateEmailKey(Email + Type + (int)EmplType, Key, provider.ValidInterval);
break;
case ConfirmType.LinkInvite:
checkKeyResult = provider.ValidateEmailKey(Type.ToString() + EmplType.ToString(), Key, provider.ValidInterval);
checkKeyResult = provider.ValidateEmailKey(Type.ToString() + (int)EmplType, Key, provider.ValidInterval);
break;
case ConfirmType.EmailChange:
checkKeyResult = provider.ValidateEmailKey(Email + Type + authContext.CurrentAccount.ID, Key, provider.ValidInterval);
@ -181,6 +181,17 @@ namespace ASC.Security.Cryptography
checkKeyResult = provider.ValidateEmailKey(Email + Type + (string.IsNullOrEmpty(hash) ? string.Empty : Hasher.Base64Hash(hash)) + UiD, Key, provider.ValidInterval);
break;
case ConfirmType.Activation:
checkKeyResult = provider.ValidateEmailKey(Email + Type + UiD, Key, provider.ValidInterval);
break;
case ConfirmType.ProfileRemove:
// validate UiD
if (P == 1)
{
var user = userManager.GetUsers(UiD.GetValueOrDefault());
if (user == null || user.Status == EmployeeStatus.Terminated || authContext.IsAuthenticated && authContext.CurrentAccount.ID != UiD)
return ValidationResult.Invalid;
}
checkKeyResult = provider.ValidateEmailKey(Email + Type + UiD, Key, provider.ValidInterval);
break;
default:
@ -196,7 +207,12 @@ namespace ASC.Security.Cryptography
var Request = QueryHelpers.ParseQuery(httpRequest.Headers["confirm"]);
_ = Request.TryGetValue("type", out var type);
_ = Enum.TryParse<ConfirmType>(type, out var confirmType);
ConfirmType? cType = null;
if (Enum.TryParse<ConfirmType>(type, out var confirmType))
{
cType = confirmType;
}
_ = Request.TryGetValue("key", out var key);
@ -213,7 +229,7 @@ namespace ASC.Security.Cryptography
return new EmailValidationKeyModel
{
Key = key,
Type = confirmType,
Type = cType,
Email = _email,
EmplType = employeeType,
UiD = userId,
@ -221,7 +237,7 @@ namespace ASC.Security.Cryptography
};
}
public void Deconstruct(out string key, out string email, out EmployeeType? employeeType, out Guid? userId, out ConfirmType confirmType, out int? p)
public void Deconstruct(out string key, out string email, out EmployeeType? employeeType, out Guid? userId, out ConfirmType? confirmType, out int? p)
=> (key, email, employeeType, userId, confirmType, p) = (Key, Email, EmplType, UiD, Type, P);
}
}

View File

@ -1,15 +1,15 @@
syntax = "proto3";
import "google/protobuf/wrappers.proto";
package ASC.Core;
message AzRecordCache {
string SubjectId = 1;
google.protobuf.StringValue SubjectId = 1;
string ActionId = 2;
google.protobuf.StringValue ActionId = 2;
string ObjectId = 3;
google.protobuf.StringValue ObjectId = 3;
string Reaction = 4;
google.protobuf.StringValue Reaction = 4;
int32 Tenant = 5;
}

View File

@ -9,7 +9,7 @@
"ConnectionStrings": {
"default": {
"name": "default",
"connectionString": "Server=172.18.0.3;Port=3306;Database=onlyoffice;User ID=onlyoffice_user;Password=onlyoffice_pass;Pooling=true;Character Set=utf8;AutoEnlist=false;SSL Mode=none",
"connectionString": "Server=172.18.0.5;Port=3306;Database=onlyoffice;User ID=onlyoffice_user;Password=onlyoffice_pass;Pooling=true;Character Set=utf8;AutoEnlist=false;SSL Mode=none",
"providerName": "MySql.Data.MySqlClient"
}
},

View File

@ -88,10 +88,11 @@ class ArticleBodyContent extends React.Component {
render() {
const { data, selectedKeys } = this.props;
console.log("PeopleTreeMenu", this.props);
//console.log("PeopleTreeMenu", this.props);
return (
<TreeMenu
className="people-tree-menu"
checkable={false}
draggable={false}
disabled={false}

View File

@ -1,11 +1,17 @@
import React from 'react';
import { connect } from 'react-redux';
import { Text } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { getCurrentModule } from '../../../store/auth/selectors';
const ArticleHeaderContent = () => {
const { t } = useTranslation('translation', { i18n });
return <Text.MenuHeader>{t('People')}</Text.MenuHeader>;
const ArticleHeaderContent = ({currentModuleName}) => {
return <Text.MenuHeader>{currentModuleName}</Text.MenuHeader>;
}
export default ArticleHeaderContent;
const mapStateToProps = (state) => {
const currentModule = getCurrentModule(state.auth.modules, state.auth.settings.currentProductId);
return {
currentModuleName: (currentModule && currentModule.title) || ""
}
}
export default connect(mapStateToProps)(ArticleHeaderContent);

View File

@ -86,21 +86,25 @@ class PureArticleMainButtonContent extends React.Component {
};
};
const mapStateToProps = (state) => {
return {
isAdmin: isAdmin(state.auth.user),
settings: state.auth.settings
}
}
const ArticleMainButtonContentContainer = withTranslation()(PureArticleMainButtonContent);
const ArticleMainButtonContent = (props) => <I18nextProvider i18n={i18n}><ArticleMainButtonContentContainer {...props} /></I18nextProvider>;
const ArticleMainButtonContent = (props) => {
const { language } = props;
i18n.changeLanguage(language);
return (<I18nextProvider i18n={i18n}><ArticleMainButtonContentContainer {...props} /></I18nextProvider>);
};
ArticleMainButtonContent.propTypes = {
isAdmin: PropTypes.bool.isRequired,
history: PropTypes.object.isRequired
};
const mapStateToProps = (state) => {
return {
isAdmin: isAdmin(state.auth.user),
language: state.auth.user.cultureName || state.auth.settings.culture,
settings: state.auth.settings
}
}
export default connect(mapStateToProps)(withRouter(ArticleMainButtonContent));

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -49,7 +52,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -2,10 +2,7 @@
"InviteLinkTitle": "Invitation link",
"ImportPeople": "Import people",
"Actions": "Actions",
"People": "People",
"LblInviteAgain": "Invite again",
"CustomNewEmployee": "New {{typeUser, lowercase}}",
"CustomNewGuest": "New {{typeGuest, lowercase}}",
"CustomNewDepartment": "New {{department, lowercase}}"

View File

@ -0,0 +1,9 @@
{
"InviteLinkTitle": "Пригласительная ссылка",
"ImportPeople": "Импортировать людей",
"Actions": "Действия",
"LblInviteAgain": "Отправить приглашение ещё раз",
"CustomNewEmployee": "Новый {{typeUser, lowercase}}",
"CustomNewGuest": "Новый {{typeGuest, lowercase}}",
"CustomNewDepartment": "Новый {{department, lowercase}}"
}

View File

@ -32,6 +32,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -6,6 +6,7 @@ import { Layout, Toast } from 'asc-web-components';
import { logout } from '../../store/auth/actions';
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from "./i18n";
import { isAdmin } from "../../store/auth/selectors";
class PurePeopleLayout extends React.Component {
shouldComponentUpdate(nextProps) {
@ -66,7 +67,23 @@ class PurePeopleLayout extends React.Component {
};
const getAvailableModules = (modules) => {
const getAvailableModules = (modules, currentUser) => {
const isUserAdmin = isAdmin(currentUser);
const customModules = isUserAdmin ? [
{
separator: true,
id: "nav-separator-2"
},
{
id: 'settings',
title: 'Settings',
iconName: "SettingsIcon",
notifications: 0,
url: '/settings',
onClick: () => window.open('/settings', "_self"),
onBadgeClick: e => console.log("SettingsIconBadge Clicked", e)
}] : [];
const separator = { separator: true, id: 'nav-separator-1' };
const products = modules.map(product => {
return {
@ -80,22 +97,27 @@ const getAvailableModules = (modules) => {
};
}) || [];
return products.length ? [separator, ...products] : products;
return products.length ? [separator, ...products, ...customModules] : products;
};
function mapStateToProps(state) {
return {
hasChanges: state.auth.isAuthenticated && state.auth.isLoaded,
availableModules: getAvailableModules(state.auth.modules),
availableModules: getAvailableModules(state.auth.modules, state.auth.user),
currentUser: state.auth.user,
currentModuleId: state.auth.settings.currentProductId,
settings: state.auth.settings
settings: state.auth.settings,
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
const PeopleLayoutContainer = withTranslation()(PurePeopleLayout);
const PeopleLayout = (props) => <I18nextProvider i18n={i18n}><PeopleLayoutContainer {...props} /></I18nextProvider>;
const PeopleLayout = (props) => {
const { language } = props;
i18n.changeLanguage(language);
return (<I18nextProvider i18n={i18n}><PeopleLayoutContainer {...props} /></I18nextProvider>);
};
PeopleLayout.propTypes = {
logout: PropTypes.func.isRequired

View File

@ -0,0 +1,5 @@
{
"Profile": "Профиль",
"AboutCompanyTitle": "О программе",
"LogoutButton": "Выйти"
}

View File

@ -1,201 +1,208 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Link,
Checkbox,
Button,
Textarea,
Text
toastr,
ModalDialog,
Link,
Checkbox,
Button,
Textarea,
Text
} from "asc-web-components";
import { getInvitationLink, getShortenedLink } from '../../../store/profile/actions';
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from './i18n';
import { typeGuests } from './../../../helpers/customNames';
import styled from 'styled-components'
import copy from 'copy-to-clipboard';
import { getShortenedLink } from "../../../store/services/api";
import { withTranslation, I18nextProvider } from "react-i18next";
import i18n from "./i18n";
import { typeGuests } from "./../../../helpers/customNames";
import styled from "styled-components";
import copy from "copy-to-clipboard";
const ModalDialogContainer = styled.div`
.margin-text {
margin: 12px 0;
}
.margin-text {
margin: 12px 0;
}
.margin-link {
margin-right: 12px;
}
.margin-link {
margin-right: 12px;
}
.margin-textarea {
margin-top: 12px;
}
.margin-textarea {
margin-top: 12px;
}
.flex{
display: flex;
justify-content: space-between;
}
.flex {
display: flex;
justify-content: space-between;
}
`;
const textAreaName = 'link-textarea';
const textAreaName = "link-textarea";
class PureInviteDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
isGuest: false,
userInvitationLink: this.props.userInvitationLink,
guestInvitationLink: this.props.guestInvitationLink,
isLoading: false,
isLinkShort: false,
visible: false
}
}
onCopyLinkToClipboard = () => {
// console.log("COPY");
const { t } = this.props;
copy(this.state.isGuest ? this.state.guestInvitationLink : this.state.userInvitationLink);
toastr.success(t('LinkCopySuccess'));
constructor(props) {
super(props);
this.state = {
isGuest: false,
userInvitationLink: this.props.userInvitationLink,
guestInvitationLink: this.props.guestInvitationLink,
isLoading: false,
isLinkShort: false,
visible: false
};
}
onCheckedGuest = () => this.setState({ isGuest: !this.state.isGuest });
onCopyLinkToClipboard = () => {
// console.log("COPY");
const { t } = this.props;
copy(
this.state.isGuest
? this.state.guestInvitationLink
: this.state.userInvitationLink
);
toastr.success(t("LinkCopySuccess"));
};
onGetShortenedLink = () => {
this.setState({ isLoading: true });
const { getShortenedLink, userInvitationLink, guestInvitationLink } = this.props;
onCheckedGuest = () => this.setState({ isGuest: !this.state.isGuest });
getShortenedLink(userInvitationLink)
.then((res) => {
// console.log("getShortInvitationLinkuser success", res.data.response);
this.setState({ userInvitationLink: res.data.response });
})
.catch(e => {
console.error("getShortInvitationLink error", e);
this.setState({ isLoading: false });
});
onGetShortenedLink = () => {
this.setState({ isLoading: true });
const {
userInvitationLink,
guestInvitationLink
} = this.props;
getShortenedLink(guestInvitationLink)
.then((res) => {
// console.log("getShortInvitationLinkGuest success", res.data.response);
this.setState({
guestInvitationLink: res.data.response,
isLoading: false,
isLinkShort: true
});
})
.catch(e => {
console.error("getShortInvitationLink error", e);
});
getShortenedLink(userInvitationLink)
.then(link => this.setState({ userInvitationLink: link }))
.catch(e => {
console.error("getShortInvitationLink error", e);
this.setState({ isLoading: false });
});
};
getShortenedLink(guestInvitationLink)
.then(link =>
this.setState({
guestInvitationLink: link,
isLoading: false,
isLinkShort: true
})
)
.catch(e => {
console.error("getShortInvitationLink error", e);
});
};
componentDidUpdate(prevProps) {
console.log('invitelink did UPDATE')
if (this.props.visible && !prevProps.visible) {
this.onCopyLinkToClipboard();
}
componentDidUpdate(prevProps) {
console.log("invitelink did UPDATE");
if (this.props.visible && !prevProps.visible) {
this.onCopyLinkToClipboard();
}
}
onClickToCloseButton = () => this.props.onCloseButton && this.props.onCloseButton();
onClose = () => this.props.onClose && this.props.onClose();
onClickToCloseButton = () =>
this.props.onCloseButton && this.props.onCloseButton();
onClose = () => this.props.onClose && this.props.onClose();
render() {
console.log("InviteDialog render");
const { t, visible, settings } = this.props;
render() {
console.log("InviteDialog render");
const { t, visible, settings } = this.props;
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
onClose={this.onClose}
headerContent={t('InviteLinkTitle')}
bodyContent={(
<>
<Text.Body
className='margin-text'
as='p'>
{t('HelpAnswerLinkInviteSettings')}
</Text.Body>
<Text.Body
className='margin-text'
as='p'>
{t('InviteLinkValidInterval', { count: 7 })}
</Text.Body>
<div className='flex'>
<div>
<Link
className='margin-link'
type='action'
isHovered={true}
onClick={this.onCopyLinkToClipboard}
>
{t('CopyToClipboard')}
</Link>
{
settings && !this.state.isLinkShort &&
<Link type='action'
isHovered={true}
onClick={this.onGetShortenedLink}
>
{t('GetShortenLink')}
</Link>
}
</div>
<Checkbox
label={t('InviteUsersAsCollaborators', { typeGuests })}
isChecked={this.state.isGuest}
onChange={this.onCheckedGuest}
isDisabled={this.state.isLoading}
/>
</div>
<Textarea
className='margin-textarea'
isReadOnly={true}
isDisabled={this.state.isLoading}
name={textAreaName}
value={this.state.isGuest ? this.state.guestInvitationLink : this.state.userInvitationLink}
/>
</>
)}
footerContent={(
<>
<Button
key="CloseBtn"
label={this.state.isLoading ? t('LoadingProcessing') : t('CloseButton')}
size="medium"
primary={true}
onClick={this.onClickToCloseButton}
isLoading={this.state.isLoading}
/>
</>
)}
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
onClose={this.onClose}
headerContent={t("InviteLinkTitle")}
bodyContent={
<>
<Text.Body className="margin-text" as="p">
{t("HelpAnswerLinkInviteSettings")}
</Text.Body>
<Text.Body className="margin-text" as="p">
{t("InviteLinkValidInterval", { count: 7 })}
</Text.Body>
<div className="flex">
<div>
<Link
className="margin-link"
type="action"
isHovered={true}
onClick={this.onCopyLinkToClipboard}
>
{t("CopyToClipboard")}
</Link>
{settings && !this.state.isLinkShort && (
<Link
type="action"
isHovered={true}
onClick={this.onGetShortenedLink}
>
{t("GetShortenLink")}
</Link>
)}
</div>
<Checkbox
label={t("InviteUsersAsCollaborators", { typeGuests })}
isChecked={this.state.isGuest}
onChange={this.onCheckedGuest}
isDisabled={this.state.isLoading}
/>
</ModalDialogContainer>
);
};
};
const mapStateToProps = (state) => {
return {
settings: state.auth.settings.hasShortenService,
userInvitationLink: state.auth.settings.inviteLinks.userLink,
guestInvitationLink: state.auth.settings.inviteLinks.guestLink
}
</div>
<Textarea
className="margin-textarea"
isReadOnly={true}
isDisabled={this.state.isLoading}
name={textAreaName}
value={
this.state.isGuest
? this.state.guestInvitationLink
: this.state.userInvitationLink
}
/>
</>
}
footerContent={
<>
<Button
key="CloseBtn"
label={
this.state.isLoading
? t("LoadingProcessing")
: t("CloseButton")
}
size="medium"
primary={true}
onClick={this.onClickToCloseButton}
isLoading={this.state.isLoading}
/>
</>
}
/>
</ModalDialogContainer>
);
}
}
const mapStateToProps = state => {
return {
settings: state.auth.settings.hasShortenService,
userInvitationLink: state.auth.settings.inviteLinks.userLink,
guestInvitationLink: state.auth.settings.inviteLinks.guestLink
};
};
const InviteDialogContainer = withTranslation()(PureInviteDialog);
const InviteDialog = (props) => <I18nextProvider i18n={i18n}><InviteDialogContainer {...props} /></I18nextProvider>;
const InviteDialog = props => (
<I18nextProvider i18n={i18n}>
<InviteDialogContainer {...props} />
</I18nextProvider>
);
InviteDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onCloseButton: PropTypes.func.isRequired
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onCloseButton: PropTypes.func.isRequired
};
export default connect(mapStateToProps, { getInvitationLink, getShortenedLink })(withRouter(InviteDialog));
export default connect(mapStateToProps)(withRouter(InviteDialog));

View File

@ -32,6 +32,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -1,9 +1,23 @@
import React from 'react';
import React, { useEffect } from 'react';
import { connect } from "react-redux";
import { ErrorContainer } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import i18n from './i18n';
export const Error404 = () => {
const Error404Container = ({language}) => {
const { t } = useTranslation('translation', { i18n });
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return <ErrorContainer>{t("Error404Text")}</ErrorContainer>;
};
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export const Error404 = connect(mapStateToProps)(Error404Container);

View File

@ -0,0 +1,3 @@
{
"Error404Text": "Извините, страница не найдена."
}

View File

@ -28,7 +28,7 @@ import {
updateGroup
} from "../../../../../store/group/actions";
import styled from "styled-components";
import { fetchSelectorUsers } from "../../../../../store/people/actions";
import { fetchSelectorUsers, fetchPeople, fetchGroups } from "../../../../../store/people/actions";
import { GUID_EMPTY } from "../../../../../helpers/constants";
import isEqual from "lodash/isEqual";
@ -110,34 +110,40 @@ class SectionBodyContent extends React.Component {
key: 0,
label: t("CustomAddEmployee", { typeUser })
},
groupMembers: group && group.members ? group.members.map(m => {
return {
key: m.id,
label: m.displayName
}
}) : [],
groupManager: group && group.manager ? {
key: group.manager.id,
label: group.manager.displayName
} : {
key: GUID_EMPTY,
label: t("CustomAddEmployee", { typeUser })
}
groupMembers:
group && group.members
? group.members.map(m => {
return {
key: m.id,
label: m.displayName
};
})
: [],
groupManager:
group && group.manager
? {
key: group.manager.id,
label: group.manager.displayName
}
: {
key: GUID_EMPTY,
label: t("CustomAddEmployee", { typeUser })
}
};
return newState;
}
};
componentDidMount() {
const { users, fetchSelectorUsers } = this.props;
if(!users || !users.length) {
if (!users || !users.length) {
fetchSelectorUsers();
}
}
componentDidUpdate(prevProps) {
//const { users, group } = this.props;
if(!isEqual(this.props, prevProps)) {
if (!isEqual(this.props, prevProps)) {
this.setState(this.mapPropsToState());
}
}
@ -163,12 +169,12 @@ class SectionBodyContent extends React.Component {
};
onHeadSelectorSelect = option => {
this.setState({
this.setState({
groupManager: {
key: option.key,
label: option.label
},
isHeaderSelectorOpen: !this.state.isHeaderSelectorOpen
isHeaderSelectorOpen: !this.state.isHeaderSelectorOpen
});
};
@ -178,24 +184,24 @@ class SectionBodyContent extends React.Component {
});
};
onUsersSelectorSearch = (value) => {
onUsersSelectorSearch = value => {
/*setOptions(
options.filter(option => {
return option.label.indexOf(value) > -1;
})
);*/
};
onUsersSelectorSelect = (selectedOptions) => {
onUsersSelectorSelect = selectedOptions => {
//console.log("onSelect", selectedOptions);
//this.onUsersSelectorClick();
this.setState({
this.setState({
groupMembers: selectedOptions.map(option => {
return {
key: option.key,
label: option.label
};
}),
isUsersSelectorOpen: !this.state.isUsersSelectorOpen
isUsersSelectorOpen: !this.state.isUsersSelectorOpen
});
};
@ -211,31 +217,36 @@ class SectionBodyContent extends React.Component {
});
};
save = (group) => {
const { createGroup, updateGroup } = this.props;
return group.id
? updateGroup(group.id, group.name, group.managerKey, group.members)
: createGroup(group.name, group.managerKey, group.members);
};
onSave = () => {
const { history, group, createGroup, updateGroup, resetGroup } = this.props;
const { group } = this.props;
const { groupName, groupManager, groupMembers } = this.state;
if (!groupName || !groupName.trim().length) return false;
this.setState({ inLoading: true });
(group && group.id
? updateGroup(
group.id,
groupName,
groupManager.key,
groupMembers.map(u => u.key)
)
: createGroup(groupName, groupManager.key, groupMembers.map(u => u.key))
)
.then(() => {
toastr.success("Success");
//this.setState({ inLoading: true });
history.goBack();
resetGroup();
const newGroup = {
name: groupName,
managerKey: groupManager.key,
members: groupMembers.map(u => u.key)
};
if(group && group.id)
newGroup.id = group.id;
this.save(newGroup)
.then(group => {
toastr.success(`Group '${group.name}' has been saved successfully`);
})
.catch(error => {
toastr.error(error.message);
toastr.error(error);
this.setState({ inLoading: false });
});
};
@ -247,11 +258,11 @@ class SectionBodyContent extends React.Component {
history.goBack();
};
onSelectedItemClose = (member) => {
this.setState({
onSelectedItemClose = member => {
this.setState({
groupMembers: this.state.groupMembers.filter(g => g.key !== member.key)
});
}
};
renderModal = () => {
const { groups, modalVisible } = this.state;
@ -413,7 +424,7 @@ class SectionBodyContent extends React.Component {
<Icons.CatalogGuestIcon size="medium" />
</ComboButton>
<AdvancedSelector
isDropDown={true}
displayType="dropdown"
isOpen={isHeadSelectorOpen}
size="full"
placeholder={"Search"}
@ -435,7 +446,7 @@ class SectionBodyContent extends React.Component {
isRequired={false}
hasError={false}
isVertical={true}
labelText="Members"
labelText={t("Members")}
>
<ComboButton
id="users-selector"
@ -455,7 +466,7 @@ class SectionBodyContent extends React.Component {
<Icons.CatalogGuestIcon size="medium" />
</ComboButton>
<AdvancedSelector
isDropDown={true}
displayType="dropdown"
isOpen={isUsersSelectorOpen}
size="full"
placeholder={"Search"}
@ -559,11 +570,11 @@ function mapStateToProps(state) {
settings: state.auth.settings,
group: state.group.targetGroup,
groups: convertGroups(state.people.groups),
users: convertUsers(state.people.selector.users) //TODO: replace to api requests with search
users: convertUsers(state.people.selector.users), //TODO: replace to api requests with search
};
}
export default connect(
mapStateToProps,
{ resetGroup, createGroup, updateGroup, fetchSelectorUsers }
{ resetGroup, createGroup, updateGroup, fetchSelectorUsers, fetchPeople, fetchGroups }
)(withRouter(withTranslation()(SectionBodyContent)));

View File

@ -32,6 +32,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -50,7 +53,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -31,7 +31,9 @@ class GroupAction extends React.Component {
render() {
console.log("GroupAction render")
const { group, match } = this.props;
const { group, match, language } = this.props;
i18n.changeLanguage(language);
return (
<I18nextProvider i18n={i18n}>
@ -59,6 +61,7 @@ class GroupAction extends React.Component {
function mapStateToProps(state) {
return {
settings: state.auth.settings,
language: state.auth.user.cultureName || state.auth.settings.culture,
group: state.group.targetGroup
};
}

View File

@ -8,5 +8,6 @@
"CustomAddEmployee": "Add {{typeUser, lowercase}}",
"CustomNewDepartment": "New {{department, lowercase}}",
"CustomEditDepartment": "Edit {{department, lowercase}}",
"CustomDepartmentName": "{{department}} name"
"CustomDepartmentName": "{{department}} name",
"Members": "Members"
}

View File

@ -0,0 +1,13 @@
{
"SaveButton": "Сохранить",
"CancelButton": "Отмена",
"CustomHeadOfDepartment": "{{headOfDepartment}}",
"CustomAddEmployee": "Добавить {{typeUser, lowercase}}",
"CustomNewDepartment": "Новый {{department, lowercase}}",
"CustomEditDepartment": "Редактирование {{department, lowercase}}",
"CustomDepartmentName": "Имя {{department}}",
"Members": "Участники"
}

View File

@ -100,7 +100,7 @@ class SectionBodyContent extends React.PureComponent {
</Text.Body>
)
)
.catch(e => toastr.error("ERROR"))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
@ -172,7 +172,7 @@ class SectionBodyContent extends React.PureComponent {
onLoading(true);
updateUserStatus(EmployeeStatus.Disabled, [user.id])
.then(() => toastr.success("SUCCESS Context action: Disable"))
.catch(e => toastr.error("FAILED Context action: Disable", e))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
};
@ -182,7 +182,7 @@ class SectionBodyContent extends React.PureComponent {
onLoading(true);
updateUserStatus(EmployeeStatus.Active, [user.id])
.then(() => toastr.success("SUCCESS Context action: Enable"))
.catch(e => toastr.error("FAILED Context action: Enable", e))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
};
@ -230,7 +230,7 @@ class SectionBodyContent extends React.PureComponent {
toastr.success("User has been removed successfully");
return fetchPeople(filter);
})
.catch(e => toastr.error("ERROR"))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
@ -290,7 +290,7 @@ class SectionBodyContent extends React.PureComponent {
</Text.Body>
)
)
.catch(e => toastr.error("ERROR"))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
@ -320,7 +320,7 @@ class SectionBodyContent extends React.PureComponent {
</Text.Body>
)
)
.catch(e => toastr.error("ERROR"))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
};
getUserContextOptions = (user, viewer) => {

View File

@ -1,38 +1,75 @@
import React, { useCallback } from "react";
import { withRouter } from "react-router";
import { RowContent, Link, Icons } from "asc-web-components";
import { RowContent, Link, LinkWithDropdown, Icons, toastr } from "asc-web-components";
import { connect } from "react-redux";
import { getUserStatus } from "../../../../../store/people/selectors";
import { useTranslation } from 'react-i18next';
import { headOfDepartment } from './../../../../../helpers/customNames';
import history from "../../../../../history";
const getFormatedGroups = groups => {
let temp = [];
if (!groups) temp.push({ key: 0, label: '' });
groups && groups.map(group =>
temp.push(
{
key: group.id,
label: group.name,
onClick: () => history.push(`/products/people/filter?group=${group.id}`)
}
)
);
if (temp.length <= 1) {
return (
<Link
containerWidth='160px'
type='action'
title={temp[0].label}
fontSize={12}
onClick={temp[0].onClick}
>
{temp[0].label}
</Link>);
} else {
return (
<LinkWithDropdown
isTextOverflow={true}
containerWidth='160px'
type='action'
title={temp[0].label}
fontSize={12}
data={temp}
>
{temp[0].label}
</LinkWithDropdown>);
}
};
const UserContent = ({ user, history, settings }) => {
const { userName, displayName, headDepartment, department, mobilePhone, email } = user;
const { userName, displayName, title, mobilePhone, email } = user;
const status = getUserStatus(user);
const groups = getFormatedGroups(user.groups);
const onUserNameClick = useCallback(() => {
console.log("User name action");
history.push(`${settings.homepage}/view/${userName}`);
}, [history, settings.homepage, userName]);
const onHeadDepartmentClick = useCallback(
() => console.log("Head of department action"),
[]
const onUserTitleClick = useCallback(
() => toastr.success(`Filter action by user title: ${title}`),
[title]
);
const onDepartmentClick = useCallback(
() => console.log("Department action"),
[]
);
const onPhoneClick = useCallback(
() => console.log("Phone action"),
[]
() => window.open(`sms:${mobilePhone}`),
[mobilePhone]
);
const onEmailClick = useCallback(
() => console.log("Email action"),
[]
() => window.open(`mailto:${email}`),
[email]
);
const nameColor = status === 'pending' ? '#A3A9AE' : '#333333';
@ -40,7 +77,7 @@ const UserContent = ({ user, history, settings }) => {
const { t } = useTranslation();
const headDepartmentStyle = {
width: '80px'
width: '110px'
}
return (
@ -50,20 +87,20 @@ const UserContent = ({ user, history, settings }) => {
{status === 'pending' && <Icons.SendClockIcon size='small' isfill={true} color='#3B72A7' />}
{status === 'disabled' && <Icons.CatalogSpamIcon size='small' isfill={true} color='#3B72A7' />}
</>
{headDepartment
{title
? <Link
containerWidth='80px'
containerWidth='110px'
type='page'
title={t('CustomHeadOfDepartment', { headOfDepartment })}
title={title}
fontSize={12}
color={sideInfoColor}
onClick={onHeadDepartmentClick}
onClick={onUserTitleClick}
>
{t('CustomHeadOfDepartment', { headOfDepartment })}
{title}
</Link>
: <div style={headDepartmentStyle}></div>
}
<Link containerWidth='160px' type='action' title={department} fontSize={12} color={sideInfoColor} onClick={onDepartmentClick} >{department}</Link>
{groups}
<Link type='page' title={mobilePhone} fontSize={12} color={sideInfoColor} onClick={onPhoneClick} >{mobilePhone}</Link>
<Link containerWidth='220px' type='page' title={email} fontSize={12} color={sideInfoColor} onClick={onEmailClick} >{email}</Link>
</RowContent>

View File

@ -12,18 +12,6 @@ import {
department
} from "./../../../../../helpers/customNames";
import { withRouter } from "react-router";
import Filter from "../../../../../store/people/filter";
import {
EMPLOYEE_STATUS,
ACTIVATION_STATUS,
ROLE,
GROUP,
SEARCH,
SORT_BY,
SORT_ORDER,
PAGE,
PAGE_COUNT
} from "../../../../../helpers/constants";
import { getFilterByLocation } from "../../../../../helpers/converters";
const getEmployeeStatus = filterValues => {

View File

@ -1,11 +1,13 @@
import React, { useCallback } from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import {
GroupButtonsMenu,
DropDownItem,
Text,
toastr,
ContextMenuButton
ContextMenuButton,
IconButton
} from "asc-web-components";
import { connect } from "react-redux";
import {
@ -28,13 +30,25 @@ import {
resendUserInvites,
deleteUsers
} from "../../../../../store/services/api";
import { deleteGroup } from '../../../../../store/group/actions';
import { deleteGroup } from "../../../../../store/group/actions";
const StyledContainer = styled.div`
.group-button-menu-container {
margin: 0 -16px;
}
const wrapperStyle = {
display: "flex",
alignItems: "center"
};
.header-container {
position: relative;
display: flex;
align-items: center;
max-width: calc(100vw - 32px);
.add-group-button {
margin-left: 8px;
}
}
`;
const SectionHeaderContent = props => {
const {
@ -50,7 +64,7 @@ const SectionHeaderContent = props => {
selection,
updateUserStatus,
updateUserType,
onLoading,
onLoading,
filter,
history,
settings,
@ -83,7 +97,7 @@ const SectionHeaderContent = props => {
const onSentInviteAgain = useCallback(() => {
resendUserInvites(selectedUserIds)
.then(() => toastr.success("The invitation was successfully sent"))
.catch(e => toastr.error("ERROR"));
.catch(error => toastr.error(error));
}, [selectedUserIds]);
const onDelete = useCallback(() => {
@ -93,7 +107,7 @@ const SectionHeaderContent = props => {
toastr.success("Users have been removed successfully");
return fetchPeople(filter);
})
.catch(e => toastr.error("ERROR"))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
}, [selectedUserIds, onLoading, filter]);
@ -148,11 +162,15 @@ const SectionHeaderContent = props => {
}
];
const onEditGroup = useCallback(() => history.push(`${settings.homepage}/group/edit/${group.id}`), [history, settings, group]);
const onEditGroup = useCallback(
() => history.push(`${settings.homepage}/group/edit/${group.id}`),
[history, settings, group]
);
const onDeleteGroup = useCallback(() => {
deleteGroup(group.id)
.then(() => toastr.success("Group has been removed successfully"));
deleteGroup(group.id).then(() =>
toastr.success("Group has been removed successfully")
);
}, [deleteGroup, group]);
const getContextOptions = useCallback(() => {
@ -170,37 +188,60 @@ const SectionHeaderContent = props => {
];
}, [t, onEditGroup, onDeleteGroup]);
return isHeaderVisible ? (
<div style={{ margin: "0 -16px" }}>
<GroupButtonsMenu
checked={isHeaderChecked}
isIndeterminate={isHeaderIndeterminate}
onChange={onCheck}
menuItems={menuItems}
visible={isHeaderVisible}
moreLabel={t("More")}
closeTitle={t("CloseButton")}
onClose={onClose}
selected={menuItems[0].label}
/>
</div>
) : group ? (
<div style={wrapperStyle}>
<Text.ContentHeader>{group.name}</Text.ContentHeader>
{isAdmin && (
<ContextMenuButton
directionX="right"
title={t("Actions")}
iconName="VerticalDotsIcon"
size={16}
color="#A3A9AE"
getData={getContextOptions.bind(this, t)}
isDisabled={false}
/>
const onAddDepartmentsClick = useCallback(() => {
history.push(`${settings.homepage}/group/create`);
}, [history, settings]);
return (
<StyledContainer>
{isHeaderVisible ? (
<div className="group-button-menu-container">
<GroupButtonsMenu
checked={isHeaderChecked}
isIndeterminate={isHeaderIndeterminate}
onChange={onCheck}
menuItems={menuItems}
visible={isHeaderVisible}
moreLabel={t("More")}
closeTitle={t("CloseButton")}
onClose={onClose}
selected={menuItems[0].label}
/>
</div>
) : (
<div className="header-container">
{group ? (
<>
<Text.ContentHeader truncate={true}>{group.name}</Text.ContentHeader>
{isAdmin && (
<ContextMenuButton
directionX="right"
title={t("Actions")}
iconName="VerticalDotsIcon"
size={16}
color="#A3A9AE"
getData={getContextOptions.bind(this, t)}
isDisabled={false}
/>
)}
</>
) : (
<>
<Text.ContentHeader>Departments</Text.ContentHeader>
{isAdmin && (
<IconButton
className="add-group-button"
size={16}
iconName="PlusIcon"
isFill={false}
onClick={onAddDepartmentsClick}
/>
)}
</>
)}
</div>
)}
</div>
) : (
<Text.ContentHeader>{t("People")}</Text.ContentHeader>
</StyledContainer>
);
};

View File

@ -32,6 +32,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -50,7 +53,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -39,13 +39,13 @@ class PureHome extends React.Component {
headerVisible && selection.length > 0 && selection.length < users.length;
const headerChecked = headerVisible && selection.length === users.length;
console.log(`renderGroupButtonMenu()
/*console.log(`renderGroupButtonMenu()
headerVisible=${headerVisible}
headerIndeterminate=${headerIndeterminate}
headerChecked=${headerChecked}
selection.length=${selection.length}
users.length=${users.length}
selected=${selected}`);
selected=${selected}`);*/
let newState = {};
@ -143,18 +143,13 @@ class PureHome extends React.Component {
}
}
function mapStateToProps(state) {
return {
users: state.people.users,
selection: state.people.selection,
selected: state.people.selected,
isLoaded: state.auth.isLoaded
};
}
const HomeContainer = withTranslation()(PureHome);
const Home = (props) => <I18nextProvider i18n={i18n}><HomeContainer {...props}/></I18nextProvider>;
const Home = (props) => {
const {language} = props;
i18n.changeLanguage(language);
return (<I18nextProvider i18n={i18n}><HomeContainer {...props}/></I18nextProvider>);
}
Home.propTypes = {
users: PropTypes.array.isRequired,
@ -162,6 +157,16 @@ Home.propTypes = {
isLoaded: PropTypes.bool
};
function mapStateToProps(state) {
return {
users: state.people.users,
selection: state.people.selection,
selected: state.people.selected,
isLoaded: state.auth.isLoaded,
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export default connect(
mapStateToProps,
{ setSelected }

View File

@ -0,0 +1,56 @@
{
"LoadingProcessing": "Загрузка...",
"LblSendEmail": "Отправить email",
"LblSendMessage": "Отправить сообщение",
"EditButton": "Редактировать",
"PasswordChangeButton": "Изменить пароль",
"EmailChangeButton": "Изменить email",
"DisableUserButton": "Заблокировать",
"EnableUserButton": "Разблокировать",
"ReassignData": "Передать данные",
"RemoveData": "Удалить личные данные",
"DeleteSelfProfile": "Удалить профиль",
"LoadingDescription": "Пожалуйста подождите...",
"NotFoundDescription": "В данном разделе нет людей, соответствующих фильтру. Пожалуйста, выберите другие параметры или очистите фильтр, чтобы просмотреть всех людей в этом разделе.",
"NotFoundTitle": "Результатов, соответствующих заданным критериям, не найдено",
"ClearButton": "Сбросить фильтр",
"UserStatus": "Статус",
"LblActive": "Активный",
"LblTerminated": "Заблокирован",
"Email": "Email",
"LblPending": "Ожидание",
"UserType": "Тип",
"Administrator": "Администратор",
"LblOther": "Другое",
"DeleteButton": "Удалить",
"SuccessChangeUserStatus": "Статус пользователя успешно изменен",
"SuccessChangeUserType": "Тип пользователя успешно изменен",
"LblSelect": "Выберите",
"More": "Больше",
"CloseButton": "Закрыть",
"Actions": "Действия",
"People": "Люди",
"PreviousPage": "Предыдущая",
"NextPage": "Следующая",
"LblInviteAgain": "Выслать прилашение ещё раз",
"ByFirstNameSorting": "По имени",
"ByLastNameSorting": "По фамилии",
"LblInvited": "Invited",
"LblSetActive": "Разблокировать",
"LblSetDisabled": "Заблокировать",
"CustomHeadOfDepartment": " {{headOfDepartment}}",
"CustomTypeGuest": "{{typeGuest}}",
"CustomTypeUser": "{{typeUser}}",
"CustomMakeUser": "Сделать {{typeUser, lowercase}}",
"CustomMakeGuest": "Сделать {{typeGuest, lowercase}}",
"CustomDepartment": "{{department}}",
"CountPerPage": "{{count}} на странице",
"PageOfTotalPage": "{{page}} из {{totalPage}}",
"DirectionAscLabel":"А-Я",
"DirectionDescLabel":"Я-А",
"DefaultSelectLabel": "Выберите"
}

View File

@ -0,0 +1,389 @@
import React from "react";
import { Trans } from 'react-i18next';
import { department as departmentName, position, employedSinceDate } from '../../../../../../helpers/customNames';
import { resendUserInvites, sendInstructionsToChangeEmail } from "../../../../../../store/services/api";
import {
Text,
TextInput,
Button,
IconButton,
Link,
toastr,
ModalDialog,
ComboBox,
HelpButton
} from "asc-web-components";
import styled from 'styled-components';
import history from "../../../../../../history";
const InfoContainer = styled.div`
margin-bottom: 24px;
`;
const InfoItem = styled.div`
font-family: Open Sans;
font-style: normal;
font-weight: normal;
font-size: 13px;
line-height: 24px;
display: flex;
width: 400px;
`;
const InfoItemLabel = styled.div`
width: 140px;
white-space: nowrap;
color: #A3A9AE;
`;
const InfoItemValue = styled.div`
width: 240px;
.language-combo {
padding-top: 4px;
float: left;
& > div {
padding-left: 0px;
}
}
`;
const TooltipIcon = styled.span`
display: inline-flex;
padding-top: 6px;
`;
const IconButtonWrapper = styled.div`
${props => props.isBefore
? `margin-right: 8px;`
: `margin-left: 8px;`
}
display: inline-flex;
:hover {
& > div > svg > path {
fill: #3B72A7;
}
}
`;
const onGroupClick = (department) => {
history.push(`/products/people/filter?group=${department.id}`)
};
const getFormattedDepartments = departments => {
const formattedDepartments = departments.map((department, index) => {
return (
<span key={index}>
<Link type="page" fontSize={13} isHovered={true} onClick={onGroupClick.bind(this, department)}>
{department.name}
</Link>
{departments.length - 1 !== index ? ", " : ""}
</span>
);
});
return formattedDepartments;
};
const capitalizeFirstLetter = string => {
return string && string.charAt(0).toUpperCase() + string.slice(1);
};
class ProfileInfo extends React.PureComponent {
constructor(props) {
super(props);
this.state = this.mapPropsToState(props);
}
mapPropsToState = (props) => {
const newState = {
profile: props.profile,
dialog: {
visible: false,
header: "",
body: "",
buttons: [],
newEmail: props.profile.email,
}
};
return newState;
};
onEmailChange = e => {
const emailRegex = /.+@.+\..+/;
const newEmail = e.target.value || this.state.dialog.newEmail || this.props.profile.email;
const hasError = !emailRegex.test(newEmail);
const dialog = {
visible: true,
header: "Change email",
body: (
<Text.Body>
<span style={{ display: "block", marginBottom: "8px" }}>The activation instructions will be sent to the entered email</span>
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={newEmail}
onChange={this.onEmailChange}
hasError={hasError}
/>
</Text.Body>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendEmailChangeInstructions}
isDisabled={hasError}
/>
],
value: newEmail
};
this.setState({ dialog: dialog })
}
onSendEmailChangeInstructions = () => {
sendInstructionsToChangeEmail(this.state.profile.id, this.state.dialog.value)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(this.onDialogClose);
}
onSentInviteAgain = id => {
resendUserInvites(new Array(id))
.then(() => toastr.success("The invitation was successfully sent"))
.catch(error => toastr.error(error));
};
onDialogClose = value => {
const dialog = { visible: false, value: value };
this.setState({ dialog: dialog })
}
onEmailClick = (e, email) => {
if (e.target.title)
window.open("mailto:" + email);
}
onLanguageSelect = (language) => {
console.log("onLanguageSelect", language);
const { profile, updateProfileCulture } = this.props;
if (profile.cultureName === language.key) return;
updateProfileCulture(profile.id, language.key);
}
getLanguages = () => {
const { cultures, t } = this.props;
return cultures.map((culture) => {
return { key: culture, label: t(`Culture_${culture}`) };
});
}
render() {
const { dialog } = this.state;
const { isVisitor, email, activationStatus, department, groups, title, mobilePhone, sex, workFrom, birthday, location, cultureName, currentCulture, id } = this.props.profile;
const isAdmin = this.props.isAdmin;
const isSelf = this.props.isSelf;
const { t, i18n } = this.props;
const type = isVisitor ? "Guest" : "Employee";
const language = cultureName || currentCulture || this.props.culture;
const languages = this.getLanguages();
const selectedLanguage = languages.find(item => item.key === language);
const workFromDate = new Date(workFrom).toLocaleDateString(language);
const birthDayDate = new Date(birthday).toLocaleDateString(language);
const formatedSex = capitalizeFirstLetter(sex);
const formatedDepartments = department && getFormattedDepartments(groups);
const supportEmail = "documentation@onlyoffice.com";
const tooltipLanguage =
<Text.Body fontSize={13}>
<Trans i18nKey="NotFoundLanguage" i18n={i18n}>
"In case you cannot find your language in the list of the
available ones, feel free to write to us at
<Link href="mailto:documentation@onlyoffice.com" isHovered={true}>
{{ supportEmail }}
</Link> to take part in the translation and get up to 1 year free of
charge."
</Trans>
{" "}
<Link isHovered={true} href="https://helpcenter.onlyoffice.com/ru/guides/become-translator.aspx">{t("LearnMore")}</Link>
</Text.Body>
return (
<InfoContainer>
<InfoItem>
<InfoItemLabel>
{t('UserType')}:
</InfoItemLabel>
<InfoItemValue>
{type}
</InfoItemValue>
</InfoItem>
{email &&
<InfoItem>
<InfoItemLabel>
{t('Email')}:
</InfoItemLabel>
<InfoItemValue>
<>
{activationStatus === 2 && (isAdmin || isSelf) &&
<IconButtonWrapper isBefore={true} title={t('PendingTitle')}>
<IconButton
color='#C96C27'
size={16}
iconName='DangerIcon'
isFill={true} />
</IconButtonWrapper>
}
<Link
type="page"
fontSize={13}
isHovered={true}
title={email}
onClick={this.onEmailClick.bind(email)}
>
{email}
</Link>
{(isAdmin || isSelf) &&
<IconButtonWrapper title={t('EmailChangeButton')} >
<IconButton
color="#A3A9AE"
size={16}
iconName='AccessEditIcon'
isFill={true}
onClick={this.onEmailChange} />
</IconButtonWrapper>
}
{activationStatus === 2 && (isAdmin || isSelf) &&
<IconButtonWrapper title={t('SendInviteAgain')}>
<IconButton
color="#A3A9AE"
size={16}
iconName='FileActionsConvertIcon'
isFill={true}
onClick={this.onSentInviteAgain.bind(this, id)} />
</IconButtonWrapper>
}
</>
</InfoItemValue>
</InfoItem>
}
{department &&
<InfoItem>
<InfoItemLabel>
{t("CustomDepartment", { department: departmentName })}:
</InfoItemLabel>
<InfoItemValue>
{formatedDepartments}
</InfoItemValue>
</InfoItem>
}
{title &&
<InfoItem>
<InfoItemLabel>
{t("CustomPosition", { position })}:
</InfoItemLabel>
<InfoItemValue>
{title}
</InfoItemValue>
</InfoItem>
}
{(mobilePhone) &&
<InfoItem>
<InfoItemLabel>
{t('PhoneLbl')}:
</InfoItemLabel>
<InfoItemValue>
{mobilePhone}
</InfoItemValue>
</InfoItem>
}
{sex &&
<InfoItem>
<InfoItemLabel>
{t('Sex')}:
</InfoItemLabel>
<InfoItemValue>
{formatedSex}
</InfoItemValue>
</InfoItem>
}
{workFrom &&
<InfoItem>
<InfoItemLabel>
{t("CustomEmployedSinceDate", { employedSinceDate })}:
</InfoItemLabel>
<InfoItemValue>
{workFromDate}
</InfoItemValue>
</InfoItem>
}
{birthday &&
<InfoItem>
<InfoItemLabel>
{t('Birthdate')}:
</InfoItemLabel>
<InfoItemValue>
{birthDayDate}
</InfoItemValue>
</InfoItem>
}
{location &&
<InfoItem>
<InfoItemLabel>
{t('Location')}:
</InfoItemLabel>
<InfoItemValue>
{location}
</InfoItemValue>
</InfoItem>
}
{isSelf &&
<InfoItem>
<InfoItemLabel>
{t('Language')}:
</InfoItemLabel>
<InfoItemValue>
<ComboBox
options={languages}
selectedOption={selectedLanguage}
onSelect={this.onLanguageSelect}
isDisabled={false}
noBorder={true}
scaled={false}
scaledOptions={false}
size='content'
className='language-combo'
/>
<TooltipIcon>
<HelpButton place="bottom" offsetLeft={50} offsetRight={0} tooltipContent={tooltipLanguage} />
</TooltipIcon>
</InfoItemValue>
</InfoItem>
}
<ModalDialog
visible={dialog.visible}
headerContent={dialog.header}
bodyContent={dialog.body}
footerContent={dialog.buttons}
onClose={this.onDialogClose.bind(this, email)}
/>
</InfoContainer>
);
}
};
export default ProfileInfo;

View File

@ -1,24 +1,19 @@
import React, { useCallback } from "react";
import { withRouter } from "react-router";
import { useTranslation } from 'react-i18next';
import { department as departmentName, position, employedSinceDate } from '../../../../../helpers/customNames';
import { resendUserInvites, sendInstructionsToChangeEmail } from "../../../../../store/services/api";
import {
Text,
TextInput,
Avatar,
Button,
ToggleContent,
IconButton,
Link,
toastr,
ModalDialog,
ComboBox
IconButton
} from "asc-web-components";
import { connect } from "react-redux";
import styled from 'styled-components';
import { getUserRole, getUserContacts } from "../../../../../store/people/selectors";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
import { updateProfileCulture } from "../../../../../store/profile/actions";
import ProfileInfo from "./ProfileInfo/ProfileInfo"
const ProfileWrapper = styled.div`
display: flex;
@ -35,6 +30,10 @@ const AvatarWrapper = styled.div`
const EditButtonWrapper = styled.div`
margin-top: 16px;
width: 160px;
& > button {
padding: 8px 20px 9px 20px;
}
`;
const ContactTextTruncate = styled.div`
@ -55,78 +54,6 @@ const ContactWrapper = styled.div`
width: 300px;
`;
const InfoContainer = styled.div`
margin-bottom: 24px;
`;
const InfoItem = styled.div`
font-family: Open Sans;
font-style: normal;
font-weight: normal;
font-size: 13px;
line-height: 24px;
display: flex;
width: 400px;
`;
const InfoItemLabel = styled.div`
width: 120px;
white-space: nowrap;
color: #A3A9AE;
`;
const InfoItemValue = styled.div`
width: 220px;
.language-combo {
padding-top: 4px;
& > div {
padding-left: 0px;
& > div {
line-height: 18px;
}
}
}
`;
const IconButtonWrapper = styled.div`
${props => props.isBefore
? `margin-right: 8px;`
: `margin-left: 8px;`
}
display: inline-flex;
:hover {
& > div > svg > path {
fill: #3B72A7;
}
}
`;
const getFormattedDepartments = departments => {
const splittedDepartments = departments.split(",");
const departmentsLength = splittedDepartments.length - 1;
const formattedDepartments = splittedDepartments.map((department, index) => {
return (
<span key={index}>
<Link type="page" fontSize={13} isHovered={true}>
{department.trim()}
</Link>
{departmentsLength !== index ? ", " : ""}
</span>
);
});
return formattedDepartments;
};
const capitalizeFirstLetter = string => {
return string && string.charAt(0).toUpperCase() + string.slice(1);
};
const createContacts = contacts => {
const styledContacts = contacts.map((contact, index) => {
return (
@ -140,278 +67,13 @@ const createContacts = contacts => {
return styledContacts;
};
class ProfileInfo extends React.PureComponent {
constructor(props) {
super(props);
this.state = this.mapPropsToState(props);
}
mapPropsToState = (props) => {
const newState = {
profile: props.profile,
dialog: {
visible: false,
header: "",
body: "",
buttons: [],
newEmail: props.profile.email,
}
};
return newState;
};
onEmailChange = e => {
const emailRegex = /.+@.+\..+/;
const newEmail = e.target.value || this.state.dialog.newEmail || this.props.profile.email;
const hasError = !emailRegex.test(newEmail);
const dialog = {
visible: true,
header: "Change email",
body: (
<Text.Body>
<span style={{ display: "block", marginBottom: "8px" }}>The activation instructions will be sent to the entered email</span>
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={newEmail}
onChange={this.onEmailChange}
hasError={hasError}
/>
</Text.Body>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendEmailChangeInstructions}
isDisabled={hasError}
/>
],
value: newEmail
};
this.setState({ dialog: dialog })
}
onSendEmailChangeInstructions = () => {
sendInstructionsToChangeEmail(this.state.profile.id, this.state.dialog.value)
.then((res) => {
res.data.error ? toastr.error(res.data.error.message) : toastr.success(res.data.response)
})
.catch((error) => toastr.error(error.message))
.finally(this.onDialogClose);
}
onSentInviteAgain = id => {
resendUserInvites(new Array(id))
.then(() => toastr.success("The invitation was successfully sent"))
.catch(e => toastr.error("ERROR"));
};
onDialogClose = value => {
const dialog = { visible: false, value: value };
this.setState({ dialog: dialog })
}
onEmailClick = (e, email) => {
if (e.target.title)
window.open("mailto:" + email);
}
render() {
const { dialog } = this.state;
const { isVisitor, email, activationStatus, department, title, mobilePhone, sex, workFrom, birthday, location, cultureName, currentCulture, id } = this.props.profile;
const isAdmin = this.props.isAdmin;
const isSelf = this.props.isSelf;
const t = this.props.t;
const type = isVisitor ? "Guest" : "Employee";
const fakeLanguage = [{
key: "en-US",
label: "English (United States)"
},
{
key: "ru-RU",
label: "Russian (Russia)"
}];
const language = cultureName || currentCulture;
const workFromDate = new Date(workFrom).toLocaleDateString(language);
const birthDayDate = new Date(birthday).toLocaleDateString(language);
const formatedSex = capitalizeFirstLetter(sex);
const formatedDepartments = getFormattedDepartments(department);
return (
<InfoContainer>
<InfoItem>
<InfoItemLabel>
{t('UserType')}:
</InfoItemLabel>
<InfoItemValue>
{type}
</InfoItemValue>
</InfoItem>
{email &&
<InfoItem>
<InfoItemLabel>
{t('Email')}:
</InfoItemLabel>
<InfoItemValue>
<Link
type="page"
fontSize={13}
isHovered={true}
title={email}
onClick={this.onEmailClick.bind(email)}
>
{activationStatus === 2 && (isAdmin || isSelf) &&
<IconButtonWrapper isBefore={true} title={t('PendingTitle')}>
<IconButton
color='#C96C27'
size={16}
iconName='DangerIcon'
isFill={true} />
</IconButtonWrapper>
}
{email}
{(isAdmin || isSelf) &&
<IconButtonWrapper title={t('EmailChangeButton')} >
<IconButton
color="#A3A9AE"
size={16}
iconName='AccessEditIcon'
isFill={true}
onClick={this.onEmailChange} />
</IconButtonWrapper>
}
{activationStatus === 2 && (isAdmin || isSelf) &&
<IconButtonWrapper title={t('SendInviteAgain')}>
<IconButton
color="#A3A9AE"
size={16}
iconName='FileActionsConvertIcon'
isFill={true}
onClick={this.onSentInviteAgain.bind(this, id)} />
</IconButtonWrapper>
}
</Link>
</InfoItemValue>
</InfoItem>
}
{department &&
<InfoItem>
<InfoItemLabel>
{t("CustomDepartment", { department: departmentName })}:
</InfoItemLabel>
<InfoItemValue>
{formatedDepartments}
</InfoItemValue>
</InfoItem>
}
{title &&
<InfoItem>
<InfoItemLabel>
{t("CustomPosition", { position })}:
</InfoItemLabel>
<InfoItemValue>
{title}
</InfoItemValue>
</InfoItem>
}
{(mobilePhone || isSelf) &&
<InfoItem>
<InfoItemLabel>
{t('PhoneLbl')}:
</InfoItemLabel>
<InfoItemValue>
{mobilePhone}
</InfoItemValue>
</InfoItem>
}
{sex &&
<InfoItem>
<InfoItemLabel>
{t('Sex')}:
</InfoItemLabel>
<InfoItemValue>
{formatedSex}
</InfoItemValue>
</InfoItem>
}
{workFrom &&
<InfoItem>
<InfoItemLabel>
{t("CustomEmployedSinceDate", { employedSinceDate })}:
</InfoItemLabel>
<InfoItemValue>
{workFromDate}
</InfoItemValue>
</InfoItem>
}
{birthday &&
<InfoItem>
<InfoItemLabel>
{t('Birthdate')}:
</InfoItemLabel>
<InfoItemValue>
{birthDayDate}
</InfoItemValue>
</InfoItem>
}
{location &&
<InfoItem>
<InfoItemLabel>
{t('Location')}:
</InfoItemLabel>
<InfoItemValue>
{location}
</InfoItemValue>
</InfoItem>
}
{isSelf &&
<InfoItem>
<InfoItemLabel>
{t('Language')}:
</InfoItemLabel>
<InfoItemValue>
<ComboBox
options={fakeLanguage}
onSelect={() => { }}
selectedOption={fakeLanguage.find(item => item.key === language)}
isDisabled={false}
noBorder={true}
dropDownMaxHeight={250}
scaled={false}
scaledOptions={true}
size='content'
className='language-combo'
/>
</InfoItemValue>
</InfoItem>
}
<ModalDialog
visible={dialog.visible}
headerContent={dialog.header}
bodyContent={dialog.body}
footerContent={dialog.buttons}
onClose={this.onDialogClose.bind(this, email)}
/>
</InfoContainer>
);
}
};
const SectionBodyContent = props => {
const { t } = useTranslation();
const { profile, history, settings, isAdmin, viewer } = props;
const { profile, updateProfileCulture, history, settings, isAdmin, viewer } = props;
const contacts = profile.contacts && getUserContacts(profile.contacts);
const role = getUserRole(profile);
const socialContacts = contacts && createContacts(contacts.social);
const socialContacts = (contacts && contacts.social && contacts.social.length > 0 && createContacts(contacts.social)) || null;
const infoContacts = contacts && createContacts(contacts.contact);
const isSelf = isMe(viewer, profile.userName);
@ -440,12 +102,13 @@ const SectionBodyContent = props => {
size="big"
scale={true}
label={t("EditUserDialogTitle")}
title={t("EditUserDialogTitle")}
onClick={onEditProfileClick}
/>
</EditButtonWrapper>
)}
</AvatarWrapper>
<ProfileInfo profile={profile} isSelf={isSelf} isAdmin={isAdmin} t={t} />
<ProfileInfo profile={profile} updateProfileCulture={updateProfileCulture} isSelf={isSelf} isAdmin={isAdmin} t={t} cultures={settings.cultures} culture={settings.culture} />
{isSelf && (
<ToggleWrapper isSelf={true} >
<ToggleContent label={t('Subscriptions')} isOpen={true} >
@ -474,7 +137,7 @@ const SectionBodyContent = props => {
</ToggleContent>
</ToggleWrapper>
)}
{profile.contacts && (
{socialContacts && (
<ToggleWrapper isContacts={true} >
<ToggleContent label={t('SocialProfiles')} isOpen={true} >
<Text.Body as="span">{socialContacts}</Text.Body>
@ -493,4 +156,4 @@ function mapStateToProps(state) {
};
}
export default connect(mapStateToProps)(withRouter(SectionBodyContent));
export default connect(mapStateToProps, { updateProfileCulture })(withRouter(SectionBodyContent));

View File

@ -1,6 +1,6 @@
import React from "react";
import React, { useCallback } from "react";
import { connect } from "react-redux";
import { Text, IconButton, ContextMenuButton, toastr } from "asc-web-components";
import { Text, IconButton, ContextMenuButton, toastr, utils } from "asc-web-components";
import { withRouter } from "react-router";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
import { getUserStatus } from "../../../../../store/people/selectors";
@ -8,19 +8,25 @@ import { useTranslation } from 'react-i18next';
import { resendUserInvites } from "../../../../../store/services/api";
import { EmployeeStatus } from "../../../../../helpers/constants";
import { updateUserStatus } from "../../../../../store/people/actions";
import { fetchProfile } from '../../../../../store/profile/actions';
import styled from 'styled-components';
const wrapperStyle = {
display: "flex",
alignItems: "center"
};
const textStyle = {
marginLeft: "16px",
marginRight: "16px"
};
const Header = styled(Text.ContentHeader)`
margin-left: 16px;
margin-right: 16px;
max-width: calc(100vw - 430px);
@media ${utils.device.tablet} {
max-width: calc(100vw - 96px);
}
`;
const SectionHeaderContent = props => {
const { profile, history, settings, isAdmin, viewer, updateUserStatus } = props;
const { profile, history, settings, isAdmin, viewer, updateUserStatus, fetchProfile } = props;
const selectedUserIds = new Array(profile.id);
@ -41,8 +47,9 @@ const SectionHeaderContent = props => {
};
const onDisableClick = () => {
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds)
.then(() => toastr.success(t("SuccessChangeUserStatus")))
.then(() => fetchProfile(profile.id));
};
const onEditPhoto = () => {
@ -50,8 +57,9 @@ const SectionHeaderContent = props => {
};
const onEnableClick = () => {
updateUserStatus(EmployeeStatus.Active, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
updateUserStatus(EmployeeStatus.Active, selectedUserIds)
.then(() => toastr.success(t("SuccessChangeUserStatus")))
.then(() => fetchProfile(profile.id));
};
const onReassignDataClick = user => {
@ -70,7 +78,7 @@ const SectionHeaderContent = props => {
const onInviteAgainClick = () => {
resendUserInvites(selectedUserIds)
.then(() => toastr.success("The invitation was successfully sent"))
.catch(e => toastr.error("ERROR"));
.catch(error => toastr.error(error));
};
const getUserContextOptions = (user, viewer, t) => {
@ -174,6 +182,10 @@ const SectionHeaderContent = props => {
const { t } = useTranslation();
const contextOptions = () => getUserContextOptions(profile, viewer, t);
const onClick = useCallback(() => {
history.goBack();
}, [history]);
return (
<div style={wrapperStyle}>
<div style={{ width: "16px" }}>
@ -181,13 +193,13 @@ const SectionHeaderContent = props => {
iconName={"ArrowPathIcon"}
color="#A3A9AE"
size="16"
onClick={() => history.push(settings.homepage)}
onClick={onClick}
/>
</div>
<Text.ContentHeader truncate={true} style={textStyle}>
<Header truncate={true}>
{profile.displayName}
{profile.isLDAP && ` (${t('LDAPLbl')})`}
</Text.ContentHeader>
</Header>
{(isAdmin || isMe(viewer, profile.userName)) && (
<ContextMenuButton
directionX="right"
@ -211,4 +223,4 @@ function mapStateToProps(state) {
};
}
export default connect(mapStateToProps, { updateUserStatus })(withRouter(SectionHeaderContent));
export default connect(mapStateToProps, { updateUserStatus, fetchProfile })(withRouter(SectionHeaderContent));

View File

@ -5,35 +5,8 @@ import config from "../../../../package.json";
const newInstance = i18n.createInstance();
if (process.env.NODE_ENV === "production") {
newInstance
.use(Backend)
.init({
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
react: {
useSuspense: true
},
backend: {
loadPath: `${config.homepage}/locales/Profile/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
}
};
newInstance.init({
resources: resources,
lng: 'en',
newInstance.use(Backend).init({
lng: "en",
fallbackLng: "en",
debug: true,
@ -43,6 +16,33 @@ if (process.env.NODE_ENV === "production") {
react: {
useSuspense: true
},
backend: {
loadPath: `${config.homepage}/locales/Profile/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
newInstance.init({
resources: resources,
lng: "en",
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
react: {
useSuspense: false
}
});
}

View File

@ -43,36 +43,34 @@ class PureProfile extends React.Component {
render() {
console.log("Profile render")
const { profile } = this.props;
return (
profile
?
<PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={
<SectionHeaderContent profile={profile} />
}
sectionBodyContent={
<SectionBodyContent profile={profile} />
}
/>
: <PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionBodyContent={
<Loader className="pageLoader" type="rombs" size={40} />
}
/>
);
const { profile, isVisitor } = this.props;
const articleProps = isVisitor ? {} : {
articleHeaderContent: <ArticleHeaderContent />,
articleMainButtonContent: <ArticleMainButtonContent />,
articleBodyContent: <ArticleBodyContent />
};
const sectionProps = profile ? {
sectionHeaderContent: <SectionHeaderContent profile={profile} />,
sectionBodyContent: <SectionBodyContent profile={profile} />
} : {
sectionBodyContent: <Loader className="pageLoader" type="rombs" size={40} />
};
return <PageLayout {...articleProps} {...sectionProps} />;
};
};
const ProfileContainer = withTranslation()(PureProfile);
const Profile = (props) => <I18nextProvider i18n={i18n}><ProfileContainer {...props} /></I18nextProvider>;
const Profile = (props) => {
const { language } = props;
i18n.changeLanguage(language);
return <I18nextProvider i18n={i18n}><ProfileContainer {...props} /></I18nextProvider>
};
Profile.propTypes = {
history: PropTypes.object.isRequired,
@ -84,7 +82,9 @@ Profile.propTypes = {
function mapStateToProps(state) {
return {
profile: state.profile.targetUser
profile: state.profile.targetUser,
language: state.auth.user.cultureName || state.auth.settings.culture,
isVisitor: state.auth.user.isVisitor,
};
}

View File

@ -22,6 +22,7 @@
"EditButton": "Edit",
"Actions": "Actions",
"ChangeEmailSuccess": "Mail has been successfully changed",
"NotFoundLanguage": "In case you cannot find your language in the list of the available ones, feel free to write to us at <1>{{supportEmail}}</1> to take part in the translation and get up to 1 year free of charge.",
"PhoneChange": "Change phone",
"PhoneLbl": "Phone",
@ -31,5 +32,9 @@
"CustomEmployedSinceDate": "{{employedSinceDate}}",
"CustomPosition": "{{position}}",
"CustomDepartment": "{{department}}"
"CustomDepartment": "{{department}}",
"Culture_en": "English (United Kingdom)",
"Culture_en-US": "English (United States)",
"Culture_ru-RU": "Russian (Russia)",
"LearnMore": "Learn more..."
}

View File

@ -0,0 +1,40 @@
{
"UserType": "Тип",
"Email": "Email",
"Sex": "Пол",
"Birthdate": "Дата рождения",
"Location": "Местоположение",
"Language": "Язык",
"EditUserDialogTitle": "Редактировать",
"Subscriptions": "Подписки",
"Comments": "Комментарии",
"ContactInformation": "Контактные данные",
"PendingTitle": "Pending",
"EmailChangeButton": "Изменить email",
"SendInviteAgain": "Отправить приглашение ещё раз",
"EditPhoto": "Изменить фотографию",
"PasswordChangeButton": "Измененить пароль",
"DisableUserButton": "Disable",
"EnableUserButton": "Enable",
"ReassignData": "Reassign data",
"RemoveData": "Delete personal data",
"DeleteSelfProfile": "Delete profile",
"EditButton": "Редактировать",
"Actions": "Actions",
"ChangeEmailSuccess": "Mail has been successfully changed",
"NotFoundLanguage": "Если Вы не можете найти свой язык в списке доступных, Вы всегда можете написать нам по адресу <1>{{supportEmail}}</1>, чтобы принять участие в переводе и получить до 1 года бесплатного использования.",
"PhoneChange": "Измененить номер телефона",
"PhoneLbl": "Основной телефон",
"EditSubscriptionsBtn": "Edit subscriptions",
"InviteAgainLbl": "Invite again",
"LDAPLbl": "LDAP",
"CustomEmployedSinceDate": "{{employedSinceDate}}",
"CustomPosition": "{{position}}",
"CustomDepartment": "{{department}}",
"Culture_en": "Английский (Великобритания)",
"Culture_en-US": "Английский (США)",
"Culture_ru-RU": "Русский (Россия)",
"LearnMore": "Подробнее..."
}

View File

@ -14,6 +14,7 @@ class DateField extends React.Component {
isRequired,
hasError,
labelText,
calendarHeaderContent,
inputName,
inputValue,
@ -35,6 +36,8 @@ class DateField extends React.Component {
onChange={inputOnChange}
hasError={hasError}
tabIndex={inputTabIndex}
displayType="auto"
calendarHeaderContent={calendarHeaderContent}
/>
</FieldContainer>
);

View File

@ -51,7 +51,7 @@ class DepartmentField extends React.Component {
className="department-add-btn"
/>
<AdvancedSelector
isDropDown={true}
displayType="dropdown"
isOpen={selectorIsVisible}
maxHeight={336}
width={379}

View File

@ -19,7 +19,9 @@ class RadioField extends React.Component {
radioValue,
radioOptions,
radioIsDisabled,
radioOnChange
radioOnChange,
tooltipContent
} = this.props;
return (
@ -27,6 +29,7 @@ class RadioField extends React.Component {
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
tooltipContent={tooltipContent}
>
<RadioButtonGroup
name={radioName}

View File

@ -30,7 +30,9 @@ class TextChangeField extends React.Component {
buttonText,
buttonIsDisabled,
buttonOnClick,
buttonTabIndex
buttonTabIndex,
tooltipContent
} = this.props;
return (
@ -38,6 +40,7 @@ class TextChangeField extends React.Component {
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
tooltipContent={tooltipContent}
>
<InputContainer>
<TextInput

View File

@ -20,7 +20,8 @@ class TextField extends React.Component {
inputIsDisabled,
inputOnChange,
inputAutoFocussed,
inputTabIndex
inputTabIndex,
tooltipContent
} = this.props;
return (
@ -28,6 +29,7 @@ class TextField extends React.Component {
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
tooltipContent={tooltipContent}
>
<TextInput
name={inputName}

View File

@ -1,10 +1,10 @@
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { Avatar, Button, Textarea, toastr, AvatarEditor } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { Avatar, Button, Textarea, toastr, AvatarEditor, Text } from 'asc-web-components'
import { withTranslation, Trans } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts, mapGroupsToGroupSelectorOptions, mapGroupSelectorOptionsToGroups, filterGroupSelectorOptions } from "../../../../../store/people/selectors";
import { createProfile, loadAvatar } from '../../../../../store/profile/actions';
import { createProfile } from '../../../../../store/profile/actions';
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import PasswordField from './FormFields/PasswordField'
@ -14,6 +14,7 @@ import DepartmentField from './FormFields/DepartmentField'
import ContactsField from './FormFields/ContactsField'
import InfoFieldContainer from './FormFields/InfoFieldContainer'
import { departments, department, position, employedSinceDate } from '../../../../../helpers/customNames';
import { createThumbnailsAvatar, loadAvatar } from "../../../../../store/services/api";
class CreateUserForm extends React.Component {
@ -43,38 +44,86 @@ class CreateUserForm extends React.Component {
this.onSaveAvatar = this.onSaveAvatar.bind(this);
this.onCloseAvatarEditor = this.onCloseAvatarEditor.bind(this);
this.createAvatar = this.createAvatar.bind(this);
this.onLoadFileAvatar = this.onLoadFileAvatar.bind(this);
}
createAvatar(userId,userName){
this.props.updateAvatar(
userId,
{
croppedImage: this.state.croppedAvatarImage,
defaultImage: this.state.defaultAvatarImage
})
.then((result) => {
createThumbnailsAvatar(userId, {
x: this.state.avatar.x,
y: this.state.avatar.y,
width: this.state.avatar.width,
height: this.state.avatar.height,
tmpFile: this.state.avatar.tmpFile
})
.then(() => {
toastr.success("Success");
this.props.history.push(`${this.props.settings.homepage}/view/${userName}`);
})
.catch((error) => {
toastr.error(error.message);
this.props.history.push(`${this.props.settings.homepage}/view/${userName}`);
});
})
.catch((error) => toastr.error(error));
}
openAvatarEditor(){
let avatarDefault = this.state.profile.avatarDefault ? "data:image/png;base64," + this.state.profile.avatarDefault : null;
let _this = this;
if(avatarDefault !== null){
let img = new Image();
img.onload = function () {
_this.setState({
avatar:{
defaultWidth: img.width,
defaultHeight: img.height
}
})
};
img.src = avatarDefault;
}
this.setState({
visibleAvatarEditor: true,
});
}
onSaveAvatar(result){
this.setState({
croppedAvatarImage: result.croppedImage,
defaultAvatarImage: result.defaultImage,
})
onLoadFileAvatar(file) {
let data = new FormData();
let _this = this;
data.append("file", file);
data.append("Autosave", false);
loadAvatar(0, data)
.then((response) => {
var img = new Image();
img.onload = function () {
var stateCopy = Object.assign({}, _this.state);
stateCopy.avatar = {
tmpFile: response.data,
image: response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
_this.setState(stateCopy);
};
img.src = response.data;
})
.catch((error) => toastr.error(error));
}
onSaveAvatar(isUpdate, result, file){
var stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.croppedAvatarImage = file;
if(isUpdate){
stateCopy.avatar.x = Math.round(result.x*this.state.avatar.defaultWidth - result.width/2);
stateCopy.avatar.y = Math.round(result.y*this.state.avatar.defaultHeight - result.height/2);
stateCopy.avatar.width = result.width;
stateCopy.avatar.height = result.height;
}
this.setState(stateCopy);
}
onCloseAvatarEditor(){
this.setState({
visibleAvatarEditor: false,
croppedAvatarImage: "",
avatar:{
tmpFile: ""
}
});
}
@ -95,7 +144,6 @@ class CreateUserForm extends React.Component {
return {
visibleAvatarEditor: false,
croppedAvatarImage: "",
defaultAvatarImage: "",
isLoading: false,
errors: {
firstName: false,
@ -109,6 +157,16 @@ class CreateUserForm extends React.Component {
allOptions: allOptions,
options: [...allOptions],
selected: selected
},
avatar: {
tmpFile:"",
image: profile.avatarDefault ? "data:image/png;base64," + profile.avatarDefault : null,
defaultWidth: 0,
defaultHeight: 0,
x: 0,
y: 0,
width: 0,
height: 0
}
};
}
@ -153,12 +211,15 @@ class CreateUserForm extends React.Component {
this.props.createProfile(this.state.profile)
.then((profile) => {
toastr.success("Success");
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
//if(this.state.defaultImage !== '') this.createAvatar(profile.id,profile.userName);
if(this.state.avatar.tmpFile !== ""){
this.createAvatar(profile.id,profile.userName);
}else{
toastr.success("Success");
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
}
})
.catch((error) => {
toastr.error(error.message)
toastr.error(error);
this.setState({ isLoading: false })
});
}
@ -232,7 +293,7 @@ class CreateUserForm extends React.Component {
render() {
const { isLoading, errors, profile, selector } = this.state;
const { t, settings } = this.props;
const { t, settings, i18n } = this.props;
const pattern = getUserContactsPattern();
const contacts = getUserContacts(profile.contacts);
@ -249,11 +310,19 @@ class CreateUserForm extends React.Component {
editLabel={t("AddPhoto")}
editAction={this.openAvatarEditor}
/>
<AvatarEditor
image={profile.avatarDefault ? "data:image/png;base64,"+profile.avatarDefault : null}
visible={this.state.visibleAvatarEditor}
onClose={this.onCloseAvatarEditor}
onSave={this.onSaveAvatar} />
<AvatarEditor
image={this.state.avatar.image}
visible={this.state.visibleAvatarEditor}
onClose={this.onCloseAvatarEditor}
onSave={this.onSaveAvatar}
onLoadFile={this.onLoadFileAvatar}
headerLabel={t("editAvatar")}
chooseFileLabel ={t("chooseFileLabel")}
unknownTypeError={t("unknownTypeError")}
maxSizeFileError={t("maxSizeFileError")}
unknownError ={t("unknownError")}
/>
</AvatarContainer>
<MainFieldsContainer>
<TextField
@ -286,6 +355,12 @@ class CreateUserForm extends React.Component {
inputIsDisabled={isLoading}
inputOnChange={this.onInputChange}
inputTabIndex={3}
tooltipContent={
<Trans i18nKey="EmailPopupHelper" i18n={i18n}>
The main e-mail is needed to restore access to the portal in case of loss of the password and send notifications. <p className="tooltip_email" style={{marginTop: "1rem", marginBottom: "1rem"}} >You can create a new mail on the domain as the primary. In this case, you must set a one-time password so that the user can log in to the portal for the first time.</p> The main e-mail can be used as a login when logging in to the portal.
</Trans>
}
/>
<PasswordField
isRequired={true}
@ -309,6 +384,7 @@ class CreateUserForm extends React.Component {
passwordSettings={settings.passwordSettings}
/>
<DateField
calendarHeaderContent={t("CalendarSelectDate")}
labelText={`${t("Birthdate")}:`}
inputName="birthday"
inputValue={profile.birthday ? new Date(profile.birthday) : undefined}
@ -328,6 +404,7 @@ class CreateUserForm extends React.Component {
radioOnChange={this.onInputChange}
/>
<DateField
calendarHeaderContent={t("CalendarSelectDate")}
labelText={`${t("CustomEmployedSinceDate", { employedSinceDate })}:`}
inputName="workFrom"
inputValue={profile.workFrom ? new Date(profile.workFrom) : undefined}
@ -402,7 +479,6 @@ class CreateUserForm extends React.Component {
);
};
}
const mapStateToProps = (state) => {
return {
settings: state.auth.settings,
@ -413,7 +489,6 @@ const mapStateToProps = (state) => {
export default connect(
mapStateToProps,
{
createProfile,
loadAvatar
createProfile
}
)(withRouter(withTranslation()(CreateUserForm)));

View File

@ -1,10 +1,10 @@
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput, AvatarEditor } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput, AvatarEditor, Link } from 'asc-web-components'
import { withTranslation, Trans } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts, mapGroupsToGroupSelectorOptions, mapGroupSelectorOptionsToGroups, filterGroupSelectorOptions } from "../../../../../store/people/selectors";
import { updateProfile, loadAvatar, createThumbnailsAvatar, deleteAvatar } from '../../../../../store/profile/actions';
import { updateProfile } from '../../../../../store/profile/actions';
import { sendInstructionsToChangePassword, sendInstructionsToChangeEmail } from "../../../../../store/services/api";
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
@ -15,6 +15,20 @@ import DepartmentField from './FormFields/DepartmentField'
import ContactsField from './FormFields/ContactsField'
import InfoFieldContainer from './FormFields/InfoFieldContainer'
import { departments, department, position, employedSinceDate, typeGuest, typeUser } from '../../../../../helpers/customNames';
import { createThumbnailsAvatar, loadAvatar, deleteAvatar } from "../../../../../store/services/api";
import styled from "styled-components";
const Table = styled.table`
width: 100%;
margin-bottom: 23px;
`;
const Th = styled.th`
padding: 11px 0 10px 0px;
border-top: 1px solid #ECEEF1;
`;
const Td = styled.td``;
class UpdateUserForm extends React.Component {
@ -153,8 +167,8 @@ class UpdateUserForm extends React.Component {
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
})
.catch((error) => {
toastr.error(error.message)
this.setState({isLoading: false})
toastr.error(error);
this.setState({isLoading: false});
});
}
@ -201,9 +215,9 @@ class UpdateUserForm extends React.Component {
onSendEmailChangeInstructions() {
sendInstructionsToChangeEmail(this.state.profile.id, this.state.dialog.newEmail)
.then((res) => {
res.data.error ? toastr.error(res.data.error.message) : toastr.success(res.data.response)
toastr.success(res);
})
.catch((error) => toastr.error(error.message))
.catch((error) => toastr.error(error))
.finally(this.onDialogClose);
}
@ -232,9 +246,9 @@ class UpdateUserForm extends React.Component {
onSendPasswordChangeInstructions() {
sendInstructionsToChangePassword(this.state.profile.email)
.then((res) => {
res.data.error ? toastr.error(res.data.error.message) : toastr.success(res.data.response)
toastr.success(res);
})
.catch((error) => toastr.error(error.message))
.catch((error) => toastr.error(error))
.finally(this.onDialogClose);
}
@ -324,61 +338,51 @@ class UpdateUserForm extends React.Component {
let _this = this;
data.append("file", file);
data.append("Autosave", false);
this.props.loadAvatar(this.state.profile.id, data)
.then((result) => {
loadAvatar(this.state.profile.id, data)
.then((response) => {
var img = new Image();
img.onload = function () {
var stateCopy = Object.assign({}, _this.state);
stateCopy.avatar = {
tmpFile: result.data.response.data,
image: result.data.response.data,
tmpFile: response.data,
image: response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
_this.setState(stateCopy);
};
img.src = result.data.response.data;
img.src = response.data;
})
.catch((error) => {
toastr.error(error.message);
});
.catch((error) => toastr.error(error));
}
onSaveAvatar(isUpdate, result) {
if(isUpdate){
this.props.createThumbnailsAvatar(this.state.profile.id, {
createThumbnailsAvatar(this.state.profile.id, {
x: Math.round(result.x*this.state.avatar.defaultWidth - result.width/2),
y: Math.round(result.y*this.state.avatar.defaultHeight - result.height/2),
width: result.width,
height: result.height,
tmpFile: this.state.avatar.tmpFile
})
.then((result) => {
if(result.status === 200){
.then((response) => {
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.avatar.tmpFile = '';
stateCopy.profile.avatarMax = result.data.response.max + '?_='+Math.floor(Math.random() * Math.floor(10000));
stateCopy.profile.avatarMax = response.max + '?_='+Math.floor(Math.random() * Math.floor(10000));
toastr.success("Success");
this.setState(stateCopy);
}
})
.catch((error) => {
toastr.error(error.message);
});
.catch((error) => toastr.error(error));
}else{
this.props.deleteAvatar(this.state.profile.id)
.then((result) => {
if(result.status === 200){
deleteAvatar(this.state.profile.id)
.then((response) => {
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.profile.avatarMax = result.data.response.big;
stateCopy.profile.avatarMax = response.big;
toastr.success("Success");
this.setState(stateCopy);
}
})
.catch((error) => {
toastr.error(error.message);
});
.catch((error) => toastr.error(error));
}
}
onCloseAvatarEditor() {
@ -422,10 +426,49 @@ class UpdateUserForm extends React.Component {
render() {
const { isLoading, errors, profile, dialog, selector } = this.state;
const { t } = this.props;
const { t, i18n } = this.props;
const pattern = getUserContactsPattern();
const contacts = getUserContacts(profile.contacts);
const tooltipTypeContent =
<>
<Text.Body style={{paddingBottom: 17}} fontSize={13}>{t("ProfileTypePopupHelper")}</Text.Body>
<Table>
<tbody>
<tr>
<Th>{t("ProductsAndInstruments_Products")}</Th><Th>{t("Employee")}</Th><Th>{t("GuestCaption")}</Th>
</tr>
<tr>
<Td>{t("Mail")}</Td><Td>review</Td><Td>-</Td>
</tr>
<tr>
<Td>{t("DocumentsProduct")}</Td><Td>full access</Td><Td>view</Td>
</tr>
<tr>
<Td>{t("ProjectsProduct")}</Td><Td>review</Td><Td>-</Td>
</tr>
<tr>
<Td>{t("CommunityProduct")}</Td><Td>full access</Td><Td>view</Td>
</tr>
<tr>
<Td>{t("People")}</Td><Td>review</Td><Td>-</Td>
</tr>
<tr>
<Td>{t("Message")}</Td><Td>review</Td><Td>review</Td>
</tr>
<tr>
<Td>{t("Calendar")}</Td><Td>review</Td><Td>review</Td>
</tr>
</tbody>
</Table>
<Link
color="#316DAA"
isHovered={true}
href="https://helpcenter.onlyoffice.com/ru/gettingstarted/people.aspx#ManagingAccessRights_block"
style={{marginTop: 23}}>
{t("TermsOfUsePopupHelperLink")}
</Link>
</>;
return (
<>
@ -446,6 +489,7 @@ class UpdateUserForm extends React.Component {
onClose={this.onCloseAvatarEditor}
onSave={this.onSaveAvatar}
onLoadFile={this.onLoadFileAvatar}
headerLabel={t("editAvatar")}
chooseFileLabel ={t("chooseFileLabel")}
unknownTypeError={t("unknownTypeError")}
maxSizeFileError={t("maxSizeFileError")}
@ -461,6 +505,12 @@ class UpdateUserForm extends React.Component {
buttonIsDisabled={isLoading}
buttonOnClick={this.onEmailChange}
buttonTabIndex={1}
tooltipContent={
<Trans i18nKey="EmailPopupHelper" i18n={i18n}>
The main e-mail is needed to restore access to the portal in case of loss of the password and send notifications. <p style={{height: "0", visibility: "hidden"}}>You can create a new mail on the domain as the primary. In this case, you must set a one-time password so that the user can log in to the portal for the first time.</p> The main e-mail can be used as a login when logging in to the portal.
</Trans>
}
/>
<TextChangeField
labelText={`${t("Password")}:`}
@ -502,6 +552,7 @@ class UpdateUserForm extends React.Component {
inputTabIndex={5}
/>
<DateField
calendarHeaderContent={t("CalendarSelectDate")}
labelText={`${t("Birthdate")}:`}
inputName="birthday"
inputValue={profile.birthday ? new Date(profile.birthday) : undefined}
@ -530,8 +581,11 @@ class UpdateUserForm extends React.Component {
]}
radioIsDisabled={isLoading}
radioOnChange={this.onUserTypeChange}
tooltipContent={tooltipTypeContent}
/>
<DateField
calendarHeaderContent={t("CalendarSelectDate")}
labelText={`${t("CustomEmployedSinceDate", { employedSinceDate })}:`}
inputName="workFrom"
inputValue={profile.workFrom ? new Date(profile.workFrom) : undefined}
@ -625,9 +679,6 @@ const mapStateToProps = (state) => {
export default connect(
mapStateToProps,
{
updateProfile,
loadAvatar,
deleteAvatar,
createThumbnailsAvatar
updateProfile
}
)(withRouter(withTranslation()(UpdateUserForm)));

View File

@ -2,7 +2,7 @@ import React, { useCallback } from 'react';
import styled from 'styled-components';
import { connect } from 'react-redux';
import { withRouter } from "react-router";
import { IconButton, Text } from 'asc-web-components';
import { IconButton, Text, utils } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import {typeUser, typeGuest } from './../../../../../helpers/customNames';
@ -13,6 +13,10 @@ const Wrapper = styled.div`
const Header = styled(Text.ContentHeader)`
margin-left: 16px;
max-width: calc(100vw - 430px);
@media ${utils.device.tablet} {
max-width: calc(100vw - 64px);
}
`;
const SectionHeaderContent = (props) => {
@ -29,13 +33,13 @@ const SectionHeaderContent = (props) => {
: "";
const onClick = useCallback(() => {
history.push(settings.homepage)
}, [history, settings]);
history.goBack();
}, [history]);
return (
<Wrapper>
<IconButton iconName={'ArrowPathIcon'} size="16" onClick={onClick}/>
<Header>{headerText}</Header>
<Header truncate={true}>{headerText}</Header>
</Wrapper>
);
};

View File

@ -28,6 +28,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -42,7 +45,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -2,14 +2,21 @@ import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { PageLayout, Loader } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
import { SectionHeaderContent, CreateUserForm, UpdateUserForm } from './Section';
import { fetchProfile } from '../../../store/profile/actions';
import {
ArticleHeaderContent,
ArticleMainButtonContent,
ArticleBodyContent
} from "../../Article";
import {
SectionHeaderContent,
CreateUserForm,
UpdateUserForm
} from "./Section";
import { fetchProfile } from "../../../store/profile/actions";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
class ProfileAction extends React.Component {
componentDidMount() {
const { match, fetchProfile } = this.props;
const { userId } = match.params;
@ -30,34 +37,42 @@ class ProfileAction extends React.Component {
}
render() {
console.log("ProfileAction render")
console.log("ProfileAction render");
let loaded = false;
const { profile, match } = this.props;
const { profile, isVisitor, match, language } = this.props;
const { userId, type } = match.params;
i18n.changeLanguage(language);
if (type) {
loaded = true;
} else if (profile) {
loaded = profile.userName === userId || profile.id === userId;
}
const articleProps = isVisitor
? {}
: {
articleHeaderContent: <ArticleHeaderContent />,
articleMainButtonContent: <ArticleMainButtonContent />,
articleBodyContent: <ArticleBodyContent />
};
const sectionProps = loaded
? {
sectionHeaderContent: <SectionHeaderContent />,
sectionBodyContent: type ? <CreateUserForm /> : <UpdateUserForm />
}
: {
sectionBodyContent: (
<Loader className="pageLoader" type="rombs" size={40} />
)
};
return (
<I18nextProvider i18n={i18n}>
{loaded
? <PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={<SectionHeaderContent />}
sectionBodyContent={type ? <CreateUserForm /> : <UpdateUserForm />}
/>
: <PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionBodyContent={<Loader className="pageLoader" type="rombs" size={40} />}
/>}
<PageLayout {...articleProps} {...sectionProps} />
</I18nextProvider>
);
}
@ -71,10 +86,15 @@ ProfileAction.propTypes = {
function mapStateToProps(state) {
return {
profile: state.profile.targetUser
profile: state.profile.targetUser,
language: state.auth.user.cultureName || state.auth.settings.culture,
isVisitor: state.auth.user.isVisitor,
};
}
export default connect(mapStateToProps, {
fetchProfile
})(ProfileAction);
export default connect(
mapStateToProps,
{
fetchProfile
}
)(ProfileAction);

View File

@ -18,6 +18,17 @@
"SocialProfiles": "Social Profiles",
"Search": "Search",
"SelectAll": "Select all",
"EmailPopupHelper": "The main e-mail is needed to restore access to the portal in case of loss of the password and send notifications. <1> You can create a new mail on the domain as the primary. In this case, you must set a one-time password so that the user can log in to the portal for the first time.</1> The main e-mail can be used as a login when logging in to the portal.",
"ProfileTypePopupHelper": "Guests have limited access to some portal features and modules",
"ProductsAndInstruments_Products": "Modules",
"GuestCaption": "Guest",
"TermsOfUsePopupHelperLink": "Read more about terms of use",
"Mail": "Mail",
"DocumentsProduct": "Documents",
"ProjectsProduct": "Projects",
"CommunityProduct": "Community",
"People": "People",
"Message": "Talk",
"ActivationLink": "Activation link",
"AddPhoto": "Add photo",
@ -37,8 +48,12 @@
"CustomNewGuest": "New {{typeGuest, lowercase}}",
"CustomAddDepartments": "Add {{departments, lowercase}}",
"chooseFileLabel": "Drop files here, or click to select files",
"chooseFileLabel": "Drop file here, or click to select file",
"unknownTypeError": "Unknown image file type",
"maxSizeFileError": "Maximum file size exceeded",
"unknownError": "Error"
"unknownError": "Error",
"editAvatar": "Edit photo",
"CalendarSelectDate": "Select Date:",
"Employee": "Employee",
"Calendar": "Calendar"
}

View File

@ -0,0 +1,59 @@
{
"EditPhoto": "Изменить фотографию",
"FirstName": "Имя",
"LastName": "Фамилия",
"Email": "Email",
"Password": "Пароль",
"Birthdate": "Дата рождения",
"Sex": "Пол",
"Location": "Местоположение",
"Comments": "Комментарии",
"SaveButton": "Сохранить",
"CancelButton": "Отмена",
"CopyEmailAndPassword": "Копировать email и пароль",
"UserType": "Тип",
"AddButton": "Добавить",
"ContactInformation": "Контактные данные",
"AddContact": "Добавить новый контакт",
"SocialProfiles": "Социальные профили",
"Search": "Поиск",
"SelectAll": "Выбрать все",
"EmailPopupHelper": "Основной email нужен для восстановления доступа к порталу в случае потери пароля, а также для отправки оповещений. <1>Вы можете создать новый email на домене в качестве основного. В этом случае потребуется задать одноразовый пароль, чтобы пользователь смог войти на портал в первый раз.</1> Основной email можно использовать как логин при входе на портал.",
"ProfileTypePopupHelper": "Гости имеют ограниченный доступ к некоторым функциям и модулям портала",
"ProductsAndInstruments_Products": "Модули",
"GuestCaption": "Гость",
"TermsOfUsePopupHelperLink": "Подробнее об условиях использования",
"Mail": "Почта",
"DocumentsProduct": "Документы",
"ProjectsProduct": "Проекты",
"CommunityProduct": "Сообщество",
"People": "Люди",
"Message": "Чат",
"ActivationLink": "Activation link",
"AddPhoto": "Add photo",
"TemporaryPassword": "Temporary password",
"SexMale": "Мужской",
"SexFemale": "Женский",
"RequiredField": "Обязательное поле",
"ChangeButton": "Изменить",
"Phone": "Телефон",
"CustomEmployedSinceDate": "{{employedSinceDate}}",
"CustomPosition": "{{position}}",
"CustomDepartment": "{{department}}",
"CustomTypeGuest": "{{typeGuest}}",
"CustomTypeUser": "{{typeUser}}",
"CustomNewEmployee": "Новый {{typeUser, lowercase}}",
"CustomNewGuest": "Новый {{typeGuest, lowercase}}",
"CustomAddDepartments": "Добавить {{departments, lowercase}}",
"chooseFileLabel": "Перетащите файл сюда или нажмите, чтобы выбрать файл",
"unknownTypeError": "Неизвестный тип файла изображения",
"maxSizeFileError": "Превышен максимальный размер файла",
"unknownError": "Ошибка",
"editAvatar": "Изменить фотографию",
"CalendarSelectDate": "Выберите дату:",
"Employee": "Гость",
"Calendar": "Календарь"
}

View File

@ -1,7 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import Cookies from "universal-cookie";
//import Cookies from "universal-cookie";
import setAuthorizationToken from "./store/services/setAuthorizationToken";
import { AUTH_KEY } from "./helpers/constants";
import store from "./store/store";
@ -11,7 +11,8 @@ import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { setIsLoaded, getUserInfo } from "./store/auth/actions";
var token = new Cookies().get(AUTH_KEY);
//var token = new Cookies().get(AUTH_KEY);
const token = localStorage.getItem(AUTH_KEY);
if (token) {
setAuthorizationToken(token);

View File

@ -51,7 +51,8 @@
"DeleteSelfProfile",
"EditButton",
"ChangeEmailSuccess",
"Actions"
"Actions",
"NotFoundLanguage"
]
},
"ProfileAction": {
@ -68,7 +69,21 @@
"SaveButton",
"CopyEmailAndPassword",
"UserType",
"CancelButton"
"CancelButton",
"EmailPopupHelper",
"ProfileTypePopupHelper",
"ProductsAndInstruments_Products",
"GuestCaption",
"TermsOfUsePopupHelperLink",
"Mail",
"People"
],
"FeedResource": [
"DocumentsProduct",
"ProjectsProduct",
"CommunityProduct",
"Message"
]
},
"Home": {

View File

@ -1,7 +1,8 @@
import * as api from "../services/api";
import { setGroups, fetchPeopleAsync } from "../people/actions";
import setAuthorizationToken from "../../store/services/setAuthorizationToken";
import { fetchGroups, fetchPeople } from "../people/actions";
import { setAuthorizationToken } from "../../store/services/client";
import { getFilterByLocation } from "../../helpers/converters";
import config from "../../../package.json";
export const LOGIN_POST = "LOGIN_POST";
export const SET_CURRENT_USER = "SET_CURRENT_USER";
@ -48,37 +49,28 @@ export async function getUserInfo(dispatch) {
const { user, modules, settings } = await api.getInitInfo();
let newSettings = settings;
if (user.isAdmin) {
const inviteLinkResp = await api.getInvitationLinks();
newSettings = Object.assign(newSettings, inviteLinkResp);
const inviteLinks = await api.getInvitationLinks();
newSettings = Object.assign(newSettings, inviteLinks);
}
dispatch(setCurrentUser(user));
dispatch(setModules(modules));
dispatch(setSettings(newSettings));
const groupResp = await api.getGroupList();
await fetchGroups(dispatch);
dispatch(setGroups(groupResp.data.response));
var re = new RegExp(`${config.homepage}((/?)$|/filter)`, "gm");
const match = window.location.pathname.match(re);
const newFilter = getFilterByLocation(window.location);
await fetchPeopleAsync(dispatch, newFilter);
if (match && match.length > 0)
{
const newFilter = getFilterByLocation(window.location);
await fetchPeople(newFilter, dispatch);
}
return dispatch(setIsLoaded(true));
}
export function login(data) {
return dispatch => {
return api
.login(data)
.then(res => {
const token = res.data.response.token;
setAuthorizationToken(token);
})
.then(() => getUserInfo(dispatch));
};
}
export function logout() {
return dispatch => {
setAuthorizationToken();

View File

@ -4,4 +4,8 @@ export function isAdmin(user) {
export function isMe(user, userName) {
return userName === "@self" || userName === user.userName;
};
};
export function getCurrentModule(modules, currentModuleId) {
return modules.find(module => module.id === currentModuleId);
}

View File

@ -1,5 +1,6 @@
import * as api from "../../store/services/api";
import { setGroups, fetchPeopleByFilter } from "../people/actions";
import { setGroups, fetchPeople } from "../people/actions";
import history from "../../history";
export const SET_GROUP = "SET_GROUP";
export const CLEAN_GROUP = "CLEAN_GROUP";
@ -17,42 +18,24 @@ export function resetGroup() {
};
}
export function checkResponseError(res) {
if (res && res.data && res.data.error) {
console.error(res.data.error);
throw new Error(res.data.error.message);
}
}
export function fetchGroup(groupId) {
return dispatch => {
api.getGroup(groupId).then(res => {
checkResponseError(res);
dispatch(setGroup(res.data.response || null));
});
api.getGroup(groupId)
.then(group => dispatch(setGroup(group || null)));
};
}
export function createGroup(groupName, groupManager, members) {
return (dispatch, getState) => {
const { people } = getState();
const { groups, filter } = people;
let newGroup;
const { groups } = people;
return api
.createGroup(groupName, groupManager, members)
.then(res => {
checkResponseError(res);
newGroup = res.data.response;
//dispatch(setGroup(newGroup));
return dispatch(setGroups([...groups, newGroup]));
})
.then(() => {
return fetchPeopleByFilter(dispatch, filter);
})
.then(() => {
.then(newGroup => {
history.goBack();
dispatch(resetGroup());
dispatch(setGroups([...groups, newGroup]));
return Promise.resolve(newGroup);
});
};
@ -61,28 +44,17 @@ export function createGroup(groupName, groupManager, members) {
export function updateGroup(id, groupName, groupManager, members) {
return (dispatch, getState) => {
const { people } = getState();
const { groups, filter } = people;
let newGroup;
const { groups } = people;
return api
.updateGroup(id, groupName, groupManager, members)
.then(res => {
checkResponseError(res);
newGroup = res.data.response;
//dispatch(setGroup(newGroup));
.then(newGroup => {
history.goBack();
dispatch(resetGroup());
const newGroups = groups.map(g =>
g.id === newGroup.id ? newGroup : g
);
return dispatch(setGroups(newGroups));
})
.then(() => {
return fetchPeopleByFilter(dispatch, filter);
})
.then(() => {
dispatch(setGroups(newGroups));
return Promise.resolve(newGroup);
});
};
@ -96,12 +68,11 @@ export function deleteGroup(id) {
return api
.deleteGroup(id)
.then(res => {
checkResponseError(res);
return dispatch(setGroups(groups.filter(g => g.id !== id)));
})
.then(() => {
const newFilter = filter.clone(true);
return fetchPeopleByFilter(dispatch, newFilter);
return fetchPeople(newFilter, dispatch);
});
};
}

View File

@ -11,7 +11,8 @@ import {
SORT_BY,
SORT_ORDER,
PAGE,
PAGE_COUNT
PAGE_COUNT,
EmployeeStatus
} from "../../helpers/constants";
export const SET_GROUPS = "SET_GROUPS";
@ -68,7 +69,7 @@ export function selectGroup(groupId) {
let newFilter = filter.clone();
newFilter.group = groupId;
return fetchPeopleByFilter(dispatch, newFilter);
return fetchPeople(newFilter, dispatch);
};
}
@ -86,7 +87,7 @@ export function deselectUser(user) {
};
}
export function setFilter(filter) {
export function setFilterUrl(filter) {
const defaultFilter = Filter.getDefault();
const params = [];
@ -114,13 +115,17 @@ export function setFilter(filter) {
params.push(`${PAGE_COUNT}=${filter.pageCount}`);
}
params.push(`${PAGE}=${filter.page+1}`);
params.push(`${PAGE}=${filter.page + 1}`);
params.push(`${SORT_BY}=${filter.sortBy}`);
params.push(`${SORT_ORDER}=${filter.sortOrder}`);
if (params.length > 0) {
history.push(`${config.homepage}/filter?${params.join("&")}`);
}
}
export function setFilter(filter) {
setFilterUrl(filter);
return {
type: SET_FILTER,
filter
@ -136,55 +141,57 @@ export function setSelectorUsers(users) {
export function fetchSelectorUsers() {
return dispatch => {
api
.getSelectorUserList()
.then(res => dispatch(setSelectorUsers(res.data.response)));
api.getSelectorUserList().then(data => {
const users = data.items;
return dispatch(setSelectorUsers(users));
});
};
}
export function fetchPeople(filter) {
return dispatch => {
return fetchPeopleByFilter(dispatch, filter);
};
export function fetchGroups(dispatchFunc = null) {
return api.getGroupList().then(groups => {
return dispatchFunc
? dispatchFunc(setGroups(groups))
: Promise.resolve(dispatch => dispatch(setGroups(groups)));
});
}
export function fetchPeopleByFilter(dispatch, filter) {
let filterData = (filter && filter.clone()) || Filter.getDefault();
export function fetchPeople(filter, dispatchFunc = null) {
return dispatchFunc
? fetchPeopleByFilter(dispatchFunc, filter)
: (dispatch, getState) => {
if (filter) {
return fetchPeopleByFilter(dispatch, filter);
} else {
const { people } = getState();
const { filter } = people;
return fetchPeopleByFilter(dispatch, filter);
}
};
}
return api.getUserList(filterData).then(res => {
filterData.total = res.data.total;
function fetchPeopleByFilter(dispatch, filter) {
let filterData = filter && filter.clone();
if (!filterData) {
filterData = Filter.getDefault();
filterData.employeeStatus = EmployeeStatus.Active;
}
return api.getUserList(filterData).then(data => {
filterData.total = data.total;
dispatch(setFilter(filterData));
dispatch({
type: SELECT_GROUP,
groupId: filterData.group
});
return dispatch(setUsers(res.data.response));
return dispatch(setUsers(data.items));
});
}
export async function fetchPeopleAsync(dispatch, filter = null) {
let filterData = (filter && filter.clone()) || Filter.getDefault();
const usersResp = await api.getUserList(filterData);
filterData.total = usersResp.data.total;
dispatch(setFilter(filterData));
dispatch({
type: SELECT_GROUP,
groupId: filterData.group
});
dispatch(setUsers(usersResp.data.response));
}
export function updateUserStatus(status, userIds) {
return dispatch => {
return api.updateUserStatus(status, userIds).then(res => {
if (res && res.data && res.data.error && res.data.error.message)
throw res.data.error.message;
const users = res.data.response;
return api.updateUserStatus(status, userIds).then(users => {
users.forEach(user => {
dispatch(setUser(user));
});
@ -194,12 +201,7 @@ export function updateUserStatus(status, userIds) {
export function updateUserType(type, userIds) {
return dispatch => {
return api.updateUserType(type, userIds).then(res => {
if (res && res.data && res.data.error && res.data.error.message)
throw res.data.error.message;
const users = res.data.response;
return api.updateUserType(type, userIds).then(users => {
users.forEach(user => {
dispatch(setUser(user));
});
@ -214,6 +216,6 @@ export function resetFilter() {
const newFilter = filter.clone(true);
return fetchPeopleByFilter(dispatch, newFilter);
return fetchPeople(newFilter, dispatch);
};
}

View File

@ -1,12 +1,11 @@
import { toUrlParams } from "../services/converter";
import { EmployeeStatus } from "../../helpers/constants";
const DEFAULT_PAGE = 0;
const DEFAULT_PAGE_COUNT = 25;
const DEFAULT_TOTAL = 0;
const DEFAULT_SORT_BY = "firstname";
const DEFAULT_SORT_ORDER = "ascending";
const DEFAULT_EMPLOYEE_STATUS = EmployeeStatus.Active;
const DEFAULT_EMPLOYEE_STATUS = null;
const DEFAULT_ACTIVATION_STATUS = null;
const DEFAULT_ROLE = null;
const DEFAULT_SEARCH = null;

View File

@ -1,7 +1,8 @@
import * as api from "../../store/services/api";
import { isMe } from '../auth/selectors';
import { getUserByUserName } from '../people/selectors';
import { fetchPeopleByFilter } from "../people/actions";
import { fetchPeople } from "../people/actions";
import { setCurrentUser } from "../auth/actions";
export const SET_PROFILE = 'SET_PROFILE';
export const CLEAN_PROFILE = 'CLEAN_PROFILE';
@ -19,20 +20,13 @@ export function resetProfile() {
};
};
export function checkResponseError(res) {
if (res && res.data && res.data.error) {
console.error(res.data.error);
throw new Error(res.data.error.message);
}
}
export function employeeWrapperToMemberModel(profile) {
const comment = profile.notes;
const department = profile.groups ? profile.groups.map(group => group.id) : [];
const worksFrom = profile.workFrom;
return { ...profile, comment, department, worksFrom };
}
};
export function fetchProfile(userName) {
return (dispatch, getState) => {
@ -43,16 +37,15 @@ export function fetchProfile(userName) {
} else {
const user = getUserByUserName(people.users, userName);
if (!user) {
api.getUser(userName).then(res => {
checkResponseError(res);
dispatch(setProfile(res.data.response));
api.getUser(userName).then(user => {
dispatch(setProfile(user));
});
} else {
dispatch(setProfile(user));
}
}
};
}
};
export function createProfile(profile) {
return (dispatch, getState) => {
@ -61,12 +54,11 @@ export function createProfile(profile) {
const member = employeeWrapperToMemberModel(profile);
let result;
return api.createUser(member).then(res => {
checkResponseError(res);
result = res.data.response;
return dispatch(setProfile(result));
return api.createUser(member).then(user => {
result = user;
return dispatch(setProfile(user));
}).then(() => {
return fetchPeopleByFilter(dispatch, filter);
return fetchPeople(filter, dispatch);
}).then(() => {
return Promise.resolve(result);
});
@ -80,66 +72,28 @@ export function updateProfile(profile) {
const member = employeeWrapperToMemberModel(profile);
let result;
return api.updateUser(member).then(res => {
checkResponseError(res);
result = res.data.response;
return Promise.resolve(dispatch(setProfile(result)));
return api.updateUser(member).then(user => {
result = user;
return Promise.resolve(dispatch(setProfile(user)));
}).then(() => {
return fetchPeopleByFilter(dispatch, filter);
return fetchPeople(filter, dispatch);
}).then(() => {
return Promise.resolve(result);
});
};
};
export function loadAvatar(profileId, data) {
return (dispatch, getState) => {
return api.loadAvatar(
profileId,
data
).then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
};
};
export function createThumbnailsAvatar(profileId, data) {
return (dispatch, getState) => {
return api.createThumbnailsAvatar(
profileId,
data
).then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
};
};
export function deleteAvatar(profileId) {
return (dispatch, getState) => {
return api.deleteAvatar(profileId)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
export function updateProfileCulture(id, culture) {
return (dispatch) => {
return api.updateUserCulture(id, culture).then(user => {
dispatch(setCurrentUser(user));
return dispatch(setProfile(user));
});
};
};
export function getInvitationLink(isGuest = false) {
return dispatch => {
return api.getInvitationLink(isGuest)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
return api.getInvitationLink(isGuest);
}
}
export function getShortenedLink(link) {
return dispatch => {
return api.getShortenedLink(link)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
}
}
};

View File

@ -1,55 +1,56 @@
import { request } from "./client";
import axios from "axios";
import * as fakeApi from "./fakeApi";
import Filter from "../people/filter";
const PREFIX = "api";
const VERSION = "2.0";
const API_URL = `${window.location.origin}/${PREFIX}/${VERSION}`;
const IS_FAKE = false;
const linkTtl = 6 * 3600 * 1000;
export function login(data) {
return axios.post(`${API_URL}/authentication`, data);
}
export function getModulesList() {
return IS_FAKE
? fakeApi.getModulesList()
: axios
.get(`${API_URL}/modules`)
.then(res => {
const modules = res.data.response;
return axios.all(
modules.map(m => axios.get(`${window.location.origin}/${m}`))
);
})
.then(res => {
const response = res.map(d => d.data.response);
return Promise.resolve({ data: { response } });
});
return request({
method: "get",
url: "/modules"
}).then(modules => {
return axios.all(
modules.map(m =>
request({
method: "get",
url: `${window.location.origin}/${m}`
})
)
);
});
}
export function getSettings() {
return IS_FAKE
? fakeApi.getSettings()
: axios.get(`${API_URL}/settings.json`);
return request({
method: "get",
url: "/settings.json"
});
}
export function getPortalCultures() {
return request({
method: "get",
url: "/settings/cultures.json"
});
}
export function getPortalPasswordSettings() {
return IS_FAKE
? fakeApi.getPortalPasswordSettings()
: axios.get(`${API_URL}/settings/security/password`);
return request({
method: "get",
url: "/settings/security/password"
});
}
export function getUser(userId) {
return IS_FAKE
? fakeApi.getUser()
: axios.get(`${API_URL}/people/${userId || "@self"}.json`);
return request({
method: "get",
url: "/people/@self.json"
});
}
export function getSelectorUserList() {
return axios.get(`${API_URL}/people/filter.json?fields=id,displayName,groups`);
return request({
method: "get",
url: "/people/filter.json?fields=id,displayName,groups"
});
}
export function getUserList(filter = Filter.getDefault()) {
@ -57,191 +58,238 @@ export function getUserList(filter = Filter.getDefault()) {
filter && filter instanceof Filter
? `/filter.json?${filter.toUrlParams()}`
: "";
return IS_FAKE ? fakeApi.getUsers() : axios.get(`${API_URL}/people${params}`);
return request({
method: "get",
url: `/people${params}`
});
}
export function getGroupList() {
return IS_FAKE ? fakeApi.getGroups() : axios.get(`${API_URL}/group`);
return request({
method: "get",
url: "/group"
});
}
export function createUser(data) {
return IS_FAKE ? fakeApi.createUser() : axios.post(`${API_URL}/people`, data);
return request({
method: "post",
url: "/people",
data
});
}
export function updateUser(data) {
return IS_FAKE
? fakeApi.updateUser()
: axios.put(`${API_URL}/people/${data.id}`, data);
return request({
method: "put",
url: `/people/${data.id}`,
data
});
}
export function updateUserCulture(id, cultureName) {
return request({
method: "put",
url: `/people/${id}/culture`,
data: { cultureName }
});
}
export function loadAvatar(profileId, data) {
return IS_FAKE
? fakeApi.loadAvatar()
: axios.post(`${API_URL}/people/${profileId}/photo`, data);
return request({
method: "post",
url: `/people/${profileId}/photo`,
data
});
}
export function createThumbnailsAvatar(profileId, data) {
return IS_FAKE
? fakeApi.createThumbnailsAvatar()
: axios.post(`${API_URL}/people/${profileId}/photo/thumbnails.json`, data);
return request({
method: "post",
url: `/people/${profileId}/photo/thumbnails.json`,
data
});
}
export function deleteAvatar(profileId) {
return IS_FAKE
? fakeApi.deleteAvatar()
: axios.delete(`${API_URL}/people/${profileId}/photo`, profileId);
return request({
method: "delete",
url: `/people/${profileId}/photo`
});
}
export function getInitInfo() {
return axios.all([getUser(), getModulesList(), getSettings(), getPortalPasswordSettings()]).then(
axios.spread(function (userResp, modulesResp, settingsResp, passwordSettingsResp) {
let info = {
user: userResp.data.response,
modules: modulesResp.data.response,
settings: settingsResp.data.response
};
return axios
.all([
getUser(),
getModulesList(),
getSettings(),
getPortalPasswordSettings(),
getPortalCultures()
])
.then(
axios.spread((user, modules, settings, passwordSettings, cultures) => {
const info = {
user,
modules,
settings
};
info.settings.passwordSettings = passwordSettingsResp.data.response;
info.settings.passwordSettings = passwordSettings;
info.settings.cultures = cultures || [];
return Promise.resolve(info);
})
);
return Promise.resolve(info);
})
);
}
export function getInvitationLinks() {
const isGuest = true;
return axios.all([getInvitationLink(), getInvitationLink(isGuest)]).then(
axios.spread(function (userInvitationLinkResp, guestInvitationLinkResp) {
let links = {
inviteLinks: {}
axios.spread((userInvitationLinkResp, guestInvitationLinkResp) => {
const links = {
inviteLinks: {
userLink: userInvitationLinkResp,
guestLink: guestInvitationLinkResp
}
};
links.inviteLinks = {
userLink: userInvitationLinkResp,
guestLink: guestInvitationLinkResp
}
return Promise.resolve(links);
})
);
}
export function updateUserStatus(status, userIds) {
return IS_FAKE
? fakeApi.updateUserStatus(status, userIds)
: axios.put(`${API_URL}/people/status/${status}`, { userIds });
return request({
method: "put",
url: `/people/status/${status}`,
data: { userIds }
});
}
export function updateUserType(type, userIds) {
return IS_FAKE
? fakeApi.updateUserType(type, userIds)
: axios.put(`${API_URL}/people/type/${type}`, { userIds });
return request({
method: "put",
url: `/people/type/${type}`,
data: { userIds }
});
}
export function resendUserInvites(userIds) {
return IS_FAKE
? fakeApi.resendUserInvites(userIds)
: axios.put(`${API_URL}/people/invite`, { userIds });
return request({
method: "put",
url: "/people/invite",
data: { userIds }
});
}
export function sendInstructionsToDelete() {
return IS_FAKE
? fakeApi.sendInstructionsToDelete()
: axios.put(`${API_URL}/people/self/delete.json`);
return request({
method: "put",
url: "/people/self/delete.json"
});
}
export function sendInstructionsToChangePassword(email) {
return IS_FAKE
? fakeApi.sendInstructionsToChangePassword()
: axios.post(`${API_URL}/people/password.json`, { email });
return request({
method: "post",
url: "/people/password.json",
data: { email }
});
}
export function sendInstructionsToChangeEmail(userId, email) {
return IS_FAKE
? fakeApi.sendInstructionsToChangeEmail()
: axios.post(`${API_URL}/people/email.json`, { userId, email });
return request({
method: "post",
url: "/people/email.json",
data: { userId, email }
});
}
export function deleteUser(userId) {
return IS_FAKE
? fakeApi.deleteUser(userId)
: axios.delete(`${API_URL}/people/${userId}.json`);
return request({
method: "delete",
url: `/people/${userId}.json`
});
}
export function deleteUsers(userIds) {
return IS_FAKE
? fakeApi.deleteUsers(userIds)
: axios
.put(`${API_URL}/people/delete.json`, { userIds })
.then(CheckError);
return request({
method: "put",
url: "/people/delete.json",
data: { userIds }
});
}
export function getGroup(groupId) {
return IS_FAKE
? fakeApi.getGroup(groupId)
: axios.get(`${API_URL}/group/${groupId}.json`);
return request({
method: "get",
url: `/group/${groupId}.json`
});
}
const GUEST_INVITE_LINK = "guestInvitationLink";
const USER_INVITE_LINK = "userInvitationLink";
const INVITE_LINK_TTL = "localStorageLinkTtl";
const LINKS_TTL = 6 * 3600 * 1000;
export function getInvitationLink(isGuest) {
let localStorageLinkTtl = localStorage.getItem('localStorageLinkTtl');
const curLinksTtl = localStorage.getItem(INVITE_LINK_TTL);
const now = +new Date();
if (localStorageLinkTtl === null) {
localStorage.setItem('localStorageLinkTtl', +new Date());
}
else if (+new Date() - localStorageLinkTtl > linkTtl) {
localStorage.clear();
localStorage.setItem('localStorageLinkTtl', +new Date());
if (!curLinksTtl) {
localStorage.setItem(INVITE_LINK_TTL, now);
} else if (now - curLinksTtl > LINKS_TTL) {
localStorage.removeItem(GUEST_INVITE_LINK);
localStorage.removeItem(USER_INVITE_LINK);
localStorage.setItem(INVITE_LINK_TTL, now);
}
if (IS_FAKE) {
return fakeApi.getInvitationLink(isGuest);
}
else
if (isGuest) {
const guestInvitationLink = localStorage.getItem('guestInvitationLink');
return guestInvitationLink
? guestInvitationLink
: axios.get(`${API_URL}/portal/users/invite/2.json`)
.then(res => {
localStorage.setItem('guestInvitationLink', res.data.response);
return Promise.resolve(res.data.response);
})
}
else {
const userInvitationLink = localStorage.getItem('userInvitationLink');
return userInvitationLink
? userInvitationLink
: axios.get(`${API_URL}/portal/users/invite/1.json`)
.then(res => {
localStorage.setItem('userInvitationLink', res.data.response);
return Promise.resolve(res.data.response);
})
}
const link = localStorage.getItem(
isGuest ? GUEST_INVITE_LINK : USER_INVITE_LINK
);
return link
? Promise.resolve(link)
: request({
method: "get",
url: `/portal/users/invite/${isGuest ? 2 : 1}.json`
}).then(link => {
localStorage.setItem(
isGuest ? GUEST_INVITE_LINK : USER_INVITE_LINK,
link
);
return Promise.resolve(link);
});
}
export function getShortenedLink(link) {
return IS_FAKE
? fakeApi.getShortenedLink(link)
: axios.put(`${API_URL}/portal/getshortenlink.json`, link);
}
function CheckError(res) {
if (res.data && res.data.error) {
const error = res.data.error.message || "Unknown error has happened";
console.trace(error);
throw error;
}
return Promise.resolve(res);
return request({
method: "put",
url: "/portal/getshortenlink.json",
data: link
});
}
export function createGroup(groupName, groupManager, members) {
const group = { groupName, groupManager, members };
return axios.post(`${API_URL}/group.json`, group);
const data = { groupName, groupManager, members };
return request({
method: "post",
url: "/group.json",
data
});
}
export function updateGroup(id, groupName, groupManager, members) {
const group = { groupId: id, groupName, groupManager, members };
return axios.put(`${API_URL}/group/${id}.json`, group);
const data = { groupId: id, groupName, groupManager, members };
return request({
method: "put",
url: `/group/${id}.json`,
data
});
}
export function deleteGroup(id) {
return axios.delete(`${API_URL}/group/${id}.json`);
return request({
method: "delete",
url: `/group/${id}.json`
});
}

View File

@ -0,0 +1,99 @@
import axios from "axios";
import Cookies from "universal-cookie";
import history from "../../history";
import { AUTH_KEY } from "../../helpers/constants.js";
const PREFIX = "api";
const VERSION = "2.0";
const baseURL = `${window.location.origin}/${PREFIX}/${VERSION}`;
/**
* @description axios instance for ajax requests
*/
const client = axios.create({
baseURL: baseURL,
responseType: "json",
timeout: 30000 // default is `0` (no timeout)
});
setAuthorizationToken(localStorage.getItem(AUTH_KEY));
client.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response.status === 401) {
//place your reentry code
history.push("/login/error=unauthorized");
}
if (error.response.status === 502) {
//toastr.error(error.response);
history.push(`/error/${error.response.status}`);
}
return error;
}
);
export function setAuthorizationToken(token) {
const cookies = new Cookies();
if (token) {
client.defaults.headers.common["Authorization"] = token;
localStorage.setItem(AUTH_KEY, token);
const current = new Date();
const nextYear = new Date();
nextYear.setFullYear(current.getFullYear() + 1);
cookies.set(AUTH_KEY, token, {
path: "/",
expires: nextYear
});
} else {
localStorage.clear();
delete client.defaults.headers.common["Authorization"];
cookies.remove(AUTH_KEY, {
path: "/"
});
}
}
const checkResponseError = res => {
if (res && res.data && res.data.error) {
console.error(res.data.error);
throw new Error(res.data.error.message);
}
};
/**
* @description wrapper for making ajax requests
* @param {object} object with method,url,data etc.
*/
export const request = function(options) {
const onSuccess = function(response) {
checkResponseError(response);
return response.data && response.data.hasOwnProperty("total")
? { total: +response.data.total, items: response.data.response }
: response.data.response;
};
const onError = function(error) {
console.error("Request Failed:", error.config);
if (error.response) {
console.error("Status:", error.response.status);
console.error("Data:", error.response.data);
console.error("Headers:", error.response.headers);
} else {
console.error("Error Message:", error.message);
}
return Promise.reject(error.response || error.message);
};
return client(options)
.then(onSuccess)
.catch(onError);
};

View File

@ -1,14 +1,33 @@
import axios from 'axios';
import Cookies from 'universal-cookie';
//import Cookies from 'universal-cookie';
import { AUTH_KEY } from '../../helpers/constants';
import history from '../../history';
import { toastr } from 'asc-web-components';
export default function setAuthorizationToken(token) {
const cookies = new Cookies();
axios.interceptors.response.use(response => {
return response;
}, error => {
if (error.response.status === 401) {
//place your reentry code
history.push("/login/error=unauthorized");
}
if (error.response.status === 502) {
//toastr.error(error.response);
history.push(`/error/${error.response.status}`);
}
return error;
});
//const cookies = new Cookies();
if (token) {
axios.defaults.headers.common["Authorization"] = token;
localStorage.setItem(AUTH_KEY, token);
const current = new Date();
/*const current = new Date();
const nextYear = new Date();
nextYear.setFullYear(current.getFullYear() + 1);
@ -16,13 +35,13 @@ export default function setAuthorizationToken(token) {
cookies.set(AUTH_KEY, token, {
path: '/',
expires: nextYear,
});
});*/
}
else {
localStorage.clear();
delete axios.defaults.headers.common["Authorization"];
cookies.remove(AUTH_KEY, {
/*cookies.remove(AUTH_KEY, {
path: '/'
});
});*/
}
}

View File

@ -1802,14 +1802,15 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../../packages/asc-web-components":
version "1.0.87"
version "1.0.102"
dependencies:
moment "^2.24.0"
prop-types "^15.7.2"
rc-tree "^2.1.2"
react-autosize-textarea "^7.0.0"
react-avatar-edit "^0.8.3"
react-avatar-editor "^11.0.7"
react-custom-scrollbars "^4.2.1"
react-dropzone "^10.1.8"
react-text-mask "^5.4.3"
react-toastify "^5.3.2"
react-virtualized-auto-sizer "^1.0.2"
@ -1897,6 +1898,13 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.3.tgz#48230c79f93790ef2775fcec4f0db0f5db41ca52"
integrity sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==
dependencies:
core-js "^2.5.0"
autoprefixer@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47"
@ -2968,7 +2976,7 @@ core-js@3.1.4:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07"
integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==
core-js@^2.4.0, core-js@^2.6.4:
core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.4:
version "2.6.9"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
@ -4320,6 +4328,13 @@ file-loader@3.0.1:
loader-utils "^1.0.2"
schema-utils "^1.0.0"
file-selector@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
dependencies:
tslib "^1.9.0"
filesize@3.6.1:
version "3.6.1"
resolved "https://registry.yarnpkg.com/filesize/-/filesize-3.6.1.tgz#090bb3ee01b6f801a8a8be99d31710b3422bb317"
@ -6298,11 +6313,6 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
konva@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/konva/-/konva-2.5.1.tgz#cca611a9522e831e54cf57c508a1aed3f0ceac25"
integrity sha512-YdHEWqmbWPieqIZuLx7JFGm9Ui08hSUaSJ2k2Ml8o5giFgJ0WmxAS0DPXIM+Ty2ADRagOHZfXSJ/skwYqqlwgQ==
last-call-webpack-plugin@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz#9742df0e10e3cf46e5c0381c2de90d3a7a2d7555"
@ -8736,12 +8746,12 @@ react-autosize-textarea@^7.0.0:
line-height "^0.3.1"
prop-types "^15.5.6"
react-avatar-edit@^0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-avatar-edit/-/react-avatar-edit-0.8.3.tgz#0ebf21391328fc255429bdfbc782f795827109bf"
integrity sha512-QEedh6DjDCSI7AUsUHHtfhxApCWC5hJAoywxUA5PtUdw03iIjEurgVqPOIt1UBHhU/Zk/9amElRF3oepN9JZSg==
react-avatar-editor@^11.0.7:
version "11.0.7"
resolved "https://registry.yarnpkg.com/react-avatar-editor/-/react-avatar-editor-11.0.7.tgz#021053cfeaa138407b79279ee5a0384f273f0c54"
integrity sha512-GbNYBd1/L1QyuU9VRvOW0hSkW1R0XSneOWZFgqI5phQf6dX+dF/G3/AjiJ0hv3JWh2irMQ7DL0oYDKzwtTnNBQ==
dependencies:
konva "2.5.1"
prop-types "^15.5.8"
react-custom-scrollbars@^4.2.1:
version "4.2.1"
@ -8798,6 +8808,15 @@ react-dom@^16.9.0:
prop-types "^15.6.2"
scheduler "^0.15.0"
react-dropzone@^10.1.8:
version "10.1.9"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-10.1.9.tgz#8093ecd7d2dc4002280eb2dac1d5fa4216c800ee"
integrity sha512-7iqALZ0mzk+4g/AsYxEy3QyWPMTVQYKQVkYUe9zIbH18u+pi7EBDg010KEwfIX6jeTDH2qP0E6/eUnXvBYrovA==
dependencies:
attr-accept "^1.1.3"
file-selector "^0.1.11"
prop-types "^15.7.2"
react-error-overlay@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.1.tgz#b8d3cf9bb991c02883225c48044cb3ee20413e0f"

View File

@ -486,6 +486,42 @@ namespace ASC.Employee.Core.Controllers
return new EmployeeWraperFull(user, ApiContext, UserManager, UserPhotoManager, WebItemSecurity, TenantManager, CommonLinkUtility, DisplayUserSettings);
}
[Update("{userid}/culture")]
public EmployeeWraperFull UpdateMemberCulture(string userid, UpdateMemberModel memberModel)
{
var user = GetUserInfo(userid);
if (UserManager.IsSystemUser(user.ID))
throw new SecurityException();
PermissionContext.DemandPermissions(new UserSecurityProvider(user.ID), Constants.Action_EditUser);
var curLng = user.CultureName;
if (SetupInfo.EnabledCultures.Find(c => string.Equals(c.Name, memberModel.CultureName, StringComparison.InvariantCultureIgnoreCase)) != null)
{
if (curLng != memberModel.CultureName)
{
user.CultureName = memberModel.CultureName;
try
{
UserManager.SaveUserInfo(user);
}
catch (Exception ex)
{
user.CultureName = curLng;
throw ex;
}
MessageService.Send(MessageAction.UserUpdatedLanguage, MessageTarget.Create(user.ID), user.DisplayUserName(false, DisplayUserSettings));
}
}
return new EmployeeWraperFull(user, ApiContext, UserManager, UserPhotoManager, WebItemSecurity, TenantManager, CommonLinkUtility, DisplayUserSettings); ;
}
[Update("{userid}")]
public EmployeeWraperFull UpdateMember(string userid, UpdateMemberModel memberModel)
{
@ -621,6 +657,50 @@ namespace ASC.Employee.Core.Controllers
return new EmployeeWraperFull(user, ApiContext, UserManager, UserPhotoManager, WebItemSecurity, TenantManager, CommonLinkUtility, DisplayUserSettings);
}
[Delete("@self")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "ProfileRemove")]
public EmployeeWraperFull DeleteProfile()
{
ApiContext.AuthByClaim();
if (UserManager.IsSystemUser(SecurityContext.CurrentAccount.ID))
throw new SecurityException();
var user = GetUserInfo(SecurityContext.CurrentAccount.ID.ToString());
if (!UserManager.UserExists(user))
throw new Exception(Resource.ErrorUserNotFound);
if (user.IsLDAP())
throw new SecurityException();
_ = SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem);
user.Status = EmployeeStatus.Terminated;
UserManager.SaveUserInfo(user);
var userName = user.DisplayUserName(false, DisplayUserSettings);
MessageService.Send(MessageAction.UsersUpdatedStatus, MessageTarget.Create(user.ID), userName);
CookiesManager.ResetUserCookie(user.ID);
MessageService.Send(MessageAction.CookieSettingsUpdated);
if (CoreBaseSettings.Personal)
{
UserPhotoManager.RemovePhoto(user.ID);
UserManager.DeleteUser(user.ID);
MessageService.Send(MessageAction.UserDeleted, MessageTarget.Create(user.ID), userName);
}
else
{
//StudioNotifyService.Instance.SendMsgProfileHasDeletedItself(user);
//StudioNotifyService.SendMsgProfileDeletion(Tenant.TenantId, user);
}
return new EmployeeWraperFull(user, ApiContext, UserManager, UserPhotoManager, WebItemSecurity, TenantManager, CommonLinkUtility, DisplayUserSettings);
}
[Update("{userid}/contacts")]
public EmployeeWraperFull UpdateMemberContacts(string userid, UpdateMemberModel memberModel)
{

View File

@ -28,5 +28,6 @@ namespace ASC.People.Models
{
public string UserId { get; set; }
public bool? Disable { get; set; }
public string CultureName { get; set; }
}
}

View File

@ -84,7 +84,7 @@ namespace ASC.Web.Api.Controllers
public string GeInviteLink(EmployeeType employeeType)
{
PermissionContext.DemandPermissions(Constants.Action_AddRemoveUser);
return CommonLinkUtility.GetConfirmationUrl(string.Empty, ConfirmType.LinkInvite, (int)employeeType, AuthContext.CurrentAccount.ID)
return CommonLinkUtility.GetConfirmationUrl(string.Empty, ConfirmType.LinkInvite, (int)employeeType)
+ $"&emplType={employeeType:d}";
}

View File

@ -256,7 +256,7 @@ namespace ASC.Api.Settings
settings.TrustedDomains = Tenant.TrustedDomains;
settings.TrustedDomainsType = Tenant.TrustedDomainsType;
var timeZone = Tenant.TimeZone;
settings.Timezone = timeZone.ToSerializedString();
settings.Timezone = timeZone.Id;
settings.UtcOffset = timeZone.GetUtcOffset(DateTime.UtcNow);
settings.UtcHoursOffset = settings.UtcOffset.TotalHours;
}
@ -277,6 +277,19 @@ namespace ASC.Api.Settings
return SetupInfo.EnabledCultures;
}
[Read("timezones")]
public IEnumerable<TimeZoneInfo> GetTimeZones()
{
var timeZones = TimeZoneInfo.GetSystemTimeZones().ToList();
if (timeZones.All(tz => tz.Id != "UTC"))
{
timeZones.Add(TimeZoneInfo.Utc);
}
return timeZones;
}
//[Read("recalculatequota")]
//public void RecalculateQuota()
//{
@ -380,7 +393,7 @@ namespace ASC.Api.Settings
}
[Read("security/password")]
[Authorize(AuthenticationSchemes = "confirm")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "Everyone")]
public object GetPasswordSettings()
{
var UserPasswordSettings = PasswordSettings.Load();

View File

@ -1,19 +1,21 @@
import React, { Suspense, lazy } from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { Router, Route, Switch } from "react-router-dom";
import { Loader } from "asc-web-components";
import StudioLayout from "./components/Layout/index";
import Login from "./components/pages/Login";
import { PrivateRoute } from "./helpers/privateRoute";
import PrivateRoute from "./helpers/privateRoute";
import PublicRoute from "./helpers/publicRoute";
import { Error404 } from "./components/pages/Error";
import history from './history';
const Home = lazy(() => import("./components/pages/Home"));
const About = lazy(() => import("./components/pages/About"));
const Confirm = lazy(() => import("./components/pages/Confirm"));
const Settings = lazy(() => import("./components/pages/Settings"));
const App = () => {
return (
<BrowserRouter>
<Router history={history}>
<StudioLayout>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
@ -21,13 +23,14 @@ const App = () => {
<Switch>
<PublicRoute exact path={["/login","/login/error=:error", "/login/confirmed-email=:confirmedEmail"]} component={Login} />
<Route path="/confirm" component={Confirm} />
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path={["/","/error=:error"]} component={Home} />
<PrivateRoute exact path="/about" component={About} />
<PrivateRoute restricted path="/settings" component={Settings} />
<PrivateRoute component={Error404} />
</Switch>
</Suspense>
</StudioLayout>
</BrowserRouter>
</Router>
);
};

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -6,15 +6,13 @@ import { Layout, Toast } from "asc-web-components";
import { logout } from "../../store/auth/actions";
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from "./i18n";
import isEqual from "lodash/isEqual";
import { isAdmin } from "../../store/auth/selectors";
class PureStudioLayout extends React.Component {
shouldComponentUpdate(nextProps) {
if(this.props.hasChanges !== nextProps.hasChanges) {
return true;
}
return false;
}
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
onProfileClick = () => {
window.location.href = "/products/people/view/@self";
@ -65,8 +63,23 @@ class PureStudioLayout extends React.Component {
};
const getAvailableModules = modules => {
const getAvailableModules = (modules, currentUser) => {
const isUserAdmin = isAdmin(currentUser);
const separator = { separator: true, id: "nav-separator-1" };
const customModules = isUserAdmin ? [
{
separator: true,
id: "nav-separator-2"
},
{
id: 'settings',
title: 'Settings',
iconName: "SettingsIcon",
notifications: 0,
url: '/settings',
onClick: () => window.open('/settings', "_self"),
onBadgeClick: e => console.log("SettingsIconBadge Clicked", e)
}] : [];
const products =
modules.map(product => {
return {
@ -80,21 +93,27 @@ const getAvailableModules = modules => {
};
}) || [];
return products.length ? [separator, ...products] : products;
return products.length ? [separator, ...products, ...customModules] : products;
};
function mapStateToProps(state) {
let availableModules = getAvailableModules(state.auth.modules);
let availableModules = getAvailableModules(state.auth.modules, state.auth.user);
return {
hasChanges: state.auth.isAuthenticated && state.auth.isLoaded,
availableModules: availableModules,
currentUser: state.auth.user,
currentModuleId: state.auth.settings.currentModuleId
currentModuleId: state.auth.settings.currentProductId,
language: state.auth.user.cultureName || state.auth.settings.culture,
};
};
const StudioLayoutContainer = withTranslation()(PureStudioLayout);
const StudioLayout = (props) => <I18nextProvider i18n={i18n}><StudioLayoutContainer {...props} /></I18nextProvider>;
const StudioLayout = (props) => {
const { language } = props;
i18n.changeLanguage(language);
return (<I18nextProvider i18n={i18n}><StudioLayoutContainer {...props} /></I18nextProvider>);
};
StudioLayout.propTypes = {
logout: PropTypes.func.isRequired
};

View File

@ -0,0 +1,5 @@
{
"Profile": "Профиль",
"AboutCompanyTitle": "О программе",
"LogoutButton": "Выйти"
}

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -49,7 +52,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -1,4 +1,5 @@
import React from "react";
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { PageLayout, Text, Link } from "asc-web-components";
import { useTranslation } from "react-i18next";
import i18n from "./i18n";
@ -29,7 +30,7 @@ const BodyStyle = styled.div`
content: "";
height: 2px;
margin-top: 9px;
width: 36%;
width: 26%;
float: right;
}
@ -38,7 +39,7 @@ const BodyStyle = styled.div`
content: "";
height: 2px;
margin-top: 9px;
width: 36%;
width: 26%;
float: left;
}
}
@ -53,9 +54,13 @@ const VersionStyle = styled.div`
padding: 8px 0px 20px 0px;
`;
const Body = () => {
const Body = ({language}) => {
const { t } = useTranslation("translation", { i18n });
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return (
<BodyStyle>
<p style={{ textAlign: "center", margin: "0px" }}>
@ -127,7 +132,7 @@ const Body = () => {
<div style={{ marginTop: "20px" }}>
<Text.Body className="text_p" fontSize={12}>
{t("LicensedUnder")}:{" "}
{t("LicensedUnder", {license: "GNU GPL v.3"} )}:{" "}
<Link
href="https://www.gnu.org/licenses/gpl-3.0.html"
isHovered={true}
@ -153,8 +158,12 @@ const Body = () => {
);
};
const About = () => {
return <PageLayout sectionBodyContent={<Body />} />;
};
const About = ({language}) => <PageLayout sectionBodyContent={<Body language={language} />} />;
export default About;
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export default connect(mapStateToProps)(About);

View File

@ -5,11 +5,11 @@
"AboutCompanyAddressTitle": "address",
"AboutCompanyEmailTitle": "email",
"AboutCompanyTelTitle": "tel.",
"LicensedUnder": "This software is licensed under", "_comment": "{0}GNU GPL v.3{1}",
"LicensedUnder": "This software is licensed under {{license}}",
"SourceCode": "Source code is available on", "_comment": "{0}GNU GPL v.3{1}","_comment":"SYNTAX ERROR"
"SourceCode": "Source code is available on"
}

View File

@ -0,0 +1,13 @@
{
"AboutCompanyTitle": "О программе",
"AboutCompanyVersion": "Версия",
"AboutCompanyLicensor": "АВТОРСКИЕ ПРАВА",
"AboutCompanyAddressTitle": "адрес",
"AboutCompanyEmailTitle": "email",
"AboutCompanyTelTitle": "тел.",
"LicensedUnder": "Это программное обеспечение лицензируется под {{license}}",
"SourceCode": "Исходный код программы доступен по cсылке"
}

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};
@ -49,7 +52,7 @@ if (process.env.NODE_ENV === "production") {
},
react: {
useSuspense: true
useSuspense: false
}
});
}

View File

@ -1,18 +1,23 @@
import React, { Suspense, lazy } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import { Loader } from "asc-web-components";
import PublicRoute from "../../../helpers/publicRoute";
import ConfirmRoute from "../../../helpers/confirmRoute";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
// import ChangeEmailForm from "./sub-components/changeEmail";
const ActivateUserForm = lazy(() => import("./sub-components/activateUser"));
const CreateUserForm = lazy(() => import("./sub-components/createUser"));
const ChangePasswordForm = lazy(() => import("./sub-components/changePassword"));
const ActivateEmailForm = lazy(() => import("./sub-components/activateEmail"));
const ChangeEmailForm = lazy(() => import("./sub-components/changeEmail"));
const ChangePhoneForm = lazy(() => import("./sub-components/changePhone"));
const ProfileRemoveForm = lazy(() => import("./sub-components/profileRemove"));
const Error404 = lazy(() => import("../Error"));
const Confirm = ({ match }) => {
const Confirm = ({ match, language }) => {
i18n.changeLanguage(language);
//console.log("Confirm render");
return (
@ -21,35 +26,52 @@ const Confirm = ({ match }) => {
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<Switch>
<PublicRoute
path={[`${match.path}/LinkInvite`, `${match.path}/Activation`]}
<ConfirmRoute
forUnauthorized
path={`${match.path}/LinkInvite`}
component={CreateUserForm}
/>
<Route
<ConfirmRoute
forUnauthorized
path={`${match.path}/Activation`}
component={ActivateUserForm}
/>
<ConfirmRoute
exact
path={`${match.path}/EmailActivation`}
component={ActivateEmailForm}
/>
<Route
<ConfirmRoute
exact
path={`${match.path}/EmailChange`}
component={ChangeEmailForm}
/>
<Route
<ConfirmRoute
exact
path={`${match.path}/PasswordChange`}
component={ChangePasswordForm}
/>
<ConfirmRoute
exact
path={`${match.path}/ProfileRemove`}
component={ProfileRemoveForm}
/>
<Route
exact
path={`${match.path}/PhoneActivation`}
component={ChangePhoneForm}
/>
<Redirect to={{ pathname: "/" }} />
<Route component={Error404} />
</Switch>
</Suspense>
</I18nextProvider >
);
};
export default Confirm;
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export default connect(mapStateToProps)(Confirm);

View File

@ -13,10 +13,16 @@
"ErrorPasswordNoUpperCase": "capital letters",
"ErrorPasswordNoSpecialSymbols": "special characters",
"EmailAndPasswordCopiedToClipboard": "Email and password copied to clipboard",
"PassworResetTitle": "Now you can create a new password.", "_comment":"SYNTAX ERROR 'Passwor' Reset Title",
"PassworResetTitle": "Now you can create a new password.",
"PasswordCustomMode": "Password",
"ImportContactsOkButton": "OK",
"LoadingProcessing": "Loading...",
"ChangePasswordSuccess": "Password has been successfully changed",
"DeleteProfileBtn": "Delete my account",
"DeleteProfileConfirmation": "Attention! You are about to delete your account.",
"DeleteProfileConfirmationInfo": "By clicking the \"Delete my account\" button you agree with our Privacy policy.",
"DeleteProfileSuccessMessage": "Your account has been successfully deleted.",
"DeleteProfileSuccessMessageInfo": "See our Privacy policy to learn more about deleting your account and data accociated with it.",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"

View File

@ -0,0 +1,24 @@
{
"InviteTitle": "Вы приглашены присоединиться к этому порталу!",
"LoginRegistryButton": "Присоединиться",
"LoginWithAccount": "или войдите с:",
"Email": "Email",
"InvitePassword": "Пароль",
"FirstName": "Имя",
"LastName": "Фамилия",
"CopyEmailAndPassword": "Скопировать email и пароль",
"ErrorPasswordMessage": "Пароль должен содержать",
"ErrorPasswordLength": "от {{fromNumber}} до {{toNumber}} символов",
"ErrorPasswordNoDigits": "цифры",
"ErrorPasswordNoUpperCase": "заглавные буквы",
"ErrorPasswordNoSpecialSymbols": "специальные символы",
"EmailAndPasswordCopiedToClipboard": "Email и пароль скопированы",
"PassworResetTitle": "Теперь вы можете создать новый пароль.",
"PasswordCustomMode": "Пароль",
"ImportContactsOkButton": "OK",
"LoadingProcessing": "Загрузка...",
"ChangePasswordSuccess": "Пароль был успешно изменен",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
}

View File

@ -3,30 +3,23 @@ import { withRouter } from "react-router";
import { withTranslation } from 'react-i18next';
import { PageLayout, Loader } from 'asc-web-components';
import { connect } from 'react-redux';
import { logout, validateActivatingEmail } from '../../../../store/auth/actions';
import { logout, changeEmail } from '../../../../store/auth/actions';
import PropTypes from 'prop-types';
class ActivateEmail extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
queryString: `type=EmailActivation&${props.location.search.slice(1)}`
};
}
componentDidMount() {
const { history, logout, validateActivatingEmail } = this.props;
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
const { history, logout, changeEmail, linkData } = this.props;
const [email, uid, key] = [linkData.email, linkData.uid, linkData.confirmHeader];
logout();
validateActivatingEmail(linkParams)
changeEmail(uid, email, key)
.then((res) => {
const email = decodeURIComponent(res.data.response.email);
history.push(`/login/confirmed-email=${email}`);
})
.catch((e) => {
console.log('activate email error', e);
history.push(`/login/error=${e}`);
});
}
@ -47,4 +40,4 @@ ActivateEmail.propTypes = {
const ActivateEmailForm = (props) => (<PageLayout sectionBodyContent={<ActivateEmail {...props} />} />);
export default connect(null, { logout, validateActivatingEmail })(withRouter(withTranslation()(ActivateEmailForm)));
export default connect(null, { logout, changeEmail })(withRouter(withTranslation()(ActivateEmailForm)));

View File

@ -0,0 +1,328 @@
import React from 'react';
import { withRouter } from "react-router";
import { withTranslation } from 'react-i18next';
import { Button, TextInput, PageLayout, Text, PasswordInput, toastr, Loader } from 'asc-web-components';
import styled from 'styled-components';
import { Collapse } from 'reactstrap';
import { connect } from 'react-redux';
import { welcomePageTitle } from './../../../../helpers/customNames';
import { EmployeeActivationStatus } from './../../../../helpers/constants';
import { getConfirmationInfo, activateConfirmUser } from '../../../../store/auth/actions';
import PropTypes from 'prop-types';
const inputWidth = '400px';
const ConfirmContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-left: 200px;
@media (max-width: 830px) {
margin-left: 40px;
}
.start-basis {
align-items: flex-start;
}
.margin-left {
margin-left: 20px;
}
.full-width {
width: ${inputWidth}
}
.confirm-row {
margin: 23px 0 0;
}
.break-word {
word-break: break-word;
}
.display-none {
display: none;
}
`;
const emailInputName = 'email';
class Confirm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: props.linkData.email,
firstName: props.linkData.firstname,
firstNameValid: true,
lastName: props.linkData.lastname,
lastNameValid: true,
password: '',
passwordValid: true,
errorText: '',
isLoading: false,
passwordEmpty: false,
key: props.linkData.confirmHeader,
linkType: props.linkData.type,
userId: props.linkData.uid
};
}
onSubmit = (e) => {
this.setState({ isLoading: true }, function () {
const { activateConfirmUser, history } = this.props;
this.setState({ errorText: "" });
let hasError = false;
if (!this.state.firstName.trim()) {
hasError = true;
this.setState({ firstNameValid: !hasError });
}
if (!this.state.lastName.trim()) {
hasError = true;
this.setState({ lastNameValid: !hasError });
}
if (!this.state.passwordValid) {
hasError = true;
this.setState({ passwordValid: !hasError });
}
if (!this.state.password.trim()) {
this.setState({ passwordEmpty: true });
hasError = true;
}
if (hasError) {
this.setState({ isLoading: false });
return false;
}
const loginData = {
userName: this.state.email,
password: this.state.password
};
const personalData = {
firstname: this.state.firstName,
lastname: this.state.lastName
};
activateConfirmUser(personalData, loginData, this.state.key, this.state.userId, EmployeeActivationStatus.Activated)
.then(() => history.push('/'))
.catch(error => {
console.error("activate error", error);
this.setState({
errorText: error,
isLoading: false
});
});
});
};
onKeyPress = (event) => {
if (event.key === "Enter") {
this.onSubmit();
}
};
onCopyToClipboard = () => toastr.success(this.props.t('EmailAndPasswordCopiedToClipboard'));
validatePassword = (value) => this.setState({ passwordValid: value });
componentDidMount() {
const { getConfirmationInfo, history } = this.props;
getConfirmationInfo(this.state.key, this.state.linkType)
.then(
function () {
console.log("get settings success");
}
)
.catch(e => {
console.error("get settings error", e);
history.push(`/login/error=${e}`);
});
window.addEventListener('keydown', this.onKeyPress);
window.addEventListener('keyup', this.onKeyPress);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyPress);
window.removeEventListener('keyup', this.onKeyPress);
}
onChangeName = event => {
this.setState({ firstName: event.target.value });
!this.state.firstNameValid && this.setState({ firstNameValid: event.target.value });
this.state.errorText && this.setState({ errorText: "" });
}
onChangeSurname = event => {
this.setState({ lastName: event.target.value });
!this.state.lastNameValid && this.setState({ lastNameValid: true });
this.state.errorText && this.setState({ errorText: "" });;
}
onChangePassword = event => {
this.setState({ password: event.target.value });
!this.state.passwordValid && this.setState({ passwordValid: true });
(event.target.value.trim()) && this.setState({ passwordEmpty: false });
this.state.errorText && this.setState({ errorText: "" });
this.onKeyPress(event);
}
render() {
console.log('ActivateUser render');
const { settings, isConfirmLoaded, t } = this.props;
return (
!isConfirmLoaded
? (
<Loader className="pageLoader" type="rombs" size={40} />
)
: (
<ConfirmContainer>
<div className='start-basis'>
<div className='margin-left'>
<Text.Body className='confirm-row' as='p' fontSize={18}>{t('InviteTitle')}</Text.Body>
<div className='confirm-row full-width break-word'>
<a href='/login'>
<img src="images/dark_general.png" alt="Logo" />
</a>
<Text.Body as='p' fontSize={24} color='#116d9d'>{t('CustomWelcomePageTitle', { welcomePageTitle })}</Text.Body>
</div>
</div>
<div>
<div className='full-width'>
<TextInput
className='confirm-row'
id='name'
name='name'
value={this.state.firstName}
placeholder={t('FirstName')}
size='huge'
scale={true}
tabIndex={1}
isAutoFocussed={true}
autoComplete='given-name'
isDisabled={this.state.isLoading}
hasError={!this.state.firstNameValid}
onChange={this.onChangeName}
onKeyDown={this.onKeyPress}
/>
<TextInput
className='confirm-row'
id='surname'
name='surname'
value={this.state.lastName}
placeholder={t('LastName')}
size='huge'
scale={true}
tabIndex={2}
autoComplete='family-name'
isDisabled={this.state.isLoading}
hasError={!this.state.lastNameValid}
onChange={this.onChangeSurname}
onKeyDown={this.onKeyPress}
/>
<TextInput
className='confirm-row display-none'
id='email'
name={emailInputName}
value={this.state.email}
size='huge'
scale={true}
isReadOnly={true}
/>
</div>
<PasswordInput
className='confirm-row'
id='password'
inputName='password'
emailInputName={emailInputName}
inputValue={this.state.password}
placeholder={t('InvitePassword')}
size='huge'
scale={true}
tabIndex={4}
maxLength={30}
inputWidth={inputWidth}
hasError={this.state.passwordEmpty}
onChange={this.onChangePassword}
onCopyToClipboard={this.onCopyToClipboard}
onValidateInput={this.validatePassword}
clipActionResource={t('CopyEmailAndPassword')}
clipEmailResource={`${t('Email')}: `}
clipPasswordResource={`${t('InvitePassword')}: `}
tooltipPasswordTitle={`${t('ErrorPasswordMessage')}:`}
tooltipPasswordLength={`${t('ErrorPasswordLength', { fromNumber: 6, toNumber: 30 })}:`}
tooltipPasswordDigits={t('ErrorPasswordNoDigits')}
tooltipPasswordCapital={t('ErrorPasswordNoUpperCase')}
tooltipPasswordSpecial={`${t('ErrorPasswordNoSpecialSymbols')} (!@#$%^&*)`}
generatorSpecial="!@#$%^&*"
passwordSettings={settings}
isDisabled={this.state.isLoading}
onKeyDown={this.onKeyPress}
/>
<Button
className='confirm-row'
primary
size='big'
label={t('LoginRegistryButton')}
tabIndex={5}
isLoading={this.state.isLoading}
onClick={this.onSubmit}
/>
</div>
{/* <Row className='confirm-row'>
<Text.Body as='p' fontSize={14}>{t('LoginWithAccount')}</Text.Body>
</Row>
*/}
<Collapse className='confirm-row'
isOpen={!!this.state.errorText}>
<div className="alert alert-danger">{this.state.errorText}</div>
</Collapse>
</div>
</ConfirmContainer>
)
);
}
}
Confirm.propTypes = {
getConfirmationInfo: PropTypes.func.isRequired,
activateConfirmUser: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
const ActivateUserForm = (props) => (<PageLayout sectionBodyContent={<Confirm {...props} />} />);
function mapStateToProps(state) {
return {
isConfirmLoaded: state.auth.isConfirmLoaded,
settings: state.auth.settings.passwordSettings
};
}
export default connect(mapStateToProps, { getConfirmationInfo, activateConfirmUser })(withRouter(withTranslation()(ActivateUserForm)));

View File

@ -3,35 +3,41 @@ import { withRouter } from "react-router";
import { withTranslation } from 'react-i18next';
import { PageLayout, Loader } from 'asc-web-components';
import { connect } from 'react-redux';
import { logout, changeEmail } from '../../../../store/auth/actions';
import { changeEmail } from '../../../../store/auth/actions';
import PropTypes from 'prop-types';
class ChangeEmail extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
queryString: `type=EmailChange&${props.location.search.slice(1)}`
};
componentDidMount() {
const { changeEmail, userId, isLoaded, linkData } = this.props;
if (isLoaded) {
const [email, key] = [linkData.email, linkData.confirmHeader];
changeEmail(userId, email , key)
.then((res) => {
console.log('change client email success', res)
window.location.href = `${window.location.origin}/products/people/view/@self?email_change=success`;
})
.catch((e) => {
console.log('change client email error', e)
window.location.href = `${window.location.origin}/error=${e}`;
});
}
}
componentDidUpdate(){
const { logout, changeEmail, userId, isLoaded } = this.props;
if (isLoaded){
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
// logout();
const email = decodeURIComponent(linkParams.email);
changeEmail(userId, {email}, this.state.queryString)
.then((res) => {
console.log('change client email success', res)
window.location.href = `${window.location.origin}/products/people/view/@self?email_change=success`;
})
.catch((e) => {
console.log('change client email error', e)
});
componentDidUpdate() {
const { changeEmail, userId, isLoaded, linkData } = this.props;
if (isLoaded) {
const [email, key] = [linkData.email, linkData.confirmHeader];
changeEmail(userId, email, key)
.then((res) => {
console.log('change client email success', res)
window.location.href = `${window.location.origin}/products/people/view/@self?email_change=success`;
})
.catch((e) => {
console.log('change client email error', e)
});
} else {
window.location.href = '/';
}
}
@ -43,11 +49,9 @@ class ChangeEmail extends React.PureComponent {
}
}
ChangeEmail.propTypes = {
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
changeEmail: PropTypes.func.isRequired
};
const ChangeEmailForm = (props) => (<PageLayout sectionBodyContent={<ChangeEmail {...props} />} />);
@ -58,4 +62,4 @@ function mapStateToProps(state) {
};
}
export default connect(mapStateToProps, { logout, changeEmail })(withRouter(withTranslation()(ChangeEmailForm)));
export default connect(mapStateToProps, { changeEmail })(withRouter(withTranslation()(ChangeEmailForm)));

View File

@ -1,29 +1,38 @@
import React, { useState, useEffect, useCallback } from "react";
import React from "react";
import { withRouter } from "react-router";
import { withTranslation } from "react-i18next";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import styled from "styled-components";
import { Row, Col, Card, CardImg, CardTitle } from "reactstrap";
import { Button, PageLayout, Text, PasswordInput } from "asc-web-components";
import { useTranslation } from "react-i18next";
import i18n from "../i18n";
import { Container, Row, Col, Card, CardTitle, CardImg } from "reactstrap";
import {
Button,
PageLayout,
Text,
PasswordInput,
Loader,
toastr
} from "asc-web-components";
import { welcomePageTitle } from "../../../../helpers/customNames";
import { changePassword } from "../../../../../src/store/auth/actions";
import {
changePassword,
getConfirmationInfo,
logout
} from "../../../../../src/store/auth/actions";
const BodyStyle = styled.div`
const BodyStyle = styled(Container)`
margin-top: 70px;
p {
margin-bottom: 5px;
}
.button-style {
margin-top: 20px;
}
.password-row {
margin: 23px 0 0;
.password-card {
border: none;
.card-img {
max-width: 216px;
max-height: 35px;
@ -39,160 +48,180 @@ const BodyStyle = styled.div`
}
`;
const Form = props => {
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errorText, setErrorText] = useState("");
const [passwordValid, setPasswordValid] = useState(true);
const { match, location, history, changePassword } = props;
const { params } = match;
const { t } = useTranslation("translation", { i18n });
class Form extends React.PureComponent {
constructor(props) {
super(props);
const { linkData } = props;
const onSubmit = useCallback(
e => {
errorText && setErrorText("");
let hasError = false;
if (!password.trim()) {
hasError = true;
setPasswordValid(!hasError);
}
if (hasError) return false;
setIsLoading(true);
console.log("changePassword onSubmit", match, location, history);
const str = location.search.split("&");
const userId = str[1].slice(4);
const key = `type=PasswordChange&${location.search.slice(1)}`;
changePassword(userId, {password}, key)
.then(() => {
console.log("UPDATE PASSWORD");
history.push("/");
})
.catch(e => {
history.push("/");
console.log("ERROR UPDATE PASSWORD", e);
});
},
[errorText, history, location, changePassword, match, password]
);
const onKeyPress = useCallback(
target => {
if (target.key === "Enter") {
onSubmit();
}
},
[onSubmit]
);
useEffect(() => {
params.error && setErrorText(params.error);
window.addEventListener("keydown", onKeyPress);
window.addEventListener("keyup", onKeyPress);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keydown", onKeyPress);
window.removeEventListener("keyup", onKeyPress);
this.state = {
password: "",
passwordValid: true,
isValidConfirmLink: false,
isLoading: false,
passwordEmpty: false,
key: linkData.confirmHeader,
userId: linkData.uid
};
}, [onKeyPress, params.error]);
}
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
onKeyPress = target => {
if (target.key === "Enter") {
this.onSubmit();
}
};
const tooltipPasswordLength =
"from " + settings.minLength + " to 30 characters";
onChange = event => {
this.setState({ password: event.target.value });
!this.state.passwordValid && this.setState({ passwordValid: true });
event.target.value.trim() && this.setState({ passwordEmpty: false });
this.onKeyPress(event);
};
const mdOptions = { size: 6, offset: 3 };
return (
<BodyStyle>
<Row className="password-row">
<Col sm="12" md={mdOptions}>
<Card className="password-card">
<CardImg
className="card-img"
src="images/dark_general.png"
alt="Logo"
top
onSubmit = e => {
this.setState({ isLoading: true }, function() {
const { userId, password, key } = this.state;
const { history, changePassword } = this.props;
let hasError = false;
if (!this.state.passwordValid) {
hasError = true;
this.setState({ passwordValid: !hasError });
}
!this.state.password.trim() && this.setState({ passwordEmpty: true });
if (hasError) {
this.setState({ isLoading: false });
return false;
}
changePassword(userId, password, key)
.then(() => {
history.push("/");
toastr.success(this.props.t("ChangePasswordSuccess"));
})
.catch(error => {
toastr.error(this.props.t(`${error}`));
this.setState({ isLoading: false });
});
});
};
componentDidMount() {
const { getConfirmationInfo, history } = this.props;
getConfirmationInfo(this.state.key)
.catch(error => {
toastr.error(this.props.t(`${error}`));
history.push("/");
});
window.addEventListener("keydown", this.onKeyPress);
window.addEventListener("keyup", this.onKeyPress);
}
componentWillUnmount() {
window.removeEventListener("keydown", this.onKeyPress);
window.removeEventListener("keyup", this.onKeyPress);
}
validatePassword = value => this.setState({ passwordValid: value });
render() {
const { settings, isConfirmLoaded, t } = this.props;
const { isLoading, password, passwordEmpty } = this.state;
const mdOptions = { size: 6, offset: 3 };
return !isConfirmLoaded ? (
<Loader className="pageLoader" type="rombs" size={40} />
) : (
<BodyStyle>
<Row className="password-row">
<Col sm="12" md={mdOptions}>
<Card className="password-card">
<CardImg
className="card-img"
src="images/dark_general.png"
alt="Logo"
top
/>
<CardTitle className="card-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
</CardTitle>
</Card>
<Text.Body fontSize={14}>{t("PassworResetTitle")}</Text.Body>
<PasswordInput
id="password"
name="password"
inputName="password"
inputValue={password}
size="huge"
scale={true}
type="password"
isDisabled={isLoading}
hasError={passwordEmpty}
onValidateInput={this.validatePassword}
generatorSpecial="!@#$%^&*"
tabIndex={1}
value={password}
onChange={this.onChange}
emailInputName="E-mail"
passwordSettings={settings}
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength={`${t("ErrorPasswordLength", {
fromNumber: 6,
toNumber: 30
})}:`}
placeholder={t("PasswordCustomMode")}
maxLength={30}
onKeyDown={this.onKeyPress}
isAutoFocussed={true}
inputWidth="490px"
/>
<CardTitle className="card-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
</CardTitle>
</Card>
<Button
className="button-style"
primary
size="big"
tabIndex={2}
label={
isLoading ? t("LoadingProcessing") : t("ImportContactsOkButton")
}
isDisabled={isLoading}
isLoading={isLoading}
onClick={this.onSubmit}
/>
</Col>
</Row>
</BodyStyle>
);
}
}
<Text.Body fontSize={14}>{t("PassworResetTitle")}</Text.Body>
<PasswordInput
id="password"
name="password"
size="huge"
scale={true}
type="password"
isDisabled={isLoading}
hasError={!passwordValid}
tabIndex={1}
value={password}
onChange={event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
onKeyPress(event.target);
}}
emailInputName="E-mail"
passwordSettings={settings}
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength={tooltipPasswordLength}
placeholder={t("PasswordCustomMode")}
maxLength={30}
//isAutoFocussed={true}
//autocomple="current-password"
//onKeyDown={event => onKeyPress(event.target)}
/>
</Col>
</Row>
<Row className="password-row">
<Col sm="12" md={mdOptions}>
<Button
primary
size="big"
tabIndex={3}
label={
isLoading ? t("LoadingProcessing") : t("ImportContactsOkButton")
}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</Col>
</Row>
</BodyStyle>
);
};
const ChangePasswordForm = props => {
return <PageLayout sectionBodyContent={<Form {...props} />} />;
};
ChangePasswordForm.propTypes = {
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
Form.propTypes = {
history: PropTypes.object.isRequired,
changePassword: PropTypes.func.isRequired
changePassword: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
linkData: PropTypes.object.isRequired
};
ChangePasswordForm.defaultProps = {
Form.defaultProps = {
password: ""
};
const ChangePasswordForm = props => (
<PageLayout sectionBodyContent={<Form {...props} />} />
);
function mapStateToProps(state) {
return {
isValidConfirmLink: state.auth.isValidConfirmLink,
isConfirmLoaded: state.auth.isConfirmLoaded,
settings: state.auth.settings.passwordSettings,
isAuthenticated: state.auth.isAuthenticated
};
}
export default connect(
null,
{ changePassword }
mapStateToProps,
{ changePassword, getConfirmationInfo, logout }
)(withRouter(withTranslation()(ChangePasswordForm)));

View File

@ -42,6 +42,7 @@ const PhoneForm = props => {
const { t, currentPhone } = props;
const [phone, setPhone] = useState(currentPhone);
// eslint-disable-next-line no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const subTitleTranslation = `Enter mobile phone number`;

View File

@ -6,7 +6,7 @@ import styled from 'styled-components';
import { Collapse } from 'reactstrap';
import { connect } from 'react-redux';
import { welcomePageTitle } from './../../../../helpers/customNames';
import { getPasswordSettings, createConfirmUser } from '../../../../store/auth/actions';
import { getConfirmationInfo, createConfirmUser, logout, login } from '../../../../store/auth/actions';
import PropTypes from 'prop-types';
const inputWidth = '400px';
@ -66,17 +66,22 @@ class Confirm extends React.PureComponent {
errorText: '',
isLoading: false,
passwordEmpty: false,
queryString: `type=LinkInvite&${props.location.search.slice(1)}`
key: props.linkData.confirmHeader,
linkType: props.linkData.type
};
}
onSubmit = (e) => {
this.setState({ isLoading: true }, function () {
const { history, createConfirmUser } = this.props;
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
const isVisitor = parseInt(linkParams.emplType) === 2;
/*componentWillMount() {
const { isAuthenticated, logout } = this.props;
if(isAuthenticated)
logout();
}*/
onSubmit = () => {
this.setState({ isLoading: true }, () => {
const { history, createConfirmUser, linkData } = this.props;
const isVisitor = parseInt(linkData.emplType) === 2;
this.setState({ errorText: "" });
@ -112,19 +117,26 @@ class Confirm extends React.PureComponent {
const loginData = {
userName: this.state.email,
password: this.state.password
}
const registerData = {
};
const personalData = {
firstname: this.state.firstName,
lastname: this.state.lastName,
email: this.state.email,
isVisitor: isVisitor
email: this.state.email
};
createConfirmUser(registerData, loginData, this.state.queryString)
.then(() => history.push('/'))
.catch(e => {
console.error("confirm error", e);
this.setState({ errorText: e.message });
this.setState({ isLoading: false });
const registerData = Object.assign(personalData, { isVisitor: isVisitor })
createConfirmUser(registerData, loginData, this.state.key)
.then(() => {
toastr.success("User has been created successfully");
return history.push('/');
})
.catch((error) => {
console.error("confirm error", error);
this.setState({
errorText: error,
isLoading: false
});
});
});
};
@ -139,9 +151,9 @@ class Confirm extends React.PureComponent {
validatePassword = (value) => this.setState({ passwordValid: value });
componentDidMount() {
const { getPasswordSettings, history } = this.props;
const { getConfirmationInfo, history } = this.props;
getPasswordSettings(this.state.queryString)
getConfirmationInfo(this.state.key, this.state.linkType)
.then(
function () {
console.log("get settings success");
@ -188,7 +200,7 @@ class Confirm extends React.PureComponent {
}
render() {
console.log('Confirm render');
console.log('createUser render');
const { settings, isConfirmLoaded, t } = this.props;
return (
!isConfirmLoaded
@ -260,6 +272,7 @@ class Confirm extends React.PureComponent {
onChange={this.onChangeEmail}
onKeyDown={this.onKeyPress}
/>
</div>
<PasswordInput
@ -292,7 +305,6 @@ class Confirm extends React.PureComponent {
onKeyDown={this.onKeyPress}
/>
<Button
className='confirm-row'
primary
@ -324,7 +336,7 @@ class Confirm extends React.PureComponent {
Confirm.propTypes = {
getPasswordSettings: PropTypes.func.isRequired,
getConfirmationInfo: PropTypes.func.isRequired,
createConfirmUser: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
@ -335,8 +347,9 @@ const CreateUserForm = (props) => (<PageLayout sectionBodyContent={<Confirm {...
function mapStateToProps(state) {
return {
isConfirmLoaded: state.auth.isConfirmLoaded,
settings: state.auth.password
isAuthenticated: state.auth.isAuthenticated,
settings: state.auth.settings.passwordSettings
};
}
export default connect(mapStateToProps, { getPasswordSettings, createConfirmUser })(withRouter(withTranslation()(CreateUserForm)));
export default connect(mapStateToProps, { getConfirmationInfo, createConfirmUser, login, logout })(withRouter(withTranslation()(CreateUserForm)));

View File

@ -0,0 +1,111 @@
import React from 'react';
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { Button, PageLayout, Text } from 'asc-web-components';
import styled from 'styled-components';
import { welcomePageTitle } from './../../../../helpers/customNames';
import PropTypes from 'prop-types';
import { withTranslation } from 'react-i18next';
import { deleteSelf } from './../../../../store/services/api';
import { logout } from '../../../../store/auth/actions';
const ProfileRemoveContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
.start-basis {
align-items: flex-start;
}
.confirm-row {
margin: 23px 0 0;
}
.break-word {
word-break: break-word;
}
`;
class ProfileRemove extends React.PureComponent {
constructor() {
super();
this.state = {
isProfileDeleted: false
};
}
onDeleteProfile = () => {
this.setState({ isLoading: true }, function () {
const { linkData, logout } = this.props;
deleteSelf(linkData.confirmHeader)
.then((res) => {
this.setState({
isLoading: false,
isProfileDeleted: true
});
//setAuthorizationToken();
console.log('success delete', res)
return logout()
})
.catch((e) => {
this.setState({ isLoading: false });
console.log('error delete', e)
})
});
};
render() {
console.log('profileRemove render');
const { t } = this.props;
const { isProfileDeleted } = this.state;
return (
<ProfileRemoveContainer>
<div className='start-basis'>
<div className='confirm-row full-width break-word'>
<a href='/login'>
<img src="images/dark_general.png" alt="Logo" />
</a>
<Text.Body as='p' fontSize={24} color='#116d9d'>{t('CustomWelcomePageTitle', { welcomePageTitle })}</Text.Body>
</div>
{!isProfileDeleted
? <>
<Text.Body className='confirm-row' as='p' fontSize={18} >{t('DeleteProfileConfirmation')}</Text.Body>
<Text.Body className='confirm-row' as='p' fontSize={16} >{t('DeleteProfileConfirmationInfo')}</Text.Body>
<Button
className='confirm-row'
primary
size='big'
label={t('DeleteProfileBtn')}
tabIndex={1}
isLoading={this.state.isLoading}
onClick={this.onDeleteProfile}
/>
</>
: <>
<Text.Body className='confirm-row' as='p' fontSize={18} >{t('DeleteProfileSuccessMessage')}</Text.Body>
<Text.Body className='confirm-row' as='p' fontSize={16} >{t('DeleteProfileSuccessMessageInfo')}</Text.Body>
</>
}
</div>
</ProfileRemoveContainer>
);
}
}
ProfileRemove.propTypes = {
location: PropTypes.object.isRequired,
};
const ProfileRemoveForm = (props) => (<PageLayout sectionBodyContent={<ProfileRemove {...props} />} />);
export default connect(null, { logout })(withRouter(withTranslation()(ProfileRemoveForm)));

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -1,9 +1,24 @@
import React from 'react';
import React, { useEffect } from 'react';
import { connect } from "react-redux";
import { ErrorContainer } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import i18n from './i18n';
export const Error404 = () => {
const Error404Container = ({language}) => {
const { t } = useTranslation('translation', { i18n });
useEffect(() => {
i18n.changeLanguage(language);
}, [language]);
return <ErrorContainer>{t("Error404Text")}</ErrorContainer>;
};
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture,
};
}
export const Error404 = connect(mapStateToProps)(Error404Container);

View File

@ -0,0 +1,3 @@
{
"Error404Text": "Извините, страница не найдена."
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { withRouter } from "react-router";
import { Container, Col, Row, Collapse } from 'reactstrap';
import { ModuleTile, Loader, PageLayout } from 'asc-web-components';
import { ModuleTile, Loader, PageLayout, toastr } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import i18n from './i18n';
@ -30,8 +30,14 @@ Tiles.propTypes = {
history: PropTypes.object.isRequired
};
const Body = ({ modules, history, isLoaded }) => {
const Body = ({ modules, match, history, isLoaded }) => {
const { t } = useTranslation('translation', { i18n });
const { params } = match;
useEffect(() => {
params.error && toastr.error(params.error);
}, [params.error]);
return (
!isLoaded
? (

View File

@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -1,210 +1,356 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import { Collapse, Container, Row, Col, Card, CardTitle, CardImg } from 'reactstrap';
import { Button, TextInput, PageLayout, Text } from 'asc-web-components';
import { connect } from 'react-redux';
import { login } from '../../../store/auth/actions';
import styled from 'styled-components';
import { useTranslation } from 'react-i18next';
import i18n from './i18n';
import { welcomePageTitle } from './../../../helpers/customNames';
import {
Collapse,
Container,
Row,
Col,
Card,
CardTitle,
CardImg
} from "reactstrap";
import {
Button,
TextInput,
PageLayout,
Text,
Link,
toastr,
Checkbox,
HelpButton
} from "asc-web-components";
import { connect } from "react-redux";
import { login } from "../../../store/auth/actions";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import i18n from "./i18n";
import { welcomePageTitle } from "./../../../helpers/customNames";
import { sendInstructionsToChangePassword } from "../../../store/services/api";
import SubModalDialog from "./sub-components/modal-dialog";
const FormContainer = styled(Container)`
margin-top: 70px;
margin-top: 70px;
.login-row {
margin: 23px 0 0;
.link-style {
float: right;
line-height: 16px;
}
.login-card {
border: none;
.text-body {
margin-bottom: 16px;
}
.card-img {
max-width: 216px;
max-height: 35px;
}
.card-title {
word-wrap: break-word;
margin: 8px 0;
text-align: left;
font-size: 24px;
color: #116d9d;
}
}
.btn-style {
margin-right: 8px;
}
.checkbox {
float: left;
span {
font-size: 12px;
}
}
.question-icon {
float: left;
margin-left: 4px;
line-height: 16px;
}
.login-row {
margin: 23px 0 0;
.login-card {
border: none;
.card-img {
max-width: 216px;
max-height: 35px;
}
.card-title {
word-wrap: break-word;
margin: 8px 0;
text-align: left;
font-size: 24px;
color: #116d9d;
}
}
}
.button-row {
margin: 16px 0 0;
}
`;
const TooltipStyle = styled.span`
margin-left: 3px;
position: absolute;
margin-top: 2px;
`;
const mdOptions = { size: 6, offset: 3 };
const Form = props => {
const { t } = useTranslation('translation', { i18n });
const { login, match, location, history } = props;
const { params } = match;
const [identifier, setIdentifier] = useState(params.confirmedEmail || '');
const [identifierValid, setIdentifierValid] = useState(true);
const [password, setPassword] = useState('');
const [passwordValid, setPasswordValid] = useState(true);
const [errorText, setErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation("translation", { i18n });
const { login, match, history, language } = props;
const { params } = match;
const [identifier, setIdentifier] = useState(params.confirmedEmail || "");
const [identifierValid, setIdentifierValid] = useState(true);
const [password, setPassword] = useState("");
const [passwordValid, setPasswordValid] = useState(true);
const [errorText, setErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const onSubmit = useCallback((e) => {
//e.preventDefault();
const [openDialog, setOpenDialog] = useState(false);
const [email, setEmail] = useState("");
const [isDisabled, setIsDisabled] = useState(false);
const [isChecked, setIsisChecked] = useState(false);
errorText && setErrorText("");
const onClick = () => {
setOpenDialog(true);
setIsDisabled(true);
setEmail(identifier);
};
let hasError = false;
const onDialogClose = () => {
setOpenDialog(false);
setIsDisabled(false);
setIsLoading(false);
setEmail("");
};
if (!identifier.trim()) {
hasError = true;
setIdentifierValid(!hasError);
}
const onSendPasswordInstructions = useCallback(() => {
setIsLoading(true);
sendInstructionsToChangePassword(email)
.then(res => toastr.success(res), message => toastr.error(message))
.finally(onDialogClose());
}, [email]);
if (!password.trim()) {
hasError = true;
setPasswordValid(!hasError);
}
const onSubmit = useCallback(() => {
errorText && setErrorText("");
let hasError = false;
if (hasError)
return false;
const userName = identifier.trim();
setIsLoading(true);
let payload = {
userName: identifier,
password: password
};
login(payload)
.then(function () {
console.log("auth success", match, location, history);
setIsLoading(false)
history.push('/');
})
.catch(e => {
console.error("auth error", e);
setErrorText(e.message);
setIsLoading(false)
});
}, [errorText, history, identifier, location, login, match, password]);
const onKeyPress = useCallback((event) => {
if (event.key === "Enter") {
onSubmit();
}
}, [onSubmit]);
useEffect(() => {
params.error && setErrorText(params.error);
window.addEventListener('keydown', onKeyPress);
window.addEventListener('keyup', onKeyPress);
// Remove event listeners on cleanup
return () => {
window.removeEventListener('keydown', onKeyPress);
window.removeEventListener('keyup', onKeyPress);
};
}, [onKeyPress, params]);
const onChangePassword = event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
if (!userName) {
hasError = true;
setIdentifierValid(!hasError);
}
const onChangeLogin = event => {
setIdentifier(event.target.value);
!identifierValid && setIdentifierValid(true);
errorText && setErrorText("");
const pass = password.trim();
if (!pass) {
hasError = true;
setPasswordValid(!hasError);
}
// console.log('Login render');
if (hasError) return false;
return (
<FormContainer>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<Card className="login-card">
<CardImg className="card-img" src="images/dark_general.png" alt="Logo" top />
<CardTitle className="card-title">{t('CustomWelcomePageTitle', { welcomePageTitle })}</CardTitle>
</Card>
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<TextInput
id="login"
name="login"
hasError={!identifierValid}
value={identifier}
placeholder={t('RegistrationEmailWatermark')}
size='huge'
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={onChangeLogin}
onKeyDown={onKeyPress} />
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<TextInput
id="password"
name="password"
type="password"
hasError={!passwordValid}
value={password}
placeholder={t('Password')}
size='huge'
scale={true}
tabIndex={2}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
onKeyDown={onKeyPress} />
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<Button
primary
size='big'
label={isLoading ? t('LoadingProcessing') : t('LoginButton')}
tabIndex={3}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit} />
</Col>
</Row>
{params.confirmedEmail && <Row className="login-row">
<Col sm="12" md={mdOptions}>
<Text.Body isBold={true} fontSize={16}>{t('MessageEmailConfirmed')} {t('MessageAuthorize')}</Text.Body>
</Col>
</Row>
}
<Collapse isOpen={!!errorText}>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<div className="alert alert-danger">{errorText}</div>
</Col>
</Row>
</Collapse>
</FormContainer>
setIsLoading(true);
login(userName, pass).then(
() => {
//console.log("auth success", match, location, history);
setIsLoading(false);
history.push("/");
},
error => {
//console.error("auth error", error);
setErrorText(error);
setIsLoading(false);
}
);
}
}, [errorText, history, identifier, login, password]);
const LoginForm = (props) => (<PageLayout sectionBodyContent={<Form {...props} />} />);
const onKeyPress = useCallback(
event => {
if (event.key === "Enter") {
!isDisabled ? onSubmit() : onSendPasswordInstructions();
}
},
[onSendPasswordInstructions, onSubmit, isDisabled]
);
useEffect(() => {
i18n.changeLanguage(language);
params.error && setErrorText(params.error);
window.addEventListener("keyup", onKeyPress);
// Remove event listeners on cleanup
return () => {
window.removeEventListener("keyup", onKeyPress);
};
}, [onKeyPress, params, language]);
const onChangePassword = event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
};
const onChangeLogin = event => {
setIdentifier(event.target.value);
!identifierValid && setIdentifierValid(true);
errorText && setErrorText("");
};
const onChangeEmail = event => {
setEmail(event.target.value);
};
// console.log('Login render');
return (
<FormContainer>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<Card className="login-card">
<CardImg
className="card-img"
src="images/dark_general.png"
alt="Logo"
top
/>
<CardTitle className="card-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
</CardTitle>
</Card>
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<TextInput
id="login"
name="login"
hasError={!identifierValid}
value={identifier}
placeholder={t("RegistrationEmailWatermark")}
size="huge"
scale={true}
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autoComplete="username"
onChange={onChangeLogin}
onKeyDown={onKeyPress}
/>
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<TextInput
id="password"
name="password"
type="password"
hasError={!passwordValid}
value={password}
placeholder={t("Password")}
size="huge"
scale={true}
tabIndex={2}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
onKeyDown={onKeyPress}
/>
</Col>
</Row>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<Link
fontSize={12}
className="link-style"
type="page"
isHovered={true}
onClick={onClick}
>
{t("ForgotPassword")}
</Link>
<Checkbox
className="checkbox"
isChecked={isChecked}
onChange={() => setIsisChecked(!isChecked)}
label={t("Remember")}
/>
<TooltipStyle>
<HelpButton
tooltipContent={
<Text.Body fontSize={12}>{t("RememberHelper")}</Text.Body>
}
/>
</TooltipStyle>
</Col>
</Row>
{openDialog ? (
<SubModalDialog
openDialog={openDialog}
isLoading={isLoading}
email={email}
onChangeEmail={onChangeEmail}
onSendPasswordInstructions={onSendPasswordInstructions}
onDialogClose={onDialogClose}
t={t}
/>
) : null}
<Row className="button-row">
<Col sm="12" md={mdOptions}>
<Button
primary
size="big"
label={isLoading ? t("LoadingProcessing") : t("LoginButton")}
tabIndex={3}
isDisabled={isLoading}
isLoading={isLoading}
onClick={onSubmit}
/>
</Col>
</Row>
{params.confirmedEmail && (
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<Text.Body isBold={true} fontSize={16}>
{t("MessageEmailConfirmed")} {t("MessageAuthorize")}
</Text.Body>
</Col>
</Row>
)}
<Collapse isOpen={!!errorText}>
<Row className="login-row">
<Col sm="12" md={mdOptions}>
<div className="alert alert-danger">{errorText}</div>
</Col>
</Row>
</Collapse>
</FormContainer>
);
};
const LoginForm = props => (
<PageLayout sectionBodyContent={<Form {...props} />} />
);
LoginForm.propTypes = {
login: PropTypes.func.isRequired,
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
login: PropTypes.func.isRequired,
match: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
LoginForm.defaultProps = {
identifier: "",
password: ""
identifier: "",
password: "",
email: ""
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName || state.auth.settings.culture
};
}
export default connect(null, { login })(withRouter(LoginForm));
export default connect(
mapStateToProps,
{ login }
)(withRouter(LoginForm));

View File

@ -5,6 +5,13 @@
"RegistrationEmailWatermark": "Your registration email",
"MessageEmailConfirmed": "Your email was activated successfully.",
"MessageAuthorize": "Please authorize yourself.",
"ForgotPassword": "Forgot your password?",
"PasswordRecoveryTitle": "Password recovery",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Please enter the email you used while registering on the portal. The password recovery instructions will be send to that email address.",
"SendButton": "Send",
"CancelButton": "Cancel",
"Remember": "Remember",
"RememberHelper": "The default session lifetime is 20 minutes. Check this option to set it to 1 year. To set your own value, go to the settings.",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
}

View File

@ -0,0 +1,17 @@
{
"LoadingProcessing": "Загрузка...",
"LoginButton": "Войти",
"Password": "Пароль",
"RegistrationEmailWatermark": "Регистрационный email",
"MessageEmailConfirmed": "Ваш email успешно активирован.",
"MessageAuthorize": "Пожалуйста авторизуйтесь.",
"ForgotPassword": "Забыли пароль?",
"PasswordRecoveryTitle": "Восстановление пароля",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Пожалуйста, введите адрес электронной почты, указанный при регистрации на портале. Инструкции для восстановления пароля будут отправлены на этот адрес электронной почты.",
"SendButton": "Отправить",
"CancelButton": "Отмена",
"Remember": "Запомнить",
"RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
}

View File

@ -0,0 +1,86 @@
import React from "react";
import PropTypes from "prop-types";
import { Button, TextInput, Text, ModalDialog } from "asc-web-components";
class SubModalDialog extends React.Component {
render() {
const {
openDialog,
isLoading,
email,
onChangeEmail,
onSendPasswordInstructions,
onDialogClose,
t
} = this.props;
return (
<ModalDialog
visible={openDialog}
headerContent={
<Text.Body isBold={false} fontSize={21}>
{t("PasswordRecoveryTitle")}
</Text.Body>
}
bodyContent={[
<Text.Body
key="text-body"
className="text-body"
isBold={false}
fontSize={13}
>
{t("MessageSendPasswordRecoveryInstructionsOnEmail")}
</Text.Body>,
<TextInput
key="e-mail"
id="e-mail"
name="e-mail"
type="text"
size="base"
scale={true}
tabIndex={1}
isDisabled={isLoading}
value={email}
onChange={onChangeEmail}
/>
]}
footerContent={[
<Button
className="btn-style"
key="SendBtn"
label={isLoading ? t("LoadingProcessing") : t("SendButton")}
size="big"
scale={false}
primary={true}
onClick={onSendPasswordInstructions}
isLoading={isLoading}
isDisabled={isLoading}
tabIndex={2}
/>,
<Button
key="CancelBtn"
label={t("CancelButton")}
size="big"
scale={false}
primary={false}
onClick={onDialogClose}
isDisabled={isLoading}
tabIndex={3}
/>
]}
onClose={onDialogClose}
/>
);
}
}
SubModalDialog.propTypes = {
openDialog: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
email: PropTypes.string.isRequired,
onChangeEmail: PropTypes.func.isRequired,
onSendPasswordInstructions: PropTypes.func.isRequired,
onDialogClose: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
};
export default SubModalDialog;

View File

@ -0,0 +1,210 @@
import React from 'react';
import { utils } from 'asc-web-components';
import { connect } from 'react-redux';
import {
TreeMenu,
TreeNode,
Icons,
Link
} from "asc-web-components";
import { setNewSelectedNode } from '../../../../../store/auth/actions';
import { withRouter } from "react-router";
import { settingsTree } from '../../../../../helpers/constants';
import styled from 'styled-components';
import { withTranslation } from 'react-i18next';
const StyledTreeMenu = styled(TreeMenu)`
.inherit-title-link {
& > span {
font-size: inherit;
font-weight: inherit;
}
}
`;
const getItems = (data, path, t) => {
return data.map(item => {
if (item.children && item.children.length) {
const link = path + getSelectedLinkByKey(item.key);
return (
<TreeNode
title={<Link className='inherit-title-link' href={link}>{t(`Settings_${item.link}`)}</Link>}
key={item.key}
icon={item.icon && React.createElement(Icons[item.icon], {
size: 'scale',
isfill: true,
color: 'dimgray',
})}
>
{getItems(item.children, path, t)}
</TreeNode>
);
};
const link = path + getSelectedLinkByKey(item.key);
return (
<TreeNode
key={item.key}
title={<Link className='inherit-title-link' href={link}>{t(`Settings_${item.link}`)}</Link>}
icon={item.icon && React.createElement(Icons[item.icon], {
size: 'scale',
isfill: true,
color: 'dimgray',
})}
/>
);
});
};
const getKeyByLink = (data, linkArr) => {
const length = linkArr.length;
if (length === 1 || !linkArr[1].length) {
const arrLength = data.length;
for (let i = 0; i < arrLength; i++) {
if (data[i].link === linkArr[0]) {
return data[i].children ? data[i].children[0].key : data[i].key;
}
}
} else if (length === 2) {
const arrLength = data.length;
let key;
for (let i = 0; i < arrLength; i++) {
if (data[i].link === linkArr[0]) {
key = i;
break;
}
}
const selectedArr = data[key].children;
const childrenLength = selectedArr.length;
for (let i = 0; i < childrenLength; i++) {
if (selectedArr[i].link === linkArr[1]) {
return selectedArr[i].key;
}
}
}
return '0-0';
}
const getSelectedLinkByKey = key => {
const length = key.length;
if (length === 1) {
return '/' + settingsTree[key].link;
}
else if (length === 3) {
return '/' + settingsTree[key[0]].link + '/' + settingsTree[key[0]].children[key[2]].link;
}
}
class ArticleBodyContent extends React.Component {
constructor(props) {
super(props);
const { selectedKeys, match, history, setNewSelectedNode, i18n, language } = props;
const fullSettingsUrl = props.match.url;
const locationPathname = props.location.pathname;
if (locationPathname === fullSettingsUrl) {
const newPath = match.path + getSelectedLinkByKey(selectedKeys[0]);
history.push(newPath);
return;
}
const fullSettingsUrlLength = fullSettingsUrl.length;
const resultPath = locationPathname.slice(fullSettingsUrlLength + 1);
const arrayOfParams = resultPath.split('/');
const key = getKeyByLink(settingsTree, arrayOfParams);
const link = getSelectedLinkByKey(key);
setNewSelectedNode([key]);
const path = match.path + link;
history.push(path);
i18n.changeLanguage(language);
}
componentDidUpdate() {
const { selectedKeys, match, history } = this.props;
const settingsPath = getSelectedLinkByKey(selectedKeys[0]);
const newPath = match.path + settingsPath;
history.push(newPath);
}
shouldComponentUpdate(nextProps) {
if (!utils.array.isArrayEqual(nextProps.selectedKeys, this.props.selectedKeys)) {
return true;
}
return false;
}
onSelect = value => {
const { selectedKeys, setNewSelectedNode } = this.props;
if (value) {
if (utils.array.isArrayEqual(value, selectedKeys)) {
return;
}
const selectedKey = value[0];
if (selectedKey.length === 3) {
setNewSelectedNode(value);
}
else if (selectedKey.length === 1 && (selectedKey.toString() !== selectedKeys.toString()[0] || selectedKeys.toString()[2] !== '0')) {
const selectedKeys = settingsTree[value].children ? [`${value.toString()}-0`] : value;
setNewSelectedNode(selectedKeys);
}
}
};
switcherIcon = obj => {
if (obj.isLeaf) {
return null;
}
if (obj.expanded) {
return (
<Icons.ExpanderDownIcon size="scale" isfill={true} color="dimgray" />
);
} else {
return (
<Icons.ExpanderRightIcon size="scale" isfill={true} color="dimgray" />
);
}
};
render() {
const { selectedKeys, match, t } = this.props;
console.log("SettingsTreeMenu", this.props);
return (
<StyledTreeMenu
className="people-tree-menu"
checkable={false}
draggable={false}
disabled={false}
multiple={false}
showIcon={true}
defaultExpandAll={true}
switcherIcon={this.switcherIcon}
onSelect={this.onSelect}
selectedKeys={selectedKeys}
>
{getItems(settingsTree, match.path, t)}
</StyledTreeMenu>
);
};
};
function mapStateToProps(state) {
return {
selectedKeys: state.auth.settings.settingsTree.selectedKey,
language: state.auth.user.cultureName
};
}
export default connect(mapStateToProps, { setNewSelectedNode })(withRouter(withTranslation()(ArticleBodyContent)));

View File

@ -0,0 +1,10 @@
import React from 'react';
import { Text } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
const ArticleHeaderContent = () => {
const { t } = useTranslation();
return <Text.MenuHeader>{t('Settings')}</Text.MenuHeader>;
}
export default ArticleHeaderContent;

View File

@ -0,0 +1,2 @@
export { default as ArticleHeaderContent } from './Header';
export { default as ArticleBodyContent } from './Body';

View File

@ -0,0 +1,33 @@
import React, { lazy } from "react";
import { Route, Switch } from "react-router-dom";
import { withRouter } from "react-router";
import { Scrollbar } from 'asc-web-components'
const CustomizationSettings = lazy(() => import("../../sub-components/common/customization"));
const NotImplementedSettings = lazy(() => import("../../sub-components/notImplementedSettings"));
const AccessRight = lazy(() => import("../../sub-components/security/accessRights"));
class SectionBodyContent extends React.PureComponent {
render() {
return (
<Scrollbar stype="mediumBlack">
<Switch>
<Route
exact
path={[`${this.props.match.path}/common/customization`,`${this.props.match.path}/common`, this.props.match.path]}
component={CustomizationSettings}
/>
<Route
exact
path={`${this.props.match.path}/security/access-rights`}
component={AccessRight}
/>
<Route component={NotImplementedSettings} />
</Switch>
</Scrollbar>
);
};
};
export default withRouter(SectionBodyContent);

View File

@ -0,0 +1,45 @@
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { Text, utils } from 'asc-web-components';
import styled from 'styled-components';
import { settingsTree } from '../../../../../helpers/constants';
import { useTranslation } from 'react-i18next';
const Header = styled(Text.ContentHeader)`
margin-left: 16px;
margin-right: 16px;
max-width: calc(100vw - 430px);
@media ${utils.device.tablet} {
max-width: calc(100vw - 96px);
}
`;
const getSelectedLinkByKey = key => {
const length = key.length;
if (length === 1) {
return settingsTree[key].link;
}
else if (length === 3) {
return settingsTree[key[0]].children[key[2]].link;
}
};
const SectionHeaderContent = props => {
const { t } = useTranslation();
const header = getSelectedLinkByKey(props.selectedKey)
return (
<Header truncate={true}>
{t(`Settings_${header}`)}
</Header>
);
};
function mapStateToProps(state) {
return {
selectedKey: state.auth.settings.settingsTree.selectedKey[0]
};
}
export default connect(mapStateToProps)(withRouter(SectionHeaderContent));

Some files were not shown because too many files have changed in this diff Show More