This commit is contained in:
NikolayRechkin 2019-09-25 09:04:30 +03:00
commit 7bcf2a6bf8
125 changed files with 4531 additions and 1585 deletions

View File

@ -2,15 +2,15 @@
"folders": [
{
"name": "ASC.Web.Components"
"path": ".\\web\\ASC.Web.Components"
"path": "./web/ASC.Web.Components"
},
{
"name": "ASC.Web.Client"
"path": ".\\web\\ASC.Web.Client"
"path": "./web/ASC.Web.Client"
},
{
"name": "ASC.People.Client"
"path": ".\\products\\ASC.People\\Client"
"path": "./products/ASC.People/Client"
}
],
"settings": {}

View File

@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0-preview8.19405.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Authentication;
using System.Security.Claims;
using System.Text.Encodings.Web;
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.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@ -22,36 +26,62 @@ namespace ASC.Api.Core.Auth
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (SecurityContext.IsAuthenticated)
var Request = QueryHelpers.ParseQuery(Context.Request.Headers["confirm"]);
_ = Request.TryGetValue("type", out var type);
var _type = typeof(ConfirmType).TryParseEnum(type, ConfirmType.EmpInvite);
if (SecurityContext.IsAuthenticated && _type != ConfirmType.EmailChange)
{
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)));
}
var Request = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(Context.Request.Headers["confirm"]);
_ = Request.TryGetValue("type", out var type);
_ = Request.TryGetValue("key", out var key);
_ = Request.TryGetValue("emplType", out var emplType);
_ = Request.TryGetValue("email", out var _email);
var validInterval = SetupInfo.ValidEmailKeyInterval;
var _type = typeof(ConfirmType).TryParseEnum(type, ConfirmType.EmpInvite);
EmailValidationKeyProvider.ValidationResult checkKeyResult;
switch (_type)
{
case ConfirmType.EmpInvite:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + emplType, key, validInterval);
break;
case ConfirmType.LinkInvite:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_type + emplType, key, validInterval);
break;
case ConfirmType.EmailChange:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + SecurityContext.CurrentAccount.ID, key, validInterval);
break;
case ConfirmType.PasswordChange:
var userHash = Request.TryGetValue("p", out var p) && p == "1";
var hash = string.Empty;
if (userHash)
{
var tenantId = CoreContext.TenantManager.GetCurrentTenant().TenantId;
hash = CoreContext.Authentication.GetUserPasswordHash(tenantId, CoreContext.UserManager.GetUserByEmail(tenantId, _email).ID);
}
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type + (string.IsNullOrEmpty(hash) ? string.Empty : Hasher.Base64Hash(hash)), key, validInterval);
break;
default:
checkKeyResult = EmailValidationKeyProvider.ValidateEmailKey(_email + _type, key, validInterval);
break;
}
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem);
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Role, _type.ToString())
};
if (!SecurityContext.IsAuthenticated)
{
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem, claims);
}
else
{
SecurityContext.AuthenticateMe(SecurityContext.CurrentAccount, claims);
}
var result = checkKeyResult switch
{

View File

@ -30,27 +30,27 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ARSoft.Tools.NetStandard.DXSdata" Version="1.0.0" />
<PackageReference Include="Autofac" Version="4.9.3" />
<PackageReference Include="Autofac" Version="4.9.4" />
<PackageReference Include="Autofac.Configuration" Version="4.1.0" />
<PackageReference Include="Confluent.Kafka" Version="1.1.0" />
<PackageReference Include="Google.Protobuf" Version="3.9.1" />
<PackageReference Include="Grpc" Version="2.23.0-pre1" />
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="Confluent.Kafka" Version="1.2.0" />
<PackageReference Include="Google.Protobuf" Version="3.10.0-rc1" />
<PackageReference Include="Grpc" Version="2.24.0-pre1" />
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="log4net" Version="2.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="3.0.0-preview8.19405.7" />
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="3.0.0-preview8.19405.3" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="3.0.0" />
<PackageReference Include="MySql.Data" Version="8.0.17" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NLog" Version="4.6.6" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.8.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3-beta1" />
<PackageReference Include="NLog" Version="4.6.7" />
<PackageReference Include="NLog.Web.AspNetCore" Version="4.8.5" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NVelocity" Version="1.2.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />

View File

@ -44,15 +44,15 @@
<None Remove="protos\UserPhotoCacheItem.proto" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.CloudFront" Version="3.3.101.30" />
<PackageReference Include="AWSSDK.Core" Version="3.3.103.27" />
<PackageReference Include="AWSSDK.S3" Version="3.3.104.15" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.3.101.33" />
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="AWSSDK.CloudFront" Version="3.3.101.46" />
<PackageReference Include="AWSSDK.Core" Version="3.3.103.43" />
<PackageReference Include="AWSSDK.S3" Version="3.3.104.31" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.3.101.50" />
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MailKit" Version="2.2.0" />
<PackageReference Include="MailKit" Version="2.3.1.6" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="protos\NotifyMessage.proto" />

View File

@ -157,7 +157,7 @@ namespace ASC.Core
public bool UserExists(int tenantId, Guid id)
{
return !UserExists(GetUsers(tenantId, id));
return UserExists(GetUsers(tenantId, id));
}
public bool UserExists(UserInfo user)

View File

@ -169,7 +169,7 @@ namespace ASC.Core
return false;
}
public static string AuthenticateMe(IAccount account)
public static string AuthenticateMe(IAccount account, List<Claim> additionalClaims = null)
{
if (account == null || account.Equals(Configuration.Constants.Guest)) throw new InvalidCredentialException("account");
@ -222,6 +222,11 @@ namespace ASC.Core
};
claims.AddRange(roles.Select(r => new Claim(ClaimTypes.Role, r)));
if(additionalClaims != null)
{
claims.AddRange(additionalClaims);
}
Principal = new CustomClaimsPrincipal(new ClaimsIdentity(account, claims), account);
return cookie;

View File

@ -102,7 +102,8 @@ namespace ASC.Core.Data
var groupQuery = new SqlQuery("core_usergroup cug")
.Select("cug.userid")
.Where(Exp.EqColumns("cug.tenant", "u.tenant"))
.Where(Exp.EqColumns("u.id", "cug.userid"));
.Where(Exp.EqColumns("u.id", "cug.userid"))
.Where("cug.removed", false);
foreach (var groups in includeGroups)
{

View File

@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="UAParser" Version="3.1.41" />
<PackageReference Include="UAParser" Version="3.1.42" />
</ItemGroup>
<ItemGroup>

View File

@ -20,14 +20,14 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Api.Gax" Version="2.10.0-beta02" />
<PackageReference Include="Google.Api.Gax.Rest" Version="2.10.0-beta02" />
<PackageReference Include="Google.Apis" Version="1.40.3" />
<PackageReference Include="Google.Apis.Auth" Version="1.40.3" />
<PackageReference Include="Google.Apis.Core" Version="1.40.3" />
<PackageReference Include="Google.Apis.Storage.v1" Version="1.40.3.1635" />
<PackageReference Include="Google.Api.Gax" Version="2.10.0-beta04" />
<PackageReference Include="Google.Api.Gax.Rest" Version="2.10.0-beta04" />
<PackageReference Include="Google.Apis" Version="1.41.1" />
<PackageReference Include="Google.Apis.Auth" Version="1.41.1" />
<PackageReference Include="Google.Apis.Core" Version="1.41.1" />
<PackageReference Include="Google.Apis.Storage.v1" Version="1.41.1.1716" />
<PackageReference Include="Google.Cloud.Storage.V1" Version="2.4.0-beta03" />
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="DotNetOpenAuth.Ultimate" Version="4.3.4.13329" />
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -14,7 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="UAParser" Version="3.1.41" />
<PackageReference Include="UAParser" Version="3.1.42" />
</ItemGroup>
<ItemGroup>

View File

@ -13,9 +13,9 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.6.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" />
</ItemGroup>

View File

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.Tools" Version="2.23.0-pre1">
<PackageReference Include="Grpc.Tools" Version="2.24.0-pre1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3-beta1" />
</ItemGroup>
<ItemGroup>

View File

@ -53,7 +53,8 @@
"hub": {
"url": "",
"internal": ""
}
},
"cultures": "en-US,ru-RU"
},
"ConnectionStrings": {
"default": {

View File

@ -9,7 +9,7 @@
"ConnectionStrings": {
"default": {
"name": "default",
"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",
"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",
"providerName": "MySql.Data.MySqlClient"
}
},

View File

@ -8,6 +8,7 @@
"axios": "^0.19.0",
"bootstrap": "4.3.1",
"connected-react-router": "6.5.2",
"copy-to-clipboard": "^3.2.0",
"history": "4.9.0",
"i18next": "17.0.12",
"i18next-browser-languagedetector": "3.0.3",

View File

@ -9,6 +9,7 @@ import Profile from './components/pages/Profile';
import ProfileAction from './components/pages/ProfileAction';
import GroupAction from './components/pages/GroupAction';
import { Error404 } from "./components/pages/Error";
import Reassign from './components/pages/Reassign';
/*const Profile = lazy(() => import("./components/pages/Profile"));
const ProfileAction = lazy(() => import("./components/pages/ProfileAction"));
@ -49,6 +50,11 @@ const App = ({ settings }) => {
component={GroupAction}
restricted
/>
<PrivateRoute
path={`${homepage}/reassign/:userId`}
component={Reassign}
restricted
/>
<PrivateRoute component={Error404} />
</Switch>
</Suspense>

View File

@ -7,12 +7,20 @@ import {
DropDownItem,
toastr
} from "asc-web-components";
import InviteDialog from './../../dialogs/Invite';
import { isAdmin } from '../../../store/auth/selectors';
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
import { typeUser, typeGuest, department } from './../../../helpers/customNames';
class PureArticleMainButtonContent extends React.Component {
constructor(props) {
super(props);
this.state = {
dialogVisible: false
}
}
onDropDownItemClick = (link) => {
this.props.history.push(link);
};
@ -21,48 +29,57 @@ class PureArticleMainButtonContent extends React.Component {
toastr.success(text);
};
onInvitationDialogClick = () => this.setState({ dialogVisible: !this.state.dialogVisible });
render() {
console.log("People ArticleMainButtonContent render");
const { isAdmin, settings, t } = this.props;
return (
isAdmin ?
<MainButton
isDisabled={false}
isDropdown={true}
text={t('Actions')}
>
<DropDownItem
icon="CatalogEmployeeIcon"
label={t('CustomNewEmployee', { typeUser })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/create/user`)}
<>
<MainButton
isDisabled={false}
isDropdown={true}
text={t('Actions')}
>
<DropDownItem
icon="CatalogEmployeeIcon"
label={t('CustomNewEmployee', { typeUser })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/create/user`)}
/>
<DropDownItem
icon="CatalogGuestIcon"
label={t('CustomNewGuest', { typeGuest })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/create/guest`)}
/>
<DropDownItem
icon="CatalogDepartmentsIcon"
label={t('CustomNewDepartment', { department })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/group/create`)}
/>
<DropDownItem isSeparator />
<DropDownItem
icon="InvitationLinkIcon"
label={t('InviteLinkTitle')}
onClick={this.onInvitationDialogClick}
/>
<DropDownItem
icon="PlaneIcon"
label={t('LblInviteAgain')}
onClick={this.onNotImplementedClick.bind(this, "Invite again action")}
/>
<DropDownItem
icon="ImportIcon"
label={t('ImportPeople')}
onClick={this.onNotImplementedClick.bind(this, "Import people action")}
/>
</MainButton>
<InviteDialog
visible={this.state.dialogVisible}
onClose={this.onInvitationDialogClick}
onCloseButton={this.onInvitationDialogClick}
/>
<DropDownItem
icon="CatalogGuestIcon"
label={t('CustomNewGuest', { typeGuest })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/create/guest`)}
/>
<DropDownItem
icon="CatalogDepartmentsIcon"
label={t('CustomNewDepartment', { department })}
onClick={this.onDropDownItemClick.bind(this, `${settings.homepage}/group/create`)}
/>
<DropDownItem isSeparator />
<DropDownItem
icon="InvitationLinkIcon"
label={t('InviteLinkTitle')}
onClick={this.onNotImplementedClick.bind(this, "Invitation link action")}
/>
<DropDownItem
icon="PlaneIcon"
label={t('LblInviteAgain')}
onClick={this.onNotImplementedClick.bind(this, "Invite again action")}
/>
<DropDownItem
icon="ImportIcon"
label={t('ImportPeople')}
onClick={this.onNotImplementedClick.bind(this, "Import people action")}
/>
</MainButton>
</>
:
<></>
);

View File

@ -23,7 +23,7 @@ class PurePeopleLayout extends React.Component {
}
onAboutClick = () => {
console.log('About clicked');
window.location.href = "/about";
}
onLogoutClick = () => {

View File

@ -0,0 +1,57 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import config from "../../../../package.json";
const newInstance = i18n.createInstance();
if (process.env.NODE_ENV === "production") {
newInstance
.use(Backend)
.init({
lng: 'en',
fallbackLng: "en",
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
format: function (value, format) {
if (format === 'lowercase') return value.toLowerCase();
return value;
}
},
react: {
useSuspense: true
},
backend: {
loadPath: `${config.homepage}/locales/Invite/{{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',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
format: function (value, format) {
if (format === 'lowercase') return value.toLowerCase();
return value;
}
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,201 @@
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
} 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';
const ModalDialogContainer = styled.div`
.margin-text {
margin: 12px 0;
}
.margin-link {
margin-right: 12px;
}
.margin-textarea {
margin-top: 12px;
}
.flex{
display: flex;
justify-content: space-between;
}
`;
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'));
};
onCheckedGuest = () => this.setState({ isGuest: !this.state.isGuest });
onGetShortenedLink = () => {
this.setState({ isLoading: true });
const { getShortenedLink, userInvitationLink, guestInvitationLink } = this.props;
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 });
});
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);
});
};
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();
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}
/>
</>
)}
/>
</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>;
InviteDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onCloseButton: PropTypes.func.isRequired
};
export default connect(mapStateToProps, { getInvitationLink, getShortenedLink })(withRouter(InviteDialog));

View File

@ -0,0 +1,13 @@
{
"InviteLinkTitle": "Invitation link",
"HelpAnswerLinkInviteSettings": "Share the link to invite your colleagues to your portal.",
"InviteLinkValidInterval": "This link is valid for {{ count }} day only.",
"CopyToClipboard": "Copy the link",
"CloseButton": "Close",
"LinkCopySuccess": "Link has been copied to the clipboard",
"GetShortenLink": "Get shortened link",
"InviteUsersAsCollaborators": "Add users as {{typeGuests, lowercase}}",
"LoadingProcessing": "Loading...",
"InviteLinkValidInterval_plural": "This link is valid for {{ count }} days only."
}

View File

@ -1,116 +1,522 @@
import React, { useCallback, useState, useEffect } from 'react';
import { withRouter } from 'react-router';
import React from "react";
import { withTranslation } from "react-i18next";
import { withRouter } from "react-router";
import PropTypes from "prop-types";
import {
Button,
TextInput,
Text,
InputBlock,
Icons,
SelectedItem
SelectedItem,
AdvancedSelector,
FieldContainer,
ComboBox,
ComboButton,
ModalDialog,
SearchInput,
toastr,
utils
} from "asc-web-components";
import { useTranslation } from 'react-i18next';
import { department, headOfDepartment, typeUser } from '../../../../../helpers/customNames';
import { connect } from 'react-redux';
import { resetGroup } from '../../../../../store/group/actions';
import {
department,
headOfDepartment,
typeUser
} from "../../../../../helpers/customNames";
import { connect } from "react-redux";
import {
resetGroup,
createGroup,
updateGroup
} from "../../../../../store/group/actions";
import styled from "styled-components";
import { fetchSelectorUsers } from "../../../../../store/people/actions";
import { GUID_EMPTY } from "../../../../../helpers/constants";
import isEqual from "lodash/isEqual";
const SectionBodyContent = (props) => {
const { history, group, resetGroup } = props;
const [value, setValue] = useState(group ? group.name : "");
const [error, setError] = useState(null);
const [inLoading, setInLoading] = useState(false);
const { t } = useTranslation();
const MainContainer = styled.div`
display: flex;
flex-direction: column;
useEffect(() => {
setValue(group ? group.name : "");
setError(null);
setInLoading(false);
}, [group]);
.group-name_container {
width: 320px;
}
const groupMembers = group && group.members ? group.members : [];
.head_container {
position: relative;
width: 320px;
}
.members_container {
position: relative;
width: 320px;
}
.search_container {
margin-top: 16px;
}
.selected-members_container {
margin-top: 16px;
display: flex;
flex-wrap: wrap;
flex-direction: row;
.selected-item {
margin-right: 8px;
margin-bottom: 8px;
}
}
.buttons_container {
margin-top: 60px;
.cancel-button {
margin-left: 8px;
}
}
@media ${utils.device.tablet} {
.search_container {
width: 320px;
}
}
`;
class SectionBodyContent extends React.Component {
constructor(props) {
super(props);
this.state = this.mapPropsToState();
}
mapPropsToState = () => {
const { group, users, groups, t } = this.props;
const newState = {
id: group ? group.id : "",
groupName: group ? group.name : "",
searchValue: "",
error: null,
inLoading: false,
isHeaderSelectorOpen: false,
isUsersSelectorOpen: false,
users: users,
groups: groups,
modalVisible: false,
header: group
? {
key: 0,
label: "{SELECTED HEADER NAME}" //group.head
}
: {
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 })
}
};
return newState;
}
componentDidMount() {
const { users, fetchSelectorUsers } = this.props;
if(!users || !users.length) {
fetchSelectorUsers();
}
}
componentDidUpdate(prevProps) {
//const { users, group } = this.props;
if(!isEqual(this.props, prevProps)) {
this.setState(this.mapPropsToState());
}
}
onGroupChange = e => {
this.setState({
groupName: e.target.value
});
};
onSearchChange = e => {
this.setState({
searchValue: e.target.value
});
};
onHeadSelectorSearch = value => {
/*setOptions(
options.filter(option => {
return option.label.indexOf(value) > -1;
})
);*/
};
onHeadSelectorSelect = option => {
this.setState({
groupManager: {
key: option.key,
label: option.label
},
isHeaderSelectorOpen: !this.state.isHeaderSelectorOpen
});
};
onHeadSelectorClick = () => {
this.setState({
isHeaderSelectorOpen: !this.state.isHeaderSelectorOpen
});
};
onUsersSelectorSearch = (value) => {
/*setOptions(
options.filter(option => {
return option.label.indexOf(value) > -1;
})
);*/
};
onUsersSelectorSelect = (selectedOptions) => {
//console.log("onSelect", selectedOptions);
//this.onUsersSelectorClick();
this.setState({
groupMembers: selectedOptions.map(option => {
return {
key: option.key,
label: option.label
};
}),
isUsersSelectorOpen: !this.state.isUsersSelectorOpen
});
};
onUsersSelectorClick = () => {
this.setState({
isUsersSelectorOpen: !this.state.isUsersSelectorOpen
});
};
toggleModalVisible = () => {
this.setState({
modalVisible: !this.state.modalVisible
});
};
onSave = () => {
const { history, group, createGroup, updateGroup, resetGroup } = 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 });
resetGroup();
history.goBack();
})
.catch(error => {
toastr.error(error.message);
this.setState({ inLoading: false });
});
};
onCancel = () => {
const { history, resetGroup } = this.props;
const onCancel = useCallback(() => {
resetGroup();
history.goBack();
}, [history, resetGroup]);
};
const onChange = useCallback((e) => setValue(e.target.value), [setValue]);
onSelectedItemClose = (member) => {
this.setState({
groupMembers: this.state.groupMembers.filter(g => g.key !== member.key)
});
}
console.log("Group render", props);
renderModal = () => {
const { groups, modalVisible } = this.state;
return (
<>
<div>
<label htmlFor="group-name">
<Text.Body as="span" isBold={true}>{t('CustomDepartmentName', { department })}:</Text.Body>
</label>
<div style={{width: "320px"}}>
<TextInput id="group-name" name="group-name" scale={true} isAutoFocussed={true} tabIndex={1} value={value} onChange={onChange} />
</div>
</div>
<div style={{ marginTop: "16px" }}>
<label htmlFor="head-selector">
<Text.Body as="span" isBold={true}>{t('CustomHeadOfDepartment', { headOfDepartment })}:</Text.Body>
</label>
<InputBlock
id="head-selector"
value={t('CustomAddEmployee', { typeUser })}
iconName="ExpanderDownIcon"
iconSize={8}
isIconFill={true}
iconColor="#A3A9AE"
scale={false}
isReadOnly={true}
tabIndex={2}
>
<Icons.CatalogEmployeeIcon size="medium" />
</InputBlock>
</div>
<div style={{ marginTop: "16px" }}>
<label htmlFor="employee-selector">
<Text.Body as="span" isBold={true}>Members:</Text.Body>
</label>
<InputBlock
id="employee-selector"
value={t('CustomAddEmployee', { typeUser })}
iconName="ExpanderDownIcon"
iconSize={8}
isIconFill={true}
iconColor="#A3A9AE"
scale={false}
isReadOnly={true}
tabIndex={3}
>
<Icons.CatalogGuestIcon size="medium" />
</InputBlock>
</div>
<div style={{ marginTop: "16px", display: "flex", flexWrap: "wrap", flexDirection: "row" }}>
{groupMembers.map(member =>
<SelectedItem
key={member.id}
text={member.displayName}
onClick={(e) => console.log("onClick", e.target)}
onClose={(e) => console.log("onClose", e.target)}
isInline={true}
style={{ marginRight: "8px", marginBottom: "8px" }}
return (
<ModalDialog
zIndex={1001}
visible={modalVisible}
headerContent="New User"
bodyContent={
<div className="create_new_user_modal">
<FieldContainer
isVertical={true}
isRequired={true}
hasError={false}
labelText={"First name:"}
>
<TextInput
value={""}
hasError={false}
className="firstName-input"
scale={true}
autoComplete="off"
onChange={e => {
//set(e.target.value);
}}
/>
</FieldContainer>
<FieldContainer
isVertical={true}
isRequired={true}
hasError={false}
labelText={"Last name:"}
>
<TextInput
value={""}
hasError={false}
className="lastName-input"
scale={true}
autoComplete="off"
onChange={e => {
//set(e.target.value);
}}
/>
</FieldContainer>
<FieldContainer
isVertical={true}
isRequired={true}
hasError={false}
labelText={"E-mail:"}
>
<TextInput
value={""}
hasError={false}
className="email-input"
scale={true}
autoComplete="off"
onChange={e => {
//set(e.target.value);
}}
/>
</FieldContainer>
<FieldContainer
isVertical={true}
isRequired={true}
hasError={false}
labelText={"Group:"}
>
<ComboBox
options={groups}
className="group-input"
onSelect={option => console.log("Selected option", option)}
selectedOption={{
key: 0,
label: "Select"
}}
dropDownMaxHeight={200}
scaled={true}
scaledOptions={true}
size="content"
/>
</FieldContainer>
</div>
}
footerContent={[
<Button
key="CreateBtn"
label="Create"
primary={true}
size="big"
onClick={e => {
console.log("CreateBtn click", e);
this.toggleModalVisible();
}}
/>
]}
onClose={this.toggleModalVisible}
/>
);
};
render() {
const { t } = this.props;
const {
groupName,
users,
groups,
groupMembers,
isHeaderSelectorOpen: isHeadSelectorOpen,
isUsersSelectorOpen,
inLoading,
error,
searchValue,
modalVisible,
groupManager
} = this.state;
return (
<MainContainer>
<div style={{ visibility: "hidden", width: 1, height: 1 }}>
<Icons.SearchIcon size="small" />
</div>
<FieldContainer
className="group-name_container"
isRequired={true}
hasError={false}
isVertical={true}
labelText={t("CustomDepartmentName", { department })}
>
<TextInput
id="group-name"
name="group-name"
scale={true}
isAutoFocussed={true}
tabIndex={1}
value={groupName}
onChange={this.onGroupChange}
/>
</FieldContainer>
<FieldContainer
className="head_container"
isRequired={false}
hasError={false}
isVertical={true}
labelText={t("CustomHeadOfDepartment", { headOfDepartment })}
>
<ComboButton
id="head-selector"
tabIndex={2}
options={[]}
isOpen={isHeadSelectorOpen}
selectedOption={groupManager}
scaled={true}
size="content"
opened={isHeadSelectorOpen}
onClick={this.onHeadSelectorClick}
>
<Icons.CatalogGuestIcon size="medium" />
</ComboButton>
<AdvancedSelector
isDropDown={true}
isOpen={isHeadSelectorOpen}
size="full"
placeholder={"Search"}
onSearchChanged={this.onHeadSelectorSearch}
options={users}
groups={groups}
isMultiSelect={false}
buttonLabel={t("CustomAddEmployee", { typeUser })}
selectAllLabel={"Select all"}
onSelect={this.onHeadSelectorSelect}
onCancel={this.onHeadSelectorClick}
allowCreation={false}
//onAddNewClick={toggleModalVisible}
allowAnyClickClose={true}
/>
</FieldContainer>
<FieldContainer
className="members_container"
isRequired={false}
hasError={false}
isVertical={true}
labelText="Members"
>
<ComboButton
id="users-selector"
tabIndex={3}
options={[]}
isOpen={isUsersSelectorOpen}
selectedOption={{
key: 0,
label: t("CustomAddEmployee", { typeUser })
}}
scaled={true}
size="content"
opened={isUsersSelectorOpen}
onClick={this.onUsersSelectorClick}
>
<Icons.CatalogGuestIcon size="medium" />
</ComboButton>
<AdvancedSelector
isDropDown={true}
isOpen={isUsersSelectorOpen}
size="full"
placeholder={"Search"}
onSearchChanged={this.onUsersSelectorSearch}
options={users}
groups={groups}
isMultiSelect={true}
buttonLabel={t("CustomAddEmployee", { typeUser })}
selectAllLabel={"Select all"}
onSelect={this.onUsersSelectorSelect}
onCancel={this.onUsersSelectorClick}
allowCreation={true}
onAddNewClick={this.toggleModalVisible}
allowAnyClickClose={!modalVisible}
/>
</FieldContainer>
{groupMembers && groupMembers.length > 0 && (
<div className="search_container">
<SearchInput
id="member-search"
isDisabled={inLoading}
scale={true}
placeholder="Search"
value={searchValue}
onChange={this.onSearchChange}
/>
</div>
)}
</div>
<div>{error && <strong>{error}</strong>}</div>
<div style={{ marginTop: "60px" }}>
<Button label={t('SaveButton')} primary type="submit" isDisabled={inLoading} size="big" tabIndex={4} />
<Button
label={t('CancelButton')}
style={{ marginLeft: "8px" }}
size="big"
isDisabled={inLoading}
onClick={onCancel}
tabIndex={5}
/>
</div>
</>
);
};
<div className="selected-members_container">
{groupMembers.map(member => (
<SelectedItem
key={member.key}
text={member.label}
onClose={this.onSelectedItemClose.bind(this, member)}
isInline={true}
className="selected-item"
/>
))}
</div>
<div>{error && <strong>{error}</strong>}</div>
<div className="buttons_container">
<Button
label={t("SaveButton")}
primary
type="submit"
isLoading={inLoading}
size="big"
tabIndex={4}
onClick={this.onSave}
/>
<Button
label={t("CancelButton")}
className="cancel-button"
size="big"
isDisabled={inLoading}
onClick={this.onCancel}
tabIndex={5}
/>
</div>
{this.renderModal()}
</MainContainer>
);
}
}
SectionBodyContent.propTypes = {
group: PropTypes.object
@ -118,13 +524,42 @@ SectionBodyContent.propTypes = {
SectionBodyContent.defaultProps = {
group: null
}
};
const convertUsers = users => {
return users
? users.map(u => {
return {
key: u.id,
groups: u.groups || [],
label: u.displayName
};
})
: [];
};
const convertGroups = groups => {
return groups
? groups.map(g => {
return {
key: g.id,
label: g.name,
total: 0
};
})
: [];
};
function mapStateToProps(state) {
return {
settings: state.auth.settings,
group: state.group.targetGroup
group: state.group.targetGroup,
groups: convertGroups(state.people.groups),
users: convertUsers(state.people.selector.users) //TODO: replace to api requests with search
};
};
}
export default connect(mapStateToProps, { resetGroup })(withRouter(SectionBodyContent));
export default connect(
mapStateToProps,
{ resetGroup, createGroup, updateGroup, fetchSelectorUsers }
)(withRouter(withTranslation()(SectionBodyContent)));

View File

@ -1,11 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import PropTypes from "prop-types";
import { IconButton, Text } from 'asc-web-components';
import { useTranslation } from 'react-i18next';
import { department } from './../../../../../helpers/customNames';
import { IconButton, Text } from "asc-web-components";
import { withTranslation } from "react-i18next";
import { department } from "./../../../../../helpers/customNames";
import { resetGroup } from "../../../../../store/group/actions";
const wrapperStyle = {
display: "flex",
@ -16,19 +16,31 @@ const textStyle = {
marginLeft: "16px"
};
const SectionHeaderContent = (props) => {
const {group, history, settings} = props;
const { t } = useTranslation();
class SectionHeaderContent extends React.Component {
onBackClick = () => {
const { history, settings, resetGroup } = this.props;
const headerText = group ? t('CustomEditDepartment', { department }) : t('CustomNewDepartment', { department });
resetGroup();
history.push(settings.homepage);
};
return (
<div style={wrapperStyle}>
<IconButton iconName={'ArrowPathIcon'} size="16" onClick={() => history.push(settings.homepage)}/>
<Text.ContentHeader style={textStyle}>{headerText}</Text.ContentHeader>
</div>
);
};
render() {
const { group, t } = this.props;
const headerText = group
? t("CustomEditDepartment", { department })
: t("CustomNewDepartment", { department });
return (
<div style={wrapperStyle}>
<IconButton
iconName={"ArrowPathIcon"}
size="16"
onClick={this.onBackClick}
/>
<Text.ContentHeader style={textStyle}>{headerText}</Text.ContentHeader>
</div>
);
}
}
SectionHeaderContent.propTypes = {
group: PropTypes.object,
@ -44,6 +56,9 @@ function mapStateToProps(state) {
settings: state.auth.settings,
group: state.group.targetGroup
};
};
}
export default connect(mapStateToProps)(withRouter(SectionHeaderContent));
export default connect(
mapStateToProps,
{ resetGroup }
)(withTranslation()(withRouter(SectionHeaderContent)));

View File

@ -37,6 +37,7 @@ class GroupAction extends React.Component {
<I18nextProvider i18n={i18n}>
{group || !match.params.groupId
? <PageLayout
withBodyScroll={false}
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}

View File

@ -186,8 +186,9 @@ class SectionBodyContent extends React.PureComponent {
.finally(() => onLoading(false));
};
onReassignDataClick = () => {
toastr.success("Context action: Reassign data");
onReassignDataClick = user => {
const { history, settings } = this.props;
history.push(`${settings.homepage}/reassign/${user.userName}`);
};
onDeletePersonalDataClick = user => {
@ -390,7 +391,7 @@ class SectionBodyContent extends React.PureComponent {
{
key: "reassign-data",
label: t("ReassignData"),
onClick: this.onReassignDataClick
onClick: this.onReassignDataClick.bind(this, user)
},
{
key: "delete-personal-data",

View File

@ -16,11 +16,7 @@ import {
} from "asc-web-components";
import { connect } from "react-redux";
import styled from 'styled-components';
import {
toEmployeeWrapper,
getUserRole,
getUserContacts
} from "../../../../../store/people/selectors";
import { getUserRole, getUserContacts } from "../../../../../store/people/selectors";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
const ProfileWrapper = styled.div`
@ -448,6 +444,7 @@ const SectionBodyContent = props => {
<EditButtonWrapper>
<Button
size="big"
scale={true}
label={t("EditUserDialogTitle")}
onClick={onEditProfileClick}
/>

View File

@ -1,15 +1,13 @@
import React from "react";
import { connect } from "react-redux";
import {
Text,
IconButton,
ContextMenuButton,
toastr
} from "asc-web-components";
import { Text, IconButton, ContextMenuButton, toastr } from "asc-web-components";
import { withRouter } from "react-router";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
import { getUserStatus } from "../../../../../store/people/selectors";
import { useTranslation } from 'react-i18next';
import { resendUserInvites } from "../../../../../store/services/api";
import { EmployeeStatus } from "../../../../../helpers/constants";
import { updateUserStatus } from "../../../../../store/people/actions";
const wrapperStyle = {
display: "flex",
@ -22,10 +20,12 @@ const textStyle = {
};
const SectionHeaderContent = props => {
const { profile, history, settings, isAdmin, viewer } = props;
const { profile, history, settings, isAdmin, viewer, updateUserStatus } = props;
const onEditClick = user => {
history.push(`${settings.homepage}/edit/${user.userName}`);
const selectedUserIds = new Array(profile.id);
const onEditClick = () => {
history.push(`${settings.homepage}/edit/${profile.userName}`);
};
const onChangePasswordClick = () => {
@ -41,7 +41,8 @@ const SectionHeaderContent = props => {
};
const onDisableClick = () => {
toastr.success("Context action: Disable");
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
};
const onEditPhoto = () => {
@ -49,31 +50,34 @@ const SectionHeaderContent = props => {
};
const onEnableClick = () => {
toastr.success("Context action: Enable");
updateUserStatus(EmployeeStatus.Active, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
};
const onReassignDataClick = () => {
toastr.success("Context action: Reassign data");
const onReassignDataClick = user => {
const { history, settings } = props;
history.push(`${settings.homepage}/reassign/${user.userName}`);
};
const onDeletePersonalDataClick = user => {
const onDeletePersonalDataClick = () => {
toastr.success("Context action: Delete personal data");
};
const onDeleteProfileClick = () => {
toastr.success("Context action: Delete profile");
};
const onInviteAgainClick = () => {
toastr.success("Context action: Invite again");
};
const onInviteAgainClick = () => {
resendUserInvites(selectedUserIds)
.then(() => toastr.success("The invitation was successfully sent"))
.catch(e => toastr.error("ERROR"));
};
const getUserContextOptions = (user, viewer, t) => {
let status = "";
if(isAdmin|| (!isAdmin && isMe(user, viewer.userName))) {
status = getUserStatus(user);
if (isAdmin || (!isAdmin && isMe(user, viewer.userName))) {
status = getUserStatus(user);
}
switch (status) {
@ -83,7 +87,7 @@ const SectionHeaderContent = props => {
{
key: "edit",
label: t('EditUserDialogTitle'),
onClick: onEditClick.bind(this, user)
onClick: onEditClick
},
{
key: "edit-photo",
@ -126,12 +130,12 @@ const SectionHeaderContent = props => {
{
key: "reassign-data",
label: t('ReassignData'),
onClick: onReassignDataClick
onClick: onReassignDataClick.bind(this, user)
},
{
key: "delete-personal-data",
label: t('RemoveData'),
onClick: onDeletePersonalDataClick.bind(this, user)
onClick: onDeletePersonalDataClick
},
{
key: "delete-profile",
@ -144,7 +148,7 @@ const SectionHeaderContent = props => {
{
key: "edit",
label: t('EditButton'),
onClick: onEditClick.bind(this, user)
onClick: onEditClick
},
{
key: "edit-photo",
@ -157,7 +161,7 @@ const SectionHeaderContent = props => {
onClick: onInviteAgainClick
},
{
key: "key5",
key: "disable",
label: t('DisableUserButton'),
onClick: onDisableClick
}
@ -207,4 +211,4 @@ function mapStateToProps(state) {
};
}
export default connect(mapStateToProps)(withRouter(SectionHeaderContent));
export default connect(mapStateToProps, { updateUserStatus })(withRouter(SectionHeaderContent));

View File

@ -0,0 +1,27 @@
import React from "react";
import { withRouter } from "react-router";
// import { useTranslation } from 'react-i18next';
import { connect } from "react-redux";
import styled from 'styled-components';
const InfoContainer = styled.div`
margin-bottom: 24px;
`;
const SectionBodyContent = props => {
// const { t } = useTranslation();
return (
<InfoContainer>
See this feature in next version!
</InfoContainer>
);
};
function mapStateToProps(state) {
return {
};
}
export default connect(mapStateToProps)(withRouter(SectionBodyContent));

View File

@ -0,0 +1,48 @@
import React from "react";
import { connect } from "react-redux";
import { Text, IconButton } from "asc-web-components";
import { withRouter } from "react-router";
import { useTranslation } from 'react-i18next';
const wrapperStyle = {
display: "flex",
alignItems: "center"
};
const textStyle = {
marginLeft: "16px",
marginRight: "16px"
};
const SectionHeaderContent = props => {
const { history, settings } = props;
const { t } = useTranslation();
return (
<div style={wrapperStyle}>
<div style={{ width: "16px" }}>
<IconButton
iconName={"ArrowPathIcon"}
color="#A3A9AE"
size="16"
onClick={() => history.push(settings.homepage)}
/>
</div>
<Text.ContentHeader truncate={true} style={textStyle}>
{/* {profile.displayName}
{profile.isLDAP && ` (${t('LDAPLbl')})`}
- */}
{t('ReassignmentData')}
</Text.ContentHeader>
</div>
);
};
function mapStateToProps(state) {
return {
profile: state.profile.targetUser,
settings: state.auth.settings
};
}
export default connect(mapStateToProps)(withRouter(SectionHeaderContent));

View File

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

View File

@ -0,0 +1,50 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
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/Reassign/{{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',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,75 @@
import React from "react";
import { connect } from "react-redux";
// import PropTypes from "prop-types";
import { PageLayout } from "asc-web-components";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
// import { SectionHeaderContent } from './Section';
// import { fetchProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
import { SectionHeaderContent, SectionBodyContent } from './Section';
class Reassign extends React.Component {
componentDidMount() {
// const { match, fetchProfile } = this.props;
// const { userId } = match.params;
// if (userId) {
// fetchProfile(userId);
// }
}
componentDidUpdate(prevProps) {
// const { match, fetchProfile } = this.props;
// const { userId } = match.params;
// const prevUserId = prevProps.match.params.userId;
// if (userId !== undefined && userId !== prevUserId) {
// fetchProfile(userId);
// }
}
render() {
console.log("Reassign render")
// let loaded = false;
// const { profile, match } = this.props;
// const { userId, type } = match.params;
// if (type) {
// loaded = true;
// } else if (profile) {
// loaded = profile.userName === userId || profile.id === userId;
// }
return (
<I18nextProvider i18n={i18n}>
<PageLayout
articleHeaderContent={<ArticleHeaderContent />}
articleMainButtonContent={<ArticleMainButtonContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={<SectionHeaderContent />}
sectionBodyContent={<SectionBodyContent />}
/>
</I18nextProvider>
);
}
}
Reassign.propTypes = {
// match: PropTypes.object.isRequired,
// profile: PropTypes.object,
// fetchProfile: PropTypes.func.isRequired
};
function mapStateToProps(state) {
return {
// profile: state.profile.targetUser
};
}
export default connect(mapStateToProps, {
})(Reassign);

View File

@ -0,0 +1,16 @@
{
"ReassignmentData": "Reassignment of data",
"ReassignsToUser": "Employee to whom the data will be transferred —",
"ChooseUser": "Choose user",
"ReassignsTransferedListHdr": "Will be transferred:",
"ReassignsTransferedListItem1": "General documents and personal documents that are available to other portal users;",
"ReassignsTransferedListItem2": "Open projects, milestones and tasks;",
"ReassignsTransferedListItem3": "Contacts, open tasks, unclosed opportunities and CRM cases;",
"NotBeUndone": "Note: this action cannot be undone.",
"ReassignsReadMore": "More about data transfer",
"DeleteProfileAfterReassignment": "Delete profile when reassignment will be finished",
"CancelButton": "Cancel",
"ReassignButton": "Reassign",
"LDAPLbl": "LDAP"
}

View File

@ -4,7 +4,6 @@ $font-family-base: 'Open Sans', sans-serif;
// Import Bootstrap and its default variables
@import '~bootstrap/scss/bootstrap.scss';
@import '~react-toastify/dist/ReactToastify.min.css';
@import "~react-datepicker/dist/react-datepicker.css";
html, body {
height: 100%;

View File

@ -1,4 +1,5 @@
export const AUTH_KEY = "asc_auth_key";
export const GUID_EMPTY = "00000000-0000-0000-0000-000000000000";
/**
* Enum for employee activation status.

View File

@ -3,5 +3,6 @@ export const department = 'Department';
export const position = 'Position';
export const employedSinceDate = 'Employed since';
export const typeGuest = 'Guest';
export const typeGuests = 'Guests';
export const typeUser = 'Employee';
export const headOfDepartment = 'Head of Department';

View File

@ -9,6 +9,22 @@
"ImportPeople"
]
},
"dialogs": {
"Invite": {
"Resource": [
"HelpAnswerLinkInviteSettings",
"CopyToClipboard",
"CloseButton",
"GetShortenLink",
"LoadingProcessing",
"InviteUsersAsCollaborators",
"InviteLinkTitle"
],
"ResourceJS": [
"LinkCopySuccess"
]
}
},
"pages": {
"Profile": {
"Resource": [
@ -112,5 +128,25 @@
"SaveButton",
"CancelButton"
]
},
"Reassign":{
"PeopleResource":[
"ReassignmentData",
"ReassignsTransferedListHdr",
"ReassignsTransferedListItem1",
"ReassignsTransferedListItem2",
"ReassignsTransferedListItem3",
"ReassignsReadMore",
"DeleteProfileAfterReassignment",
"CancelButton",
"ReassignButton",
"ReassignsToUser"
],
"UserControlsCommonResource.resx":[
"NotBeUndone"
],
"PeopleJSResource":[
"ChooseUser"
]
}
}

View File

@ -23,7 +23,8 @@ const initialState = {
datePattern: "mm/dd/yy",
dateTimePattern: "DD, mm dd, yy h:mm:ss tt",
timePattern: "h:mm tt"
}
},
hasShortenService: false
}
};

View File

@ -1,7 +1,8 @@
import * as api from "../../store/services/api";
import { setGroups } from '../people/actions';
export const SET_GROUP = "SET_PROFILE";
export const CLEAN_GROUP = "CLEAN_PROFILE";
export const SET_GROUP = "SET_GROUP";
export const CLEAN_GROUP = "CLEAN_GROUP";
export function setGroup(targetGroup) {
return {
@ -32,3 +33,49 @@ export function fetchGroup(groupId) {
});
};
}
export function createGroup(groupName, groupManager, members) {
return (dispatch, getState) => {
const { people } = getState();
const { groups } = people;
return api.createGroup(groupName, groupManager, members)
.then(res => {
checkResponseError(res);
const newGroup = res.data.response;
dispatch(setGroup(newGroup));
dispatch(setGroups([...groups, newGroup]));
return Promise.resolve(newGroup);
});
};
};
export function updateGroup(id, groupName, groupManager, members) {
return (dispatch, getState) => {
const { people } = getState();
const { groups } = people;
return api.updateGroup(id, groupName, groupManager, members)
.then(res => {
checkResponseError(res);
const newGroup = res.data.response;
dispatch(setGroup(newGroup));
const newGroups = groups.map(g => {
if(g.id === id) {
return newGroup;
}
return g;
})
dispatch(setGroups(newGroups));
return Promise.resolve(newGroup);
});;
};
};

View File

@ -10,6 +10,7 @@ export const DESELECT_USER = "DESELECT_USER";
export const SET_SELECTED = "SET_SELECTED";
export const SET_FILTER = "SET_FILTER";
export const SELECT_GROUP = "SELECT_GROUP";
export const SET_SELECTOR_USERS = "SET_SELECTOR_USERS";
export function setUser(user) {
return {
@ -79,6 +80,20 @@ export function setFilter(filter) {
};
}
export function setSelectorUsers(users) {
return {
type: SET_SELECTOR_USERS,
users
};
}
export function fetchSelectorUsers() {
return dispatch => {
api.getSelectorUserList()
.then(res => dispatch(setSelectorUsers(res.data.response)));
};
}
export function fetchPeople(filter) {
return dispatch => {
return fetchPeopleByFilter(dispatch, filter);

View File

@ -7,7 +7,8 @@ import {
SET_SELECTED,
SET_FILTER,
SELECT_GROUP,
SET_USER
SET_USER,
SET_SELECTOR_USERS
} from "./actions";
import { isUserSelected, skipUser, getUsersBySelected } from "./selectors";
import Filter from "./filter";
@ -18,7 +19,10 @@ const initialState = {
selection: [],
selected: "none",
selectedGroup: null,
filter: Filter.getDefault()
filter: Filter.getDefault(),
selector: {
users: []
}
};
const peopleReducer = (state = initialState, action) => {
@ -66,6 +70,12 @@ const peopleReducer = (state = initialState, action) => {
return Object.assign({}, state, {
selectedGroup: action.groupId
});
case SET_SELECTOR_USERS:
return Object.assign({}, state, {
selector: Object.assign({}, state.selector, {
users: action.users
})
});
default:
return state;
}

View File

@ -31,7 +31,7 @@ export function employeeWrapperToMemberModel(profile) {
const department = profile.groups ? profile.groups.map(group => group.id) : [];
const worksFrom = profile.workFrom;
return {...profile, comment, department, worksFrom};
return { ...profile, comment, department, worksFrom };
}
export function fetchProfile(userName) {
@ -56,8 +56,8 @@ export function fetchProfile(userName) {
export function createProfile(profile) {
return (dispatch, getState) => {
const {people} = getState();
const {filter} = people;
const { people } = getState();
const { filter } = people;
const member = employeeWrapperToMemberModel(profile);
let result;
@ -75,8 +75,8 @@ export function createProfile(profile) {
export function updateProfile(profile) {
return (dispatch, getState) => {
const {people} = getState();
const {filter} = people;
const { people } = getState();
const { filter } = people;
const member = employeeWrapperToMemberModel(profile);
let result;
@ -120,5 +120,26 @@ export function deleteAvatar(profileId) {
checkResponseError(res);
return Promise.resolve(res);
});
}
};
};
export function getInvitationLink(isGuest = false) {
return dispatch => {
return api.getInvitationLink(isGuest)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
}
}
export function getShortenedLink(link) {
return dispatch => {
return api.getShortenedLink(link)
.then(res => {
checkResponseError(res);
return Promise.resolve(res);
});
}
}

View File

@ -7,6 +7,7 @@ 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);
@ -16,17 +17,17 @@ 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 } });
});
.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 } });
});
}
export function getSettings() {
@ -47,6 +48,10 @@ export function getUser(userId) {
: axios.get(`${API_URL}/people/${userId || "@self"}.json`);
}
export function getSelectorUserList() {
return axios.get(`${API_URL}/people.json?fields=id,displayName,groups`);
}
export function getUserList(filter = Filter.getDefault()) {
const params =
filter && filter instanceof Filter
@ -86,8 +91,8 @@ export function deleteAvatar(profileId) {
}
export function getInitInfo() {
return axios.all([getUser(), getModulesList(), getSettings(), getPortalPasswordSettings()]).then(
axios.spread(function(userResp, modulesResp, settingsResp, passwordSettingsResp) {
return axios.all([getUser(), getModulesList(), getSettings(), getPortalPasswordSettings(), getInvitationLink(), getInvitationLink(true)]).then(
axios.spread(function (userResp, modulesResp, settingsResp, passwordSettingsResp, userInvitationLinkResp, guestInvitationLinkResp) {
let info = {
user: userResp.data.response,
modules: modulesResp.data.response,
@ -95,6 +100,10 @@ export function getInitInfo() {
};
info.settings.passwordSettings = passwordSettingsResp.data.response;
info.settings.inviteLinks = {
userLink: userInvitationLinkResp,
guestLink: guestInvitationLinkResp
}
return Promise.resolve(info);
})
@ -157,6 +166,49 @@ export function getGroup(groupId) {
: axios.get(`${API_URL}/group/${groupId}.json`);
}
export function getInvitationLink(isGuest) {
let localStorageLinkTtl = localStorage.getItem('localStorageLinkTtl');
if (localStorageLinkTtl === null) {
localStorage.setItem('localStorageLinkTtl', +new Date());
}
else if (+new Date() - localStorageLinkTtl > linkTtl) {
localStorage.clear();
localStorage.setItem('localStorageLinkTtl', +new Date());
}
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);
})
}
}
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";
@ -165,3 +217,13 @@ function CheckError(res) {
}
return Promise.resolve(res);
}
export function createGroup(groupName, groupManager, members) {
const group = {groupName, groupManager, members};
return axios.post(`${API_URL}/group.json`, group);
}
export function updateGroup(id, groupName, groupManager, members) {
const group = {groupId: id, groupName, groupManager, members};
return axios.put(`${API_URL}/group/${id}.json`, group);
}

View File

@ -561,3 +561,13 @@ export function getGroup(groupId) {
]
});
}
export function getInvitationLink(isGuest) {
return fakeResponse(isGuest
? "guest invitation link"
: "user invitation link");
}
export function getShortenedLink(link) {
return fakeResponse("SHORT LINK: " + link);
}

View File

@ -19,6 +19,7 @@ export default function setAuthorizationToken(token) {
});
}
else {
localStorage.clear();
delete axios.defaults.headers.common["Authorization"];
cookies.remove(AUTH_KEY, {
path: '/'

View File

@ -1802,7 +1802,7 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../../packages/asc-web-components":
version "1.0.62"
version "1.0.87"
dependencies:
moment "^2.24.0"
prop-types "^15.7.2"
@ -1810,7 +1810,6 @@ asap@~2.0.6:
react-autosize-textarea "^7.0.0"
react-avatar-edit "^0.8.3"
react-custom-scrollbars "^4.2.1"
react-datepicker "^2.8.0"
react-text-mask "^5.4.3"
react-toastify "^5.3.2"
react-virtualized-auto-sizer "^1.0.2"
@ -2931,6 +2930,13 @@ copy-descriptor@^0.1.0:
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
copy-to-clipboard@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
dependencies:
toggle-selection "^1.0.6"
copy-webpack-plugin@^5.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.4.tgz#c78126f604e24f194c6ec2f43a64e232b5d43655"
@ -3347,11 +3353,6 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.1.0.tgz#0d7e806c3cefe14a943532dbf968995ccfd46bd9"
integrity sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -8751,17 +8752,6 @@ react-custom-scrollbars@^4.2.1:
prop-types "^15.5.10"
raf "^3.1.0"
react-datepicker@^2.8.0:
version "2.9.6"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.9.6.tgz#26190c9f71692149d0d163398aa19e08626444b1"
integrity sha512-PLiVhyAr567gWuLMZwIH9WpTIZOZVLhEFyuUzSx3kmQdiikjrYpdNlxsfbbgaxRnee5y08KJZequaqRsNySXmw==
dependencies:
classnames "^2.2.6"
date-fns "^2.0.1"
prop-types "^15.7.2"
react-onclickoutside "^6.9.0"
react-popper "^1.3.4"
react-dev-utils@^9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.3.tgz#7607455587abb84599451460eb37cef0b684131a"
@ -8831,12 +8821,7 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-onclickoutside@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f"
integrity sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A==
react-popper@^1.3.3, react-popper@^1.3.4:
react-popper@^1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.4.tgz#f0cd3b0d30378e1f663b0d79bcc8614221652ced"
integrity sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA==
@ -10438,6 +10423,11 @@ to-space-case@^1.0.0:
dependencies:
to-no-case "^1.0.0"
toggle-selection@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI=
toidentifier@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"

View File

@ -8,6 +8,7 @@ using ASC.Core;
using ASC.Core.Tenants;
using ASC.Core.Users;
using ASC.MessagingSystem;
using ASC.People.Models;
using ASC.Web.Api.Models;
using ASC.Web.Api.Routing;
using Microsoft.AspNetCore.Mvc;
@ -50,16 +51,16 @@ namespace ASC.Employee.Core.Controllers
}
[Create]
public GroupWrapperFull AddGroup(Guid groupManager, string groupName, IEnumerable<Guid> members)
public GroupWrapperFull AddGroup(GroupModel groupModel)
{
SecurityContext.DemandPermissions(Tenant, Constants.Action_EditGroups, Constants.Action_AddRemoveUser);
var group = CoreContext.UserManager.SaveGroupInfo(Tenant, new GroupInfo { Name = groupName });
var group = CoreContext.UserManager.SaveGroupInfo(Tenant, new GroupInfo { Name = groupModel.GroupName });
TransferUserToDepartment(groupManager, @group, true);
if (members != null)
TransferUserToDepartment(groupModel.GroupManager, @group, true);
if (groupModel.Members != null)
{
foreach (var member in members)
foreach (var member in groupModel.Members)
{
TransferUserToDepartment(member, group, false);
}
@ -71,32 +72,32 @@ namespace ASC.Employee.Core.Controllers
}
[Update("{groupid}")]
public GroupWrapperFull UpdateGroup(Guid groupid, Guid groupManager, string groupName, IEnumerable<Guid> members)
public GroupWrapperFull UpdateGroup(Guid groupid, GroupModel groupModel)
{
SecurityContext.DemandPermissions(Tenant, Constants.Action_EditGroups, Constants.Action_AddRemoveUser);
var group = CoreContext.UserManager.GetGroups(Tenant.TenantId).SingleOrDefault(x => x.ID == groupid).NotFoundIfNull("group not found");
if (group.ID == Constants.LostGroupInfo.ID)
if (groupid == Constants.LostGroupInfo.ID)
{
throw new ItemNotFoundException("group not found");
}
group.Name = groupName ?? group.Name;
group.Name = groupModel.GroupName ?? group.Name;
CoreContext.UserManager.SaveGroupInfo(Tenant, group);
RemoveMembersFrom(groupid, CoreContext.UserManager.GetUsersByGroup(Tenant, groupid, EmployeeStatus.All).Select(u => u.ID).Where(id => !members.Contains(id)));
RemoveMembersFrom(new GroupModel { Groupid = groupid, Members = CoreContext.UserManager.GetUsersByGroup(Tenant, groupid, EmployeeStatus.All).Select(u => u.ID).Where(id => !groupModel.Members.Contains(id)) });
TransferUserToDepartment(groupManager, @group, true);
if (members != null)
TransferUserToDepartment(groupModel.GroupManager, @group, true);
if (groupModel.Members != null)
{
foreach (var member in members)
foreach (var member in groupModel.Members)
{
TransferUserToDepartment(member, group, false);
}
}
MessageService.Send(MessageAction.GroupUpdated, MessageTarget.Create(group.ID), group.Name);
MessageService.Send(MessageAction.GroupUpdated, MessageTarget.Create(groupid), group.Name);
return GetById(groupid);
return GetById(groupModel.Groupid);
}
[Delete("{groupid}")]
@ -122,36 +123,36 @@ namespace ASC.Employee.Core.Controllers
}
[Update("{groupid}/members/{newgroupid}")]
public GroupWrapperFull TransferMembersTo(Guid groupid, Guid newgroupid)
public GroupWrapperFull TransferMembersTo(TransferGroupMembersModel transferGroupMembersModel)
{
SecurityContext.DemandPermissions(Tenant, Constants.Action_EditGroups, Constants.Action_AddRemoveUser);
var oldgroup = GetGroupInfo(groupid);
var oldgroup = GetGroupInfo(transferGroupMembersModel.GroupId);
var newgroup = GetGroupInfo(newgroupid);
var newgroup = GetGroupInfo(transferGroupMembersModel.NewGroupId);
var users = CoreContext.UserManager.GetUsersByGroup(Tenant, oldgroup.ID);
foreach (var userInfo in users)
{
TransferUserToDepartment(userInfo.ID, newgroup, false);
}
return GetById(newgroupid);
return GetById(transferGroupMembersModel.NewGroupId);
}
[Create("{groupid}/members")]
public GroupWrapperFull SetMembersTo(Guid groupid, IEnumerable<Guid> members)
public GroupWrapperFull SetMembersTo(GroupModel groupModel)
{
RemoveMembersFrom(groupid, CoreContext.UserManager.GetUsersByGroup(Tenant, groupid).Select(x => x.ID));
AddMembersTo(groupid, members);
return GetById(groupid);
RemoveMembersFrom(new GroupModel { Groupid = groupModel.Groupid, Members = CoreContext.UserManager.GetUsersByGroup(Tenant, groupModel.Groupid).Select(x => x.ID) });
AddMembersTo(groupModel);
return GetById(groupModel.Groupid);
}
[Update("{groupid}/members")]
public GroupWrapperFull AddMembersTo(Guid groupid, IEnumerable<Guid> members)
public GroupWrapperFull AddMembersTo(GroupModel groupModel)
{
SecurityContext.DemandPermissions(Tenant, Constants.Action_EditGroups, Constants.Action_AddRemoveUser);
var group = GetGroupInfo(groupid);
var group = GetGroupInfo(groupModel.Groupid);
foreach (var userId in members)
foreach (var userId in groupModel.Members)
{
TransferUserToDepartment(userId, group, false);
}
@ -159,27 +160,27 @@ namespace ASC.Employee.Core.Controllers
}
[Update("{groupid}/manager")]
public GroupWrapperFull SetManager(Guid groupid, Guid userid)
public GroupWrapperFull SetManager(SetManagerModel setManagerModel)
{
var group = GetGroupInfo(groupid);
if (CoreContext.UserManager.UserExists(Tenant.TenantId, userid))
var group = GetGroupInfo(setManagerModel.GroupId);
if (CoreContext.UserManager.UserExists(Tenant.TenantId, setManagerModel.UserId))
{
CoreContext.UserManager.SetDepartmentManager(Tenant.TenantId, group.ID, userid);
CoreContext.UserManager.SetDepartmentManager(Tenant.TenantId, group.ID, setManagerModel.UserId);
}
else
{
throw new ItemNotFoundException("user not found");
}
return GetById(groupid);
return GetById(setManagerModel.GroupId);
}
[Delete("{groupid}/members")]
public GroupWrapperFull RemoveMembersFrom(Guid groupid, IEnumerable<Guid> members)
public GroupWrapperFull RemoveMembersFrom(GroupModel groupModel)
{
SecurityContext.DemandPermissions(Tenant, Constants.Action_EditGroups, Constants.Action_AddRemoveUser);
var group = GetGroupInfo(groupid);
var group = GetGroupInfo(groupModel.Groupid);
foreach (var userId in members)
foreach (var userId in groupModel.Members)
{
RemoveUserFromDepartment(userId, group);
}

View File

@ -315,7 +315,7 @@ namespace ASC.Employee.Core.Controllers
}
[Create]
[Authorize(AuthenticationSchemes = "confirm")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "LinkInvite,Administrators")]
public EmployeeWraperFull AddMember(MemberModel memberModel)
{
ApiContext.AuthByClaim();
@ -342,8 +342,8 @@ namespace ASC.Employee.Core.Controllers
? true
: ("female".Equals(memberModel.Sex, StringComparison.OrdinalIgnoreCase) ? (bool?)false : null);
user.BirthDate = memberModel.Birthday != null ? TenantUtil.DateTimeFromUtc(Convert.ToDateTime(memberModel.Birthday)) : (DateTime?)null;
user.WorkFromDate = memberModel.Worksfrom != null ? TenantUtil.DateTimeFromUtc(Convert.ToDateTime(memberModel.Worksfrom)) : DateTime.UtcNow.Date;
user.BirthDate = memberModel.Birthday != null && memberModel.Birthday != DateTime.MinValue ? TenantUtil.DateTimeFromUtc(Convert.ToDateTime(memberModel.Birthday)) : (DateTime?)null;
user.WorkFromDate = memberModel.Worksfrom != null && memberModel.Worksfrom != DateTime.MinValue ? TenantUtil.DateTimeFromUtc(Convert.ToDateTime(memberModel.Worksfrom)) : DateTime.UtcNow.Date;
UpdateContacts(memberModel.Contacts, user);
@ -588,7 +588,7 @@ namespace ASC.Employee.Core.Controllers
return new ThumbnailsDataWrapper(Tenant, user.ID);
}
public FormFile Base64ToImage(string base64String, string fileName)
{
byte[] imageBytes = Convert.FromBase64String(base64String);
@ -617,7 +617,7 @@ namespace ASC.Employee.Core.Controllers
SecurityContext.DemandPermissions(Tenant, new UserSecurityProvider(userId), Constants.Action_EditUser);
var userPhoto = Base64ToImage(model.base64CroppedImage, "userPhoto_"+ userId.ToString());
var userPhoto = Base64ToImage(model.base64CroppedImage, "userPhoto_" + userId.ToString());
var defaultUserPhoto = Base64ToImage(model.base64DefaultImage, "defaultPhoto" + userId.ToString());
if (userPhoto.Length > SetupInfo.MaxImageUploadSize)
@ -869,8 +869,11 @@ namespace ASC.Employee.Core.Controllers
}
[Update("{userid}/password")]
[Authorize(AuthenticationSchemes = "confirm", Roles = "PasswordChange,EmailChange,Administrators")]
public EmployeeWraperFull ChangeUserPassword(Guid userid, MemberModel memberModel)
{
ApiContext.AuthByClaim();
SecurityContext.DemandPermissions(Tenant, new UserSecurityProvider(userid), Constants.Action_EditUser);
var user = CoreContext.UserManager.GetUsers(Tenant.TenantId, userid);

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
namespace ASC.People.Models
{
public class GroupModel
{
public Guid Groupid { get; set; }
public Guid GroupManager { get; set; }
public string GroupName { get; set; }
public IEnumerable<Guid> Members { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace ASC.People.Models
{
public class SetManagerModel
{
public Guid GroupId { get; set; }
public Guid UserId { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace ASC.People.Models
{
public class TransferGroupMembersModel
{
public Guid GroupId { get; set; }
public Guid NewGroupId { get; set; }
}
}

View File

@ -24,8 +24,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0-preview8.19405.4" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0-preview8-19413-06" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" />
</ItemGroup>
<ItemGroup>

View File

@ -137,6 +137,13 @@ namespace ASC.Api.Settings
return QuotaWrapper.GetCurrent(Tenant);
}
[AllowAnonymous]
[Read("cultures")]
public List<CultureInfo> GetSupportedCultures()
{
return SetupInfo.EnabledCultures;
}
[Read("recalculatequota")]
public void RecalculateQuota()
{

View File

@ -4,7 +4,7 @@ import { Loader } from "asc-web-components";
import StudioLayout from "./components/Layout/index";
import Login from "./components/pages/Login";
import { PrivateRoute } from "./helpers/privateRoute";
import { PublicRoute } from "./helpers/publicRoute";
import PublicRoute from "./helpers/publicRoute";
import { Error404 } from "./components/pages/Error";
const Home = lazy(() => import("./components/pages/Home"));
@ -19,8 +19,8 @@ const App = () => {
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<Switch>
<PublicRoute exact path={["/login","/login/:error"]} component={Login} />
<PublicRoute path="/confirm" component={Confirm} />
<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="/about" component={About} />
<PrivateRoute component={Error404} />

View File

@ -0,0 +1,57 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
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
format: function (value, format) {
if (format === 'lowercase') return value.toLowerCase();
return value;
}
},
react: {
useSuspense: true
},
backend: {
loadPath: `/locales/About/{{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',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
format: function (value, format) {
if (format === 'lowercase') return value.toLowerCase();
return value;
}
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -1,22 +1,160 @@
import React from 'react';
import React from "react";
import { PageLayout, Text, Link } from "asc-web-components";
import { useTranslation } from "react-i18next";
import i18n from "./i18n";
import version from "../../../../package.json";
import styled from "styled-components";
const About = () => (
<div>
<h1>Hello, world!</h1>
<p>Welcome to your new single-page application, built with:</p>
<ul>
<li><a href='https://get.asp.net/'>ASP.NET Core</a> and <a href='https://msdn.microsoft.com/en-us/library/67ef8sbd.aspx'>C#</a> for cross-platform server-side code</li>
<li><a href='https://facebook.github.io/react/'>React</a> and <a href='https://redux.js.org/'>Redux</a> for client-side code</li>
<li><a href='http://getbootstrap.com/'>Bootstrap</a> for layout and styling</li>
</ul>
<p>To help you get started, we've also set up:</p>
<ul>
<li><strong>Client-side navigation</strong>. For example, click <em>Counter</em> then <em>Back</em> to return here.</li>
<li><strong>Development server integration</strong>. In development mode, the development server from <code>create-react-app</code> runs in the background automatically, so your client-side resources are dynamically built on demand and the page refreshes when you modify any file.</li>
<li><strong>Efficient production builds</strong>. In production mode, development-time features are disabled, and your <code>dotnet publish</code> configuration produces minified, efficiently bundled JavaScript files.</li>
</ul>
<p>The <code>ClientApp</code> subdirectory is a standard React application based on the <code>create-react-app</code> template. If you open a command prompt in that directory, you can run <code>npm</code> commands such as <code>npm test</code> or <code>npm install</code>.</p>
</div>
);
const BodyStyle = styled.div`
margin-top: 24px;
.text_p {
text-align: center;
}
.text_span {
text-align: center;
}
.logo-img {
text-align: center;
max-width: 216px;
max-height: 35px;
}
.copyright-line {
padding-bottom: 15px;
text-align: center;
:before {
background-color: #e1e1e1;
content: "";
height: 2px;
margin-top: 9px;
width: 36%;
float: right;
}
:after {
background-color: #e1e1e1;
content: "";
height: 2px;
margin-top: 9px;
width: 36%;
float: left;
}
}
`;
const Style = styled.div`
margin-top: 8px;
text-align: center;
`;
const VersionStyle = styled.div`
padding: 8px 0px 20px 0px;
`;
const Body = () => {
const { t } = useTranslation("translation", { i18n });
return (
<BodyStyle>
<p style={{ textAlign: "center", margin: "0px" }}>
<img
className="logo-img"
src="images/dark_general.png"
width="320"
height="181"
alt="Logo"
></img>
</p>
<VersionStyle>
<Text.Body className="text_p" fontSize={14} color="#A3A9AE">
{`${t("AboutCompanyVersion")}: ${version.version}`}
</Text.Body>
</VersionStyle>
<Text.Body className="copyright-line" fontSize={14}>
{t("AboutCompanyLicensor")}
</Text.Body>
<Text.Body className="text_p" fontSize={16} isBold={true}>
Ascensio System SIA
</Text.Body>
<Style>
<Text.Body className="text_p" fontSize={12}>
<Text.Body
className="text_span"
fontSize={12}
as="span"
color="#A3A9AE"
>
{t("AboutCompanyAddressTitle")}:{" "}
</Text.Body>
20A-12 Ernesta Birznieka-Upisha street, Riga, Latvia, EU, LV-1050
</Text.Body>
<Text.Body
fontSize={12}
className="text_span"
as="span"
color="#A3A9AE"
>
{t("AboutCompanyEmailTitle")}:{" "}
<Link href="mailto:support@onlyoffice.com" fontSize={12}>
support@onlyoffice.com
</Link>
</Text.Body>
<div style={{ marginTop: "4px" }}>
<Text.Body className="text_p" fontSize={12}>
<Text.Body
fontSize={12}
className="text_span"
as="span"
color="#A3A9AE"
>
{t("AboutCompanyTelTitle")}:{" "}
</Text.Body>
+371 660-16425
</Text.Body>
</div>
<Link href="http://www.onlyoffice.com" fontSize={12}>
www.onlyoffice.com
</Link>
<div style={{ marginTop: "20px" }}>
<Text.Body className="text_p" fontSize={12}>
{t("LicensedUnder")}:{" "}
<Link
href="https://www.gnu.org/licenses/gpl-3.0.html"
isHovered={true}
fontSize={12}
>
GNU GPL v.3
</Link>{" "}
</Text.Body>
<Text.Body className="text_p" fontSize={12}>
{t("SourceCode")}:{" "}
<Link
href="https://github.com/ONLYOFFICE/CommunityServer"
isHovered={true}
fontSize={12}
>
GitHub
</Link>
</Text.Body>
</div>
</Style>
</BodyStyle>
);
};
const About = () => {
return <PageLayout sectionBodyContent={<Body />} />;
};
export default About;

View File

@ -0,0 +1,15 @@
{
"AboutCompanyTitle": "About this program",
"AboutCompanyVersion": "Version",
"AboutCompanyLicensor": "Copyright",
"AboutCompanyAddressTitle": "address",
"AboutCompanyEmailTitle": "email",
"AboutCompanyTelTitle": "tel.",
"LicensedUnder": "This software is licensed under", "_comment": "{0}GNU GPL v.3{1}",
"SourceCode": "Source code is available on", "_comment": "{0}GNU GPL v.3{1}","_comment":"SYNTAX ERROR"
}

View File

@ -1,330 +1,48 @@
import React from 'react';
import { withRouter } from "react-router";
import { I18nextProvider, withTranslation } from 'react-i18next';
import i18n from './i18n';
import { Button, TextInput, PageLayout, Text, PasswordInput, FieldContainer, toastr, Loader } from 'asc-web-components';
import styled from 'styled-components';
import { welcomePageTitle } from './../../../helpers/customNames';
import { Collapse } from 'reactstrap';
import { connect } from 'react-redux';
import { getPasswordSettings, createConfirmUser } from '../../../store/auth/actions';
import React, { Suspense, lazy } from "react";
import { Switch, Redirect, Route } from "react-router-dom";
import { Loader } from "asc-web-components";
import PublicRoute from "../../../helpers/publicRoute";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
const inputWidth = '400px';
const CreateUserForm = lazy(() => import("./sub-components/createUser"));
const ChangePasswordForm = lazy(() => import("./sub-components/changePassword"));
const ActivateEmailForm = lazy(() => import("./sub-components/activateEmail"));
const ChangePhoneForm = lazy(() => import("./sub-components/changePhone"));
const ConfirmContainer = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-left: 200px;
const Confirm = ({ match }) => {
//console.log("Confirm render");
@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;
}
`;
const emailInputName = 'email';
const passwordInputName = 'password';
const emailRegex = '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$';
const validationEmail = new RegExp(emailRegex);
class Confirm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: '',
emailValid: true,
firstName: '',
firstNameValid: true,
lastName: '',
lastNameValid: true,
password: '',
passwordValid: true,
errorText: '',
isLoading: false,
};
}
onSubmit = (e) => {
const { location, history, createConfirmUser } = this.props;
const queryString = location.search.slice(1);
const queryParams = queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
const isVisitor = parseInt(linkParams.emplType) === 2;
this.state.errorText && 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 (!validationEmail.test(this.state.email.trim())) {
hasError = true;
this.setState({ emailValid: !hasError });
}
if (!this.state.passwordValid) {
hasError = true;
this.setState({ passwordValid: !hasError });
}
if (hasError)
return false;
this.setState({ isLoading: true });
const loginData = {
userName: this.state.email,
password: this.state.password
}
const registerData = {
firstname: this.state.firstName,
lastname: this.state.lastName,
email: this.state.email,
isVisitor: isVisitor
};
createConfirmUser(registerData, loginData, queryString)
.then(() => history.push('/'))
.catch(e => {
console.error("confirm error", e);
this.setState({ errorText: e.message });
this.setState({ isLoading: false });
});
};
onKeyPress = (target) => {
if (target.code === "Enter") {
this.onSubmit();
}
};
onCopyToClipboard = () => toastr.success(this.props.t('EmailAndPasswordCopiedToClipboard'));
validatePassword = (value) => this.setState({ passwordValid: value });
componentDidMount() {
const { getPasswordSettings, history, location } = this.props;
const queryString = location.search.slice(1);
getPasswordSettings(queryString)
.then(
function () {
console.log("get settings success");
}
)
.catch(e => {
console.error("get settings error", e);
history.push(`/login/${e}`);
});
window.addEventListener('keydown', this.onKeyPress);
window.addEventListener('keyup', this.onKeyPress);
}
componentWillUnmount() {
window.removeEventListener('keydown', this.onKeyPress);
window.removeEventListener('keyup', this.onKeyPress);
}
render() {
const { settings, isLoaded, t } = this.props;
return (
!isLoaded
? (
<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 className='confirm-row'>
<div className='full-width'>
<FieldContainer isVertical={true} className=''>
<TextInput
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={event => {
this.setState({ firstName: event.target.value });
!this.state.firstNameValid && this.setState({ firstNameValid: event.target.value });
this.state.errorText && this.setState({ errorText: "" });
}}
onKeyDown={event => this.onKeyPress(event.target)}
/>
</FieldContainer>
<FieldContainer isVertical={true} className=''>
<TextInput
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={event => {
this.setState({ lastName: event.target.value });
!this.state.lastNameValid && this.setState({ lastNameValid: true });
this.state.errorText && this.setState({ errorText: "" });;
}}
onKeyDown={event => this.onKeyPress(event.target)}
/>
</FieldContainer>
<FieldContainer isVertical={true} className=''>
<TextInput
id='email'
name={emailInputName}
value={this.state.email}
placeholder={t('Email')}
size='huge'
scale={true}
tabIndex={3}
autoComplete='email'
isDisabled={this.state.isLoading}
hasError={!this.state.emailValid}
onChange={event => {
this.setState({ email: event.target.value });
!this.state.emailValid && this.setState({ emailValid: true });
this.state.errorText && this.setState({ errorText: "" });;
}}
onKeyDown={event => this.onKeyPress(event.target)}
/>
</FieldContainer>
</div>
<FieldContainer isVertical={true} className=''>
<PasswordInput
inputName={passwordInputName}
emailInputName={emailInputName}
inputValue={this.state.password}
placeholder={t('InvitePassword')}
size='huge'
scale={true}
tabIndex={4}
maxLength={30}
inputWidth={inputWidth}
hasError={!this.state.passwordValid && !this.state.password.trim()}
onChange={event => {
this.setState({ password: event.target.value });
!this.state.passwordValid && this.setState({ passwordValid: true });
this.state.errorText && this.setState({ errorText: "" });
this.onKeyPress(event.target);
}}
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}
/>
</FieldContainer>
<Button
primary
size='big'
label={t('LoginRegistryButton')}
tabIndex={5}
isDisabled={this.state.isLoading}
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>
)
);
}
}
const ConfirmWrapper = withTranslation()(Confirm);
const ConfirmWithTrans = (props) => <I18nextProvider i18n={i18n}><ConfirmWrapper {...props} /></I18nextProvider>;
ConfirmWithTrans.propTypes = {
return (
<I18nextProvider i18n={i18n}>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<Switch>
<PublicRoute
path={`${match.path}/type=LinkInvite`}
component={CreateUserForm}
/>
<Route
exact
path={`${match.path}/type=EmailActivation`}
component={ActivateEmailForm}
/>
<Route
exact
path={`${match.path}/type=PasswordChange`}
component={ChangePasswordForm}
/>
<Route
exact
path={`${match.path}/type=PhoneActivation`}
component={ChangePhoneForm}
/>
<Redirect to={{ pathname: "/" }} />
</Switch>
</Suspense>
</I18nextProvider>
);
};
const ConfirmForm = (props) => (<PageLayout sectionBodyContent={<ConfirmWithTrans {...props} />} />);
function mapStateToProps(state) {
return {
isLoaded: state.auth.isLoaded,
settings: state.auth.password
};
}
export default connect(mapStateToProps, { getPasswordSettings, createConfirmUser })(withRouter(ConfirmForm));
export default Confirm;

View File

@ -13,6 +13,9 @@
"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",
"PasswordCustomMode": "Password",
"ImportContactsOkButton": "OK",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"

View File

@ -0,0 +1,50 @@
import React from 'react';
import { withRouter } from "react-router";
import { withTranslation } from 'react-i18next';
import { PageLayout, Loader } from 'asc-web-components';
import { connect } from 'react-redux';
import { logout, validateChangingEmail } 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, validateChangingEmail } = this.props;
const queryParams = this.state.queryString.split('&');
const arrayOfQueryParams = queryParams.map(queryParam => queryParam.split('='));
const linkParams = Object.fromEntries(arrayOfQueryParams);
logout();
validateChangingEmail(linkParams)
.then((res) => {
const email = decodeURIComponent(res.data.response.email);
history.push(`/login/confirmed-email=${email}`);
});
}
render() {
console.log('Activate email render');
return (
<Loader className="pageLoader" type="rombs" size={40} />
);
}
}
ActivateEmail.propTypes = {
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
const ActivateEmailForm = (props) => (<PageLayout sectionBodyContent={<ActivateEmail {...props} />} />);
export default connect(null, { logout, validateChangingEmail })(withRouter(withTranslation()(ActivateEmailForm)));

View File

@ -0,0 +1,179 @@
import React, { useState, useEffect, useCallback } 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, TextInput, PageLayout, Text } from "asc-web-components";
import { useTranslation } from "react-i18next";
import i18n from "../i18n";
import { welcomePageTitle } from "../../../../helpers/customNames";
import { setNewPassword } from "../../../../../src/store/auth/actions";
const BodyStyle = styled.div`
margin-top: 70px;
p {
margin-bottom: 5px;
}
.password-row {
margin: 23px 0 0;
.password-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;
}
}
}
`;
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, setNewPassword } = props;
const { params } = match;
const { t } = useTranslation("translation", { i18n });
const onSubmit = useCallback(
e => {
errorText && setErrorText("");
let hasError = false;
if (!password.trim()) {
hasError = true;
setPasswordValid(!hasError);
}
if (hasError) return false;
let newPassword = {
password: password
};
setIsLoading(true);
console.log("changePassword onSubmit", match, location, history);
setNewPassword(newPassword)
.then(function() {
console.log("UPDATE PASSWORD");
history.push('/');
})
.catch(e => {
history.push('/');
console.log("ERROR UPDATE PASSWORD", e);
});
},
[errorText, history, location, setNewPassword, match, password]
);
const onKeyPress = useCallback(
target => {
if (target.code === "Enter" || target.code === "NumpadEnter") {
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.error]);
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
/>
<CardTitle className="card-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
</CardTitle>
</Card>
<Text.Body fontSize={14}>{t("PassworResetTitle")}</Text.Body>
<TextInput
id="password"
name="password"
type="password"
size="huge"
scale={true}
isAutoFocussed={true}
tabIndex={1}
autocomple="current-password"
placeholder={t("PasswordCustomMode")}
onChange={event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
onKeyPress(event.target);
}}
value={password}
hasError={!passwordValid}
isDisabled={isLoading}
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,
history: PropTypes.object.isRequired
};
ChangePasswordForm.defaultProps = {
password: ""
};
export default connect(null, { setNewPassword })(withRouter(withTranslation()(ChangePasswordForm)));

View File

@ -0,0 +1,7 @@
import React from 'react';
const changePhoneForm = (props) => {
return (<span>{props.location.pathname}</span>);
}
export default changePhoneForm;

View File

@ -0,0 +1,342 @@
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 { getPasswordSettings, createConfirmUser } 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;
}
`;
const emailInputName = 'email';
const passwordInputName = 'password';
const emailRegex = '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$';
const validationEmail = new RegExp(emailRegex);
class Confirm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: '',
emailValid: true,
firstName: '',
firstNameValid: true,
lastName: '',
lastNameValid: true,
password: '',
passwordValid: true,
errorText: '',
isLoading: false,
passwordEmpty: false,
queryString: `type=LinkInvite&${props.location.search.slice(1)}`
};
}
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;
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 (!validationEmail.test(this.state.email.trim())) {
hasError = true;
this.setState({ emailValid: !hasError });
}
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;
}
const loginData = {
userName: this.state.email,
password: this.state.password
}
const registerData = {
firstname: this.state.firstName,
lastname: this.state.lastName,
email: this.state.email,
isVisitor: isVisitor
};
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 });
});
});
};
onKeyPress = (event) => {
if (event.key === "Enter") {
this.onSubmit();
}
};
onCopyToClipboard = () => toastr.success(this.props.t('EmailAndPasswordCopiedToClipboard'));
validatePassword = (value) => this.setState({ passwordValid: value });
componentDidMount() {
const { getPasswordSettings, history } = this.props;
getPasswordSettings(this.state.queryString)
.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: "" });;
}
onChangeEmail = event => {
this.setState({ email: event.target.value });
!this.state.emailValid && this.setState({ emailValid: 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('Confirm 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'
id='email'
name={emailInputName}
value={this.state.email}
placeholder={t('Email')}
size='huge'
scale={true}
tabIndex={3}
autoComplete='email'
isDisabled={this.state.isLoading}
hasError={!this.state.emailValid}
onChange={this.onChangeEmail}
onKeyDown={this.onKeyPress}
/>
</div>
<PasswordInput
className='confirm-row'
id='password'
inputName={passwordInputName}
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 = {
getPasswordSettings: PropTypes.func.isRequired,
createConfirmUser: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
const CreateUserForm = (props) => (<PageLayout sectionBodyContent={<Confirm {...props} />} />);
function mapStateToProps(state) {
return {
isConfirmLoaded: state.auth.isConfirmLoaded,
settings: state.auth.password
};
}
export default connect(mapStateToProps, { getPasswordSettings, createConfirmUser })(withRouter(withTranslation()(CreateUserForm)));

View File

@ -2,7 +2,7 @@ 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 } from 'asc-web-components';
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';
@ -38,14 +38,14 @@ const mdOptions = { size: 6, offset: 3 };
const Form = props => {
const { t } = useTranslation('translation', { i18n });
const [identifier, setIdentifier] = useState('');
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 { login, match, location, history } = props;
const { params } = match;
const onSubmit = useCallback((e) => {
//e.preventDefault();
@ -87,8 +87,8 @@ const Form = props => {
});
}, [errorText, history, identifier, location, login, match, password]);
const onKeyPress = useCallback((target) => {
if (target.code === "Enter") {
const onKeyPress = useCallback((event) => {
if (event.key === "Enter") {
onSubmit();
}
}, [onSubmit]);
@ -102,7 +102,21 @@ const Form = props => {
window.removeEventListener('keydown', onKeyPress);
window.removeEventListener('keyup', onKeyPress);
};
}, [onKeyPress, params.error]);
}, [onKeyPress, params]);
const onChangePassword = event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
}
const onChangeLogin = event => {
setIdentifier(event.target.value);
!identifierValid && setIdentifierValid(true);
errorText && setErrorText("");
}
// console.log('Login render');
return (
<FormContainer>
@ -127,13 +141,9 @@ const Form = props => {
isAutoFocussed={true}
tabIndex={1}
isDisabled={isLoading}
autocomple="username"
onChange={event => {
setIdentifier(event.target.value);
!identifierValid && setIdentifierValid(true);
errorText && setErrorText("");
}}
onKeyDown={event => onKeyPress(event.target)} />
autoComplete="username"
onChange={onChangeLogin}
onKeyDown={onKeyPress} />
</Col>
</Row>
<Row className="login-row">
@ -149,14 +159,9 @@ const Form = props => {
scale={true}
tabIndex={2}
isDisabled={isLoading}
autocomple="current-password"
onChange={event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
errorText && setErrorText("");
onKeyPress(event.target);
}}
onKeyDown={event => onKeyPress(event.target)} />
autoComplete="current-password"
onChange={onChangePassword}
onKeyDown={onKeyPress} />
</Col>
</Row>
<Row className="login-row">
@ -171,6 +176,12 @@ const Form = props => {
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}>

View File

@ -3,6 +3,8 @@
"LoginButton": "Sign In",
"Password": "Password",
"RegistrationEmailWatermark": "Your registration email",
"MessageEmailConfirmed": "Your email was activated successfully.",
"MessageAuthorize": "Please authorize yourself.",
"CustomWelcomePageTitle": "{{welcomePageTitle}}"
}

View File

@ -21,4 +21,5 @@ export const PublicRoute = ({ component: Component, ...rest }) => {
}
/>
)
};
};
export default PublicRoute;

View File

@ -13,8 +13,13 @@ import { getUserInfo } from './store/auth/actions';
var token = (new Cookies()).get(AUTH_KEY);
if (token) {
setAuthorizationToken(token);
store.dispatch(getUserInfo);
if (!window.location.pathname.includes("confirm/type=EmailActivation")) {
setAuthorizationToken(token);
store.dispatch(getUserInfo);
}
else {
setAuthorizationToken();
}
}
ReactDOM.render(

View File

@ -1,6 +1,17 @@
{
"pages": {
"About": {
"Resource": [
"AboutCompanyTitle",
"AboutCompanyVersion",
"AboutCompanyLicensor",
"AboutCompanyAddressTitle",
"AboutCompanyEmailTitle",
"AboutCompanyTelTitle"
],
"UserControlsCommonResource": [
"LicensedUnder"
]
},
"Home": {
},
@ -9,6 +20,8 @@
"LoadingProcessing",
"Password",
"RegistrationEmailWatermark",
"MessageEmailConfirmed",
"MessageAuthorize",
"LoginButton"
]
},

View File

@ -8,6 +8,8 @@ export const SET_SETTINGS = 'SET_SETTINGS';
export const SET_IS_LOADED = 'SET_IS_LOADED';
export const LOGOUT = 'LOGOUT';
export const SET_PASSWORD_SETTINGS = 'SET_PASSWORD_SETTINGS';
export const SET_IS_CONFIRM_LOADED = 'SET_IS_CONFIRM_LOADED';
export const SET_NEW_PASSWORD = 'SET_NEW_PASSWORD';
export function setCurrentUser(user) {
return {
@ -37,6 +39,12 @@ export function setIsLoaded(isLoaded) {
};
};
export function setIsConfirmLoaded(isConfirmLoaded) {
return {
type: SET_IS_CONFIRM_LOADED,
isConfirmLoaded
};
};
export function setLogout() {
return {
@ -51,6 +59,13 @@ export function setPasswordSettings(password) {
};
};
export function setNewPasswordSettings(password) {
return {
type: SET_NEW_PASSWORD,
password
};
};
export function getUserInfo(dispatch) {
return api.getUser()
@ -85,7 +100,7 @@ export function getPasswordSettings(token) {
return dispatch => {
return api.getPasswordSettings(token)
.then((res) => dispatch(setPasswordSettings(res.data.response)))
.then(() => dispatch(setIsLoaded(true)));
.then(() => dispatch(setIsConfirmLoaded(true)));
}
};
@ -109,9 +124,25 @@ export function createConfirmUser(registerData, loginData, key) {
};
};
export function validateChangingEmail(data, key) {
return dispatch => {
return api.validateChangingEmail(data, key);
}
};
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 setNewPassword(res) {
return dispatch => {
return api.setNewPasswordSettings(res)
.then(res => {
//checkResponseError(res);
dispatch(setNewPasswordSettings(res.data.response));
})
}
}

View File

@ -1,10 +1,11 @@
import { SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS } from './actions';
import { SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS, SET_IS_CONFIRM_LOADED, SET_NEW_PASSWORD } from './actions';
import isEmpty from 'lodash/isEmpty';
import config from "../../../package.json";
const initialState = {
isAuthenticated: false,
isLoaded: false,
isConfirmLoaded: false,
user: {},
modules: [],
settings: {
@ -51,6 +52,14 @@ const authReducer = (state = initialState, action) => {
return Object.assign({}, state, {
isLoaded: action.isLoaded
});
case SET_IS_CONFIRM_LOADED:
return Object.assign({}, state, {
isConfirmLoaded: action.isConfirmLoaded
});
case SET_NEW_PASSWORD:
return Object.assign({}, state, {
password: action.password
});
case LOGOUT:
return initialState;
default:

View File

@ -47,3 +47,14 @@ export function createUser(data, key) {
? fakeApi.createUser()
: axios.post(`${API_URL}/people`, data, { headers: { 'confirm' : key } });
}
export function validateChangingEmail(data, key) {
return fakeApi.validateChangingEmail(data, key); ;
}
export function setNewPasswordSettings(data, key) {
const IS_FAKE = true;
return IS_FAKE
? fakeApi.setNewPasswordSettings()
: axios.post(`${API_URL}/people`, data, { headers: { 'confirm' : key } });
}

View File

@ -119,4 +119,23 @@ export function createUser() {
"id": "00000000-0000-0000-0000-000000000000"
};
return fakeResponse(data);
}
}
export function validateChangingEmail(payload, key) {
const data = {
"email": payload.email
};
return fakeResponse(data);
}
export function setNewPasswordSettings() {
const data = {
//minLength: 12,
//upperCase: true,
//digits: true,
//specSymbols: true
};
return fakeResponse(data);
}

View File

@ -19,6 +19,7 @@ export default function setAuthorizationToken(token) {
});
}
else {
localStorage.clear();
delete axios.defaults.headers.common["Authorization"];
cookies.remove(AUTH_KEY);
}

View File

@ -1802,7 +1802,7 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../packages/asc-web-components":
version "1.0.82"
version "1.0.86"
dependencies:
moment "^2.24.0"
prop-types "^15.7.2"
@ -1811,7 +1811,6 @@ asap@~2.0.6:
react-avatar-edit "^0.8.3"
react-avatar-editor "^11.0.7"
react-custom-scrollbars "^4.2.1"
react-datepicker "^2.8.0"
react-dropzone "^10.1.8"
react-text-mask "^5.4.3"
react-toastify "^5.3.2"
@ -3356,11 +3355,6 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.1.0.tgz#0d7e806c3cefe14a943532dbf968995ccfd46bd9"
integrity sha512-eKeLk3sLCnxB/0PN4t1+zqDtSs4jb4mXRSTZ2okmx/myfWyDqeO4r5nnmA5LClJiCwpuTMeK2v5UQPuE4uMaxA==
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@ -8774,17 +8768,6 @@ react-custom-scrollbars@^4.2.1:
prop-types "^15.5.10"
raf "^3.1.0"
react-datepicker@^2.8.0:
version "2.9.6"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-2.9.6.tgz#26190c9f71692149d0d163398aa19e08626444b1"
integrity sha512-PLiVhyAr567gWuLMZwIH9WpTIZOZVLhEFyuUzSx3kmQdiikjrYpdNlxsfbbgaxRnee5y08KJZequaqRsNySXmw==
dependencies:
classnames "^2.2.6"
date-fns "^2.0.1"
prop-types "^15.7.2"
react-onclickoutside "^6.9.0"
react-popper "^1.3.4"
react-dev-utils@^9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-9.0.3.tgz#7607455587abb84599451460eb37cef0b684131a"
@ -8858,12 +8841,7 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-onclickoutside@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.9.0.tgz#a54bc317ae8cf6131a5d78acea55a11067f37a1f"
integrity sha512-8ltIY3bC7oGhj2nPAvWOGi+xGFybPNhJM0V1H8hY/whNcXgmDeaeoCMPPd8VatrpTsUWjb/vGzrmu6SrXVty3A==
react-popper@^1.3.3, react-popper@^1.3.4:
react-popper@^1.3.3:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.4.tgz#f0cd3b0d30378e1f663b0d79bcc8614221652ced"
integrity sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA==

View File

@ -4,7 +4,5 @@ It can also be an scss file, however,
you have to go to `webpack.config.js` file
and enable the options in there
*/
@import '../node_modules/bootstrap/dist/css/bootstrap.css';
@import '../node_modules/react-toastify/dist/ReactToastify.min.css';
@import "../node_modules/react-datepicker/dist/react-datepicker.css";
@import '../node_modules/react-toastify/dist/ReactToastify.min.css';

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-components",
"version": "1.0.82",
"version": "1.0.94",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",
@ -105,7 +105,6 @@
"react-avatar-edit": "^0.8.3",
"react-avatar-editor": "^11.0.7",
"react-custom-scrollbars": "^4.2.1",
"react-datepicker": "^2.8.0",
"react-dropzone": "^10.1.8",
"react-lifecycles-compat": "^3.0.4",
"react-text-mask": "^5.4.3",

View File

@ -77,6 +77,8 @@ const groups = [
}
];
const sizes = ["compact", "full"];
storiesOf("Components|AdvancedSelector", module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
@ -101,7 +103,7 @@ storiesOf("Components|AdvancedSelector", module)
>
{({ value, set }) => (
<AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
size={select("size", sizes, "full")}
placeholder={text("placeholder", "Search users")}
onSearchChanged={value => {
action("onSearchChanged")(value);
@ -131,6 +133,8 @@ storiesOf("Components|AdvancedSelector", module)
})
);
}}
allowCreation={boolean("allowCreation", false)}
onAddNewClick={() => action("onSelect") }
/>
)}
</ArrayValue>
@ -165,7 +169,7 @@ storiesOf("Components|AdvancedSelector", module)
>
{({ value, set }) => (
<AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
size={select("size", sizes, "full")}
isDropDown={true}
isOpen={isOpen}
placeholder={text("placeholder", "Search users")}
@ -199,6 +203,8 @@ storiesOf("Components|AdvancedSelector", module)
})
);
}}
allowCreation={boolean("allowCreation", false)}
onAddNewClick={() => action("onSelect") }
/>
)}
</ArrayValue>

View File

@ -1,5 +1,5 @@
import React from "react";
import styled from "styled-components";
import styled, { css } from "styled-components";
import PropTypes from "prop-types";
import SearchInput from "../search-input";
import CustomScrollbarsVirtualList from "../scrollbar/custom-scrollbars-virtual-list";
@ -7,7 +7,9 @@ import { FixedSizeList } from "react-window";
import Link from "../link";
import Checkbox from "../checkbox";
import Button from "../button";
import { Icons } from "../icons";
import ComboBox from "../combobox";
import { Text } from "../text";
import findIndex from "lodash/findIndex";
import filter from "lodash/filter";
import isEqual from "lodash/isEqual";
@ -38,6 +40,9 @@ const Container = ({
isDropDown,
containerWidth,
containerHeight,
allowCreation,
onAddNewClick,
allowAnyClickClose,
...props
}) => <div {...props} />;
/* eslint-enable react/prop-types */
@ -47,44 +52,126 @@ const StyledContainer = styled(Container)`
display: flex;
flex-direction: column;
${props => (props.containerWidth ? `width: ${props.containerWidth}px;` : "")}
${props => (props.containerWidth ? `width: ${props.containerWidth};` : "")}
${props =>
props.containerHeight
? `height: ${props.containerHeight}px;`
? `height: ${props.containerHeight};`
: ""}
.data_container {
margin: 16px 16px 0 16px;
margin: 16px 16px -5px 16px;
.head_container {
display: flex;
margin-bottom: ${props => props.isDropDown ? 8 : 16}px;
.options_searcher {
display: inline-block;
width: 100%;
${props => props.isDropDown && props.size === "full" && css`
margin-right: ${props =>
props.allowCreation ?
8 : 16
}px;
`}
/*${props =>
props.allowCreation ?
css`
width: 272px;
margin-right: 8px;
`
: css`width: ${props => props.isDropDown ? '313px' : '100%'};`
}*/
}
.add_new_btn {
${props =>
props.allowCreation &&
css`
display: inline-block;
vertical-align: top;
height: 32px;
width: 36px;
margin-right: 16px;
line-height: 18px;
`}
}
.options_searcher {
margin-bottom: 12px;
}
.options_group_selector {
margin-bottom: 12px;
}
.option_select_all_checkbox {
margin-bottom: 12px;
/*margin-left: 8px;*/
.data_column_one {
${props =>
props.isDropDown && props.groups && props.groups.length > 0
? css`
width: 50%;
display: inline-block;
`
: ""}
.options_list {
margin-top: 4px;
margin-left: -8px;
.option {
line-height: 32px;
padding-left: ${props => props.isMultiSelect ? 8 : 0}px;
cursor: pointer;
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #f8f9f9;
}
}
}
}
.options_list {
.option {
line-height: 32px;
cursor: pointer;
.data_column_two {
${props =>
props.isDropDown && props.groups && props.groups.length > 0
? css`
width: 50%;
display: inline-block;
border-left: 1px solid #eceef1;
`
: ""}
.option_checkbox {
/*margin-left: 8px;*/
}
.group_header {
font-weight: 600;
padding-left: 16px;
padding-bottom: 14px;
}
.option_link {
.group_list {
margin-left: 8px;
.option {
line-height: 32px;
padding-left: 8px;
}
cursor: pointer;
/*&:hover {
background-color: #eceef1;
}*/
.option_checkbox {
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
background-color: #eceef1;
}
}
}
}
}
@ -121,7 +208,12 @@ class AdvancedSelector extends React.Component {
handleClick = e => {
if (
this.props.isOpen &&
this.props.allowAnyClickClose &&
this.ref &&
this.ref.current &&
!this.ref.current.contains(e.target) &&
e && e.target && e.target.className &&
typeof e.target.className.indexOf === "function" &&
e.target.className.indexOf("option_checkbox") === -1
) {
this.props.onCancel && this.props.onCancel();
@ -137,6 +229,14 @@ class AdvancedSelector extends React.Component {
}
componentDidUpdate(prevProps) {
if (this.props.isOpen !== prevProps.isOpen) {
handleAnyClick(this.props.isOpen, this.handleClick);
}
if(this.props.allowAnyClickClose !== prevProps.allowAnyClickClose) {
handleAnyClick(this.props.allowAnyClickClose, this.handleClick);
}
let newState = {};
if (!isEqual(this.props.selectedOptions, prevProps.selectedOptions)) {
@ -167,10 +267,6 @@ class AdvancedSelector extends React.Component {
if (!isEmpty(newState)) {
this.setState({ ...this.state, ...newState });
}
if (this.props.isOpen !== prevProps.isOpen) {
handleAnyClick(this.props.isOpen, this.handleClick);
}
}
convertGroups = groups => {
@ -193,7 +289,7 @@ class AdvancedSelector extends React.Component {
const currentGroup = groups.length > 0 ? groups[0] : "No groups";
return currentGroup;
};
onButtonClick = () => {
this.props.onSelect &&
this.props.onSelect(
@ -274,15 +370,23 @@ class AdvancedSelector extends React.Component {
isMultiSelect,
buttonLabel,
selectAllLabel,
size
size,
isDropDown,
onAddNewClick,
allowCreation
} = this.props;
const { selectedOptions, selectedAll, currentGroup, groups } = this.state;
const {
selectedOptions,
selectedAll,
currentGroup,
groups,
} = this.state;
const containerHeight =
size === "compact" ? (!groups || !groups.length ? 336 : 326) : 545;
/*const containerHeight =
size === "compact" ? (!groups || !groups.length ? 336 : 326) : 614;
const containerWidth =
size === "compact" ? (!groups || !groups.length ? 325 : 326) : 690;
size === "compact" ? (!groups || !groups.length ? 325 : 326) : isDropDown ? 690 : 326;
const listHeight =
size === "compact"
? !groups || !groups.length
@ -290,8 +394,31 @@ class AdvancedSelector extends React.Component {
? 176
: 226
: 120
: 345;
: 488;
const listWidth = isDropDown ? 320 : "100%";*/
let containerHeight;
let containerWidth;
let listHeight;
let listWidth;
const itemHeight = 32;
const hasGroups = groups && groups.length > 0;
switch (size) {
case "compact":
containerHeight = hasGroups ? "326px" : "100%";
containerWidth = "379px";
listWidth = isDropDown ? 356 : 356;
listHeight = hasGroups ? 488 : isMultiSelect ? 176 : 226;
break;
case "full":
default:
containerHeight = "100%";
containerWidth = isDropDown ? "690px" : "326px";
listWidth = isDropDown ? 320 : 302;
listHeight = 488;
break;
}
return (
<StyledContainer
@ -301,50 +428,96 @@ class AdvancedSelector extends React.Component {
>
<div ref={this.ref}>
<div className="data_container">
<SearchInput
className="options_searcher"
isDisabled={isDisabled}
size="base"
scale={true}
isNeedFilter={false}
placeholder={placeholder}
value={value}
onChange={onSearchChanged}
onClearSearch={onSearchChanged.bind(this, "")}
/>
{groups && groups.length > 0 && (
<ComboBox
className="options_group_selector"
<div className="data_column_one">
<div className="head_container">
<SearchInput
className="options_searcher"
isDisabled={isDisabled}
options={groups}
selectedOption={currentGroup}
dropDownMaxHeight={200}
scaled={true}
size="content"
onSelect={this.onCurrentGroupChange}
size="base"
scale={true}
isNeedFilter={false}
placeholder={placeholder}
value={value}
onChange={onSearchChanged}
onClearSearch={onSearchChanged.bind(this, "")}
/>
{allowCreation && (
<Button
className="add_new_btn"
primary={false}
size="base"
label=""
icon={
<Icons.PlusIcon
size="medium"
isfill={true}
color="#D8D8D8"
/>
}
onClick={onAddNewClick}
/>
)}
</div>
{!isDropDown &&
groups &&
groups.length > 0 && (
<ComboBox
className="options_group_selector"
isDisabled={isDisabled}
options={groups}
selectedOption={currentGroup}
dropDownMaxHeight={200}
scaled={true}
scaledOptions={true}
size="content"
onSelect={this.onCurrentGroupChange}
/>
)}
{isMultiSelect && !groups && !groups.length && (
<Checkbox
label={selectAllLabel}
isChecked={
selectedAll || selectedOptions.length === options.length
}
isIndeterminate={!selectedAll && selectedOptions.length > 0}
className="option_select_all_checkbox"
onChange={this.onSelectedAllChange}
/>
)}
<FixedSizeList
className="options_list"
height={listHeight}
width={listWidth}
itemSize={itemHeight}
itemCount={this.props.options.length}
itemData={this.props.options}
outerElementType={CustomScrollbarsVirtualList}
>
{this.renderRow.bind(this)}
</FixedSizeList>
</div>
{isDropDown && size === "full" && groups && groups.length > 0 && (
<div className="data_column_two">
<Text.Body
as="p"
className="group_header"
fontSize={15}
isBold={true}
>
Groups
</Text.Body>
<FixedSizeList
className="group_list"
height={listHeight}
itemSize={itemHeight}
itemCount={this.props.groups.length}
itemData={this.props.groups}
outerElementType={CustomScrollbarsVirtualList}
>
{this.renderRow.bind(this)}
</FixedSizeList>
</div>
)}
{isMultiSelect && (
<Checkbox
label={selectAllLabel}
isChecked={
selectedAll || selectedOptions.length === options.length
}
isIndeterminate={!selectedAll && selectedOptions.length > 0}
className="option_select_all_checkbox"
onChange={this.onSelectedAllChange}
/>
)}
<FixedSizeList
className="options_list"
height={listHeight}
itemSize={itemHeight}
itemCount={this.props.options.length}
itemData={this.props.options}
outerElementType={CustomScrollbarsVirtualList}
>
{this.renderRow.bind(this)}
</FixedSizeList>
</div>
{isMultiSelect && (
<div className="button_container">
@ -398,14 +571,18 @@ AdvancedSelector.propTypes = {
onChangeGroup: PropTypes.func,
onCancel: PropTypes.func,
isDropDown: PropTypes.bool,
isOpen: PropTypes.bool
isOpen: PropTypes.bool,
allowCreation: PropTypes.bool,
onAddNewClick: PropTypes.func,
allowAnyClickClose: PropTypes.bool
};
AdvancedSelector.defaultProps = {
isMultiSelect: false,
size: "compact",
buttonLabel: "Add members",
selectAllLabel: "Select all"
selectAllLabel: "Select all",
allowAnyClickClose: true
};
export default AdvancedSelector;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import Button from '.';
describe('<Button />', () => {
@ -10,4 +10,27 @@ describe('<Button />', () => {
expect(wrapper).toExist();
});
it('not re-render test', () => {
const onClick= () => alert('Button clicked');
const wrapper = shallow(<Button size='base' isDisabled={false} onClick={onClick} label="OK" />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it('re-render test by value', () => {
const onClick= () => alert('Button clicked');
const wrapper = shallow(<Button size='base' isDisabled={false} onClick={onClick} label="OK" />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({
...wrapper.props,
label: "Cancel"
});
expect(shouldUpdate).toBe(true);
});
});

View File

@ -2,6 +2,7 @@ import React from 'react';
import styled, { css } from 'styled-components';
import PropTypes from 'prop-types';
import Loader from '../loader';
import isEqual from "lodash/isEqual";
const activeCss = css`
background-color: ${props => (props.primary ? '#1F97CA' : '#ECEEF1')};
@ -198,20 +199,27 @@ Icon.defaultProps = {
icon: null
};
const Button = props => {
//console.log("Button render");
const { isLoading, label, primary, size, icon } = props;
return (
<StyledButton {...props}>
{(isLoading || icon) &&
isLoading
? <Loader type="oval" size={size === "big" ? 16 : 14} color={primary ? "#FFFFFF" : '#333333'} className="loader" />
: <Icon {...props} />
}
{label}
</StyledButton>
);
};
class Button extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
render() {
// console.log("Button render");
const { isLoading, label, primary, size, icon } = this.props;
return (
<StyledButton {...this.props}>
{(isLoading || icon) &&
isLoading
? <Loader type="oval" size={size === "big" ? 16 : 14} color={primary ? "#FFFFFF" : '#333333'} className="loader" />
: <Icon {...this.props} />
}
{label}
</StyledButton>
);
}
}
Button.propTypes = {
label: PropTypes.string,

View File

@ -1,198 +0,0 @@
import React from "react";
import { mount, shallow } from "enzyme";
import { Weekdays, Days, Day } from "./sub-components/";
import NewCalendar from "./";
const baseCalendarProps = {
isDisabled: false,
themeColor: "#ED7309",
selectedDate: new Date(),
openToDate: new Date(),
minDate: new Date("1970/01/01"),
maxDate: new Date("3000/01/01"),
locale: "en",
onChange: () => jest.fn()
};
const baseWeekdaysProps = {
optionsWeekdays: [
{ key: "en_0", value: "Mo", color: "" },
{ key: "en_1", value: "Tu", color: "" },
{ key: "en_2", value: "We", color: "" },
{ key: "en_3", value: "Th", color: "" },
{ key: "en_4", value: "Fr", color: "" },
{ key: "en_5", value: "Sa", color: "#A3A9AE" },
{ key: "en_6", value: "Su", color: "#A3A9AE" }
],
size: "base"
};
const baseDaysProps = {
optionsDays: [
{
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 25
},
{
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 26
}
],
onDayClick: jest.fn,
size: "base"
};
const baseDayProps = {
day: {
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 26
},
onDayClick: jest.fn,
size: "base"
};
const selectedDate = new Date("09/12/2019");
const openToDate = new Date("09/12/2019");
const minDate = new Date("01/01/1970");
const maxDate = new Date("01/01/2020");
describe("Weekdays tests:", () => {
it("Weekdays renders without error", () => {
const wrapper = mount(<Weekdays {...baseWeekdaysProps} />);
expect(wrapper).toExist();
});
it("Weekdays not re-render test", () => {
const wrapper = shallow(<Weekdays {...baseWeekdaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Weekdays property size passed", () => {
const wrapper = mount(<Weekdays {...baseWeekdaysProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
});
describe("Days tests:", () => {
it("Days renders without error", () => {
const wrapper = mount(<Days {...baseDaysProps} />);
expect(wrapper).toExist();
});
it("Days not re-render test", () => {
const wrapper = shallow(<Days {...baseDaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Days property size passed", () => {
const wrapper = mount(<Days {...baseDaysProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
/*
it("Days click event", () => {
const mockCallBack = jest.fn();
const button = shallow(<Days {...baseDaysProps} />);
button.find("DayContent").simulate("click");
expect(mockCallBack.mock.calls.length).toEqual(1);
});
*/
});
describe("Day tests:", () => {
it("Day renders without error", () => {
const wrapper = mount(<Day {...baseDayProps} />);
expect(wrapper).toExist();
});
it("Day not re-render test", () => {
const wrapper = shallow(<Day {...baseDayProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Day property size passed", () => {
const wrapper = mount(<Day {...baseDayProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
});
describe("Calendar tests:", () => {
it("Calendar renders without error", () => {
const wrapper = mount(<NewCalendar {...baseCalendarProps} />);
expect(wrapper).toExist();
});
it("Calendar has rendered content.", () => {
const wrapper = mount(<NewCalendar {...baseCalendarProps} />);
expect(wrapper.find("div")).toExist();
expect(wrapper.find("ul")).not.toExist();
});
it("Calendar not re-render test", () => {
const wrapper = shallow(<NewCalendar {...baseCalendarProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(
wrapper.props,
wrapper.state
);
expect(shouldUpdate).toBe(false);
});
it("Calendar selectedDate test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} selectedDate={selectedDate} />
);
expect(wrapper.props().selectedDate).toEqual(selectedDate);
});
it("Calendar openToDate test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} openToDate={openToDate} />
);
expect(wrapper.props().openToDate).toEqual(openToDate);
});
it("Calendar minDate test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} minDate={minDate} />
);
expect(wrapper.props().minDate).toEqual(minDate);
});
it("Calendar maxDate test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} maxDate={maxDate} />
);
expect(wrapper.props().maxDate).toEqual(maxDate);
});
it("Calendar themeColor test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} themeColor={"#fff"} />
);
expect(wrapper.props().themeColor).toEqual("#fff");
});
it("Calendar locale test", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} locale={"en-GB"} />
);
expect(wrapper.prop("locale")).toEqual("en-GB");
});
it("Calendar disabled when isDisabled is passed", () => {
const wrapper = mount(
<NewCalendar {...baseCalendarProps} isDisabled={true} />
);
expect(wrapper.prop("isDisabled")).toEqual(true);
});
});

View File

@ -7,9 +7,9 @@ Custom calendar
#### Usage
```js
import { NewCalendar } from "asc-web-components";
import { Calendar } from "asc-web-components";
<NewCalendar
<Calendar
onChange={date => {
console.log("Selected date:", date);
}}

View File

@ -10,7 +10,7 @@ import {
} from "@storybook/addon-knobs/react";
import withReadme from "storybook-readme/with-readme";
import Readme from "./README.md";
import NewCalendar from ".";
import Calendar from ".";
function myDateKnob(name, defaultValue) {
const stringTimestamp = date(name, defaultValue);
@ -51,7 +51,7 @@ storiesOf("Components|Calendar", module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add("base", () => (
<NewCalendar
<Calendar
onChange={date => {
action("Selected date")(date);
}}

View File

@ -0,0 +1,319 @@
import React from "react";
import { mount, shallow, render } from "enzyme";
import { Weekdays, Days, Day } from "./sub-components/";
import Calendar from "./";
import ComboBox from "../combobox";
import moment from "moment";
const baseCalendarProps = {
isDisabled: false,
themeColor: "#ED7309",
selectedDate: new Date(),
openToDate: new Date(),
minDate: new Date("1970/01/01"),
maxDate: new Date("3000/01/01"),
locale: "en",
onChange: () => jest.fn()
};
const baseWeekdaysProps = {
optionsWeekdays: [
{ key: "en_0", value: "Mo", color: "" },
{ key: "en_1", value: "Tu", color: "" },
{ key: "en_2", value: "We", color: "" },
{ key: "en_3", value: "Th", color: "" },
{ key: "en_4", value: "Fr", color: "" },
{ key: "en_5", value: "Sa", color: "#A3A9AE" },
{ key: "en_6", value: "Su", color: "#A3A9AE" }
],
size: "base"
};
const baseDaysProps = {
optionsDays: [
{
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 25
},
{
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 26
}
],
onDayClick: jest.fn,
size: "base"
};
const baseDayProps = {
day: {
className: "calendar-month_neighboringMonth",
dayState: "prev",
disableClass: null,
value: 26
},
onDayClick: jest.fn(),
size: "base"
};
const options = [
{ key: 0, value: "one" },
{ key: 1, value: "two" },
{ key: 2, value: "three" }
];
const baseComboBoxProps = {
options: options,
selectedOption: { key: 0, value: "one" }
};
const selectedDate = new Date("09/12/2019");
const openToDate = new Date("09/12/2019");
const minDate = new Date("01/01/1970");
const maxDate = new Date("01/01/2020");
const months = moment.months();
describe("Weekdays tests:", () => {
it("Weekdays renders without error", () => {
const wrapper = mount(<Weekdays {...baseWeekdaysProps} />);
expect(wrapper).toExist();
});
it("Weekdays not re-render test", () => {
const wrapper = shallow(<Weekdays {...baseWeekdaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Weekdays render test", () => {
const wrapper = shallow(<Weekdays {...baseWeekdaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({
...wrapper.props,
size: "big"
});
expect(shouldUpdate).toBe(true);
});
it("Weekdays property size passed", () => {
const wrapper = mount(<Weekdays {...baseWeekdaysProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
});
describe("Days tests:", () => {
it("Days renders without error", () => {
const wrapper = mount(<Days {...baseDaysProps} />);
expect(wrapper).toExist();
});
it("Days not re-render test", () => {
const wrapper = shallow(<Days {...baseDaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Days render test", () => {
const wrapper = shallow(<Days {...baseDaysProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({
...wrapper.props,
size: "big"
});
expect(shouldUpdate).toBe(true);
});
it("Days property size passed", () => {
const wrapper = mount(<Days {...baseDaysProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
});
describe("Day tests:", () => {
it("Day renders without error", () => {
const wrapper = mount(<Day {...baseDayProps} />);
expect(wrapper).toExist();
});
it("Day not re-render test", () => {
const wrapper = shallow(<Day {...baseDayProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props);
expect(shouldUpdate).toBe(false);
});
it("Day render test", () => {
const wrapper = shallow(<Day {...baseDayProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({
...wrapper.props,
size: "big"
});
expect(shouldUpdate).toBe(true);
});
it("Day property size passed", () => {
const wrapper = mount(<Day {...baseDayProps} size={"big"} />);
expect(wrapper.prop("size")).toEqual("big");
});
});
describe("Calendar tests:", () => {
it("Calendar renders without error", () => {
const wrapper = mount(<Calendar {...baseCalendarProps} />);
expect(wrapper).toExist();
});
it("Calendar selectedDate test", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} selectedDate={selectedDate} />
);
expect(wrapper.props().selectedDate).toEqual(selectedDate);
});
it("Calendar openToDate test", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} openToDate={openToDate} />
);
expect(wrapper.props().openToDate).toEqual(openToDate);
});
it("Calendar minDate test", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} minDate={minDate} />
);
expect(wrapper.props().minDate).toEqual(minDate);
});
it("Calendar maxDate test", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} maxDate={maxDate} />
);
expect(wrapper.props().maxDate).toEqual(maxDate);
});
it("Calendar themeColor test", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} themeColor={"#fff"} />
);
expect(wrapper.props().themeColor).toEqual("#fff");
});
it("Calendar locale test", () => {
const wrapper = mount(<Calendar {...baseCalendarProps} locale={"en-GB"} />);
expect(wrapper.prop("locale")).toEqual("en-GB");
});
it("Calendar size test", () => {
const wrapper = shallow(<Calendar {...baseCalendarProps} size="big" />);
expect(wrapper.prop("size")).toEqual("big");
});
it("Calendar disabled when isDisabled is passed", () => {
const wrapper = mount(
<Calendar {...baseCalendarProps} isDisabled={true} />
);
expect(wrapper.prop("isDisabled")).toEqual(true);
});
it("Calendar has rendered content ComboBox", () => {
const wrapper = mount(<Calendar {...baseCalendarProps} />);
expect(wrapper).toExist(<ComboBox {...baseComboBoxProps} />);
});
it("Calendar check the onChange callback", () => {
const onChange = jest.fn();
const props = {
selectedDate: new Date("03/03/2000"),
onChange
};
const wrapper = shallow(<Calendar {...props} />).instance();
wrapper.onDayClick({
value: 1,
disableClass: "",
className: "",
dayState: "prev"
});
expect(onChange).toBeCalled();
const wrapper2 = shallow(<Calendar {...props} />).instance();
wrapper2.onDayClick({
value: 1,
disableClass: "",
className: "",
dayState: "next"
});
expect(onChange).toBeCalled();
const wrapper3 = shallow(<Calendar {...props} />).instance();
wrapper3.onDayClick({
value: 1,
disableClass: "",
className: "",
dayState: "now"
});
expect(onChange).toBeCalled();
});
it("Calendar check onSelectYear function", () => {
const props = {
openToDate: new Date("05/01/2000"),
selectedDate: new Date("01/01/2000"),
minDate: new Date("01/01/1970"),
maxDate: new Date("01/01/2020")
};
const wrapper = shallow(<Calendar {...props} />).instance();
wrapper.onSelectYear({
key: 2020,
value: 2020
});
expect(wrapper.state.openToDate).toEqual(new Date("01/01/2020"));
});
it("Calendar check onSelectMonth function", () => {
const props = {
openToDate: new Date("01/01/2000"),
selectedDate: new Date("01/01/2000")
};
const wrapper = shallow(<Calendar {...props} />).instance();
wrapper.onSelectMonth({ key: "1", label: "February", disabled: false });
expect(wrapper.state.openToDate).toEqual(new Date("02/01/2000"));
});
it("Calendar check Compare dates function", () => {
const date = new Date();
const wrapper = shallow(<Calendar {...baseCalendarProps} />).instance();
expect(wrapper.compareDates(date, date) === 0).toEqual(true);
expect(wrapper.compareDates(date, new Date("01/01/2000")) === 0).toEqual(
false
);
});
it("Calendar error date test", () => {
const wrapper = shallow(<Calendar {...baseCalendarProps} />);
wrapper.setState({ hasError: true, isDisabled: true });
expect(wrapper.instance().state.hasError).toEqual(true);
expect(wrapper.instance().state.isDisabled).toEqual(true);
});
it("Calendar not error date test", () => {
const wrapper = shallow(<Calendar {...baseCalendarProps} />);
wrapper.setState({ hasError: false, isDisabled: false });
expect(wrapper.instance().state.hasError).toEqual(false);
});
it("Calendar componentDidUpdate() test", () => {
const wrapper = mount(<Calendar {...baseCalendarProps} />).instance();
wrapper.componentDidUpdate(wrapper.props, wrapper.state);
const wrapper2 = mount(
<Calendar {...baseCalendarProps} selectedDate={new Date("01/01/1910")} />
).instance();
expect(wrapper.props).toBe(wrapper.props);
expect(wrapper.state).toBe(wrapper.state);
expect(wrapper2.props).toBe(wrapper2.props);
expect(wrapper2.state).toBe(wrapper2.state);
});
});

View File

@ -594,35 +594,6 @@ class Calendar extends Component {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
selectedDate,
openToDate,
minDate,
maxDate,
isDisabled,
locale,
themeColor
} = this.props;
const { hasError, optionsDays } = this.state;
if (
this.compareDates(selectedDate, nextProps.selectedDate) === 0 &&
this.compareDates(openToDate, nextProps.openToDate) === 0 &&
this.compareDates(minDate, nextProps.minDate) === 0 &&
this.compareDates(maxDate, nextProps.maxDate) === 0 &&
isDisabled === nextProps.isDisabled &&
hasError === nextState.hasError &&
optionsDays === nextState.optionsDays &&
locale === nextProps.locale &&
themeColor === nextProps.themeColor
) {
return false;
}
return true;
}
render() {
//console.log("Calendar render");
@ -648,6 +619,7 @@ class Calendar extends Component {
<ComboBoxMonthStyle size={size}>
<ComboBox
scaled={true}
scaledOptions={true}
dropDownMaxHeight={dropDownSizeMonth}
onSelect={this.onSelectMonth}
selectedOption={selectedOptionMonth}
@ -658,6 +630,7 @@ class Calendar extends Component {
<ComboBoxDateStyle>
<ComboBox
scaled={true}
scaledOptions={true}
dropDownMaxHeight={dropDownSizeYear}
onSelect={this.onSelectYear}
selectedOption={selectedOptionYear}

View File

@ -83,6 +83,7 @@ const options = [
dropDownMaxHeight={200}
noBorder={false}
scale={true}
scaledOptions={true}
size='content'
onSelect={option => console.log('selected', option)}
/>
@ -90,14 +91,60 @@ const options = [
#### Properties
| Props | Type | Required | Values | Default | Description |
| ------------------- | --------- | :------: | ------------------------------------------ | ------- | --------------------------------------------------------- |
| `options` | `array` | ✅ | - | - | Combo box options |
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
| `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
| `selectedOption` | `object` | ✅ | - | - | Selected option |
| `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option |
| `dropDownMaxHeight` | `number` | - | - | - | Height of Dropdown |
| `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent |
| `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default |
| `advancedOptions` | `element` | - | - | - | If you need display options not basic options |
| Props | Type | Required | Values | Default | Description |
| ------------------- | --------- | :------: | ------------------------------------------ | ------- | ----------------------------------------------------------- |
| `options` | `array` | ✅ | - | - | Combo box options |
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
| `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
| `selectedOption` | `object` | ✅ | - | - | Selected option |
| `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option |
| `dropDownMaxHeight` | `number` | - | - | - | Height of Dropdown |
| `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent |
| `scaledOptions` | `bool` | - | - | `false` | Indicates that component`s options is scaled by ComboButton |
| `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default |
| `advancedOptions` | `element` | - | - | - | If you need display options not basic options |
## ComboButton
#### Description
> This description is for reference only, the component described below is not exported.
To create designs using combobox logic, there is a child component ComboButton.
This is an independent element that responds to changes in parameters and serves only to demonstrate set values.
```js
<ComboButton
noBorder={false}
isDisabled={false}
selectedOption={{
key: 0,
label: "Select"
}}
withOptions={false}
optionsLength={0}
withAdvancedOptions={true}
innerContainer={<>Demo container</>}
innerContainerClassName="optionalBlock"
isOpen={false}
size="content"
scaled={false}
/>
```
#### Properties
| Props | Type | Required | Values | Default | Description |
| ------------------------- | -------- | :------: | -------------------------------- | ---------------- | -------------------------------------------------------- |
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
| `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
| `selectedOption` | `object` | - | - | - | Selected option |
| `withOptions` | `bool` | - | - | `true` | Lets you style as ComboBox with options |
| `optionsLength` | `number` | - | - | - | Lets you style as ComboBox with options |
| `withAdvancedOptions` | `bool` | - | - | `false` | Lets you style as a ComboBox with advanced options |
| `innerContainer` | `node` | - | - | - | Allows displaying third-party element inside ComboButton |
| `innerContainerClassName` | `string` | - | - | `innerContainer` | Required to access third-party container |
| `isOpen` | `bool` | - | - | `false` | Lets you style as ComboBox arrow |
| `scaled` | `bool` | - | - | `false` | Indicates that component is scaled by parent |
| `size` | `oneOf` | - | `base`, `...`, `huge`, `content` | `content` | Select component width, one of default |
| `onClick` | `func` | - | - | - | Will be triggered whenever an ComboButton is clicked |

View File

@ -58,7 +58,7 @@ storiesOf('Components|Input', module)
];
const needScrollDropDown = boolean('Need scroll dropdown', false);
const dropDownMaxHeight = needScrollDropDown && number('dropDownMaxHeight', 200);
const dropDownMaxHeight = needScrollDropDown ? number('dropDownMaxHeight', 200) : null;
const optionsMultiSelect = options('children',
{
button: 'button',
@ -125,6 +125,7 @@ storiesOf('Components|Input', module)
noBorder={boolean('noBorder', false)}
dropDownMaxHeight={dropDownMaxHeight}
scaled={boolean('scaled', false)}
scaledOptions={boolean('scaledOptions', false)}
size={select('size', sizeOptions, 'content')}
>
{childrenItems}
@ -146,7 +147,6 @@ storiesOf('Components|Input', module)
>
<Icons.NavLogoIcon size="medium" key='comboIcon' />
</ComboBox>
</td>
</tr>
</tbody>

View File

@ -31,6 +31,7 @@ const baseProps = {
isDisabled: false,
selectedOption: {
key: 0,
icon: 'CatalogFolderIcon',
label: "Select"
},
options: baseOptions,
@ -41,7 +42,7 @@ const baseProps = {
};
describe('<ComboBox />', () => {
it('renders without error', () => {
it('rendered without error', () => {
const wrapper = mount(
<ComboBox {...baseProps} />
);
@ -49,7 +50,7 @@ describe('<ComboBox />', () => {
expect(wrapper).toExist();
});
it('render with advanced options', () => {
it('with advanced options', () => {
const wrapper = mount(
<ComboBox {...baseProps} options={[]} advancedOptions={advancedOptions} />
);
@ -57,13 +58,79 @@ describe('<ComboBox />', () => {
expect(wrapper).toExist();
});
it('disabled when isDisabled is passed', () => {
it('disabled', () => {
const wrapper = mount(<ComboBox {...baseProps} isDisabled={true} />);
expect(wrapper.prop('isDisabled')).toEqual(true);
});
it('not re-render test', () => {
it('without borders', () => {
const wrapper = mount(<ComboBox {...baseProps} noBorder={true} />);
expect(wrapper.prop('noBorder')).toEqual(true);
});
it('opened', () => {
const wrapper = mount(<ComboBox {...baseProps} opened={true} />);
expect(wrapper.prop('opened')).toEqual(true);
});
it('with DropDown max height', () => {
const wrapper = mount(<ComboBox {...baseProps} dropDownMaxHeight={200} />);
expect(wrapper.prop('dropDownMaxHeight')).toEqual(200);
});
it('without scaled', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={false} />);
expect(wrapper.prop('scaled')).toEqual(false);
});
it('scaled', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={true} />);
expect(wrapper.prop('scaled')).toEqual(true);
});
it('scaled options', () => {
const wrapper = mount(<ComboBox {...baseProps} scaledOptions={true} />);
expect(wrapper.prop('scaledOptions')).toEqual(true);
});
it('middle size options', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={false} size='middle' />);
expect(wrapper.prop('size')).toEqual('middle');
});
it('big size options', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={false} size='big' />);
expect(wrapper.prop('size')).toEqual('big');
});
it('huge size options', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={false} size='huge' />);
expect(wrapper.prop('size')).toEqual('huge');
});
it('by content size options', () => {
const wrapper = mount(<ComboBox {...baseProps} scaled={false} size='content' />);
expect(wrapper.prop('size')).toEqual('content');
});
it('with children node', () => {
const wrapper = mount(<ComboBox {...baseProps} ><div>demo</div></ComboBox>);
expect(wrapper.contains(<div>demo</div>)).toBe(true)
});
it('not re-render', () => {
const wrapper = shallow(<ComboBox {...baseProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props, wrapper.state);
@ -71,23 +138,121 @@ describe('<ComboBox />', () => {
expect(shouldUpdate).toBe(false);
});
it('re-render test', () => {
it('re-render', () => {
const wrapper = shallow(<ComboBox {...baseProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({
noBorder: true,
isDisabled: false,
selectedOption: {
key: 0,
label: "Select"
},
options: baseOptions,
opened: false,
onSelect: () => jest.fn(),
size: 'base',
scaled: true
}, wrapper.state);
const shouldUpdate = wrapper.shouldComponentUpdate({ opened: true }, wrapper.state);
expect(shouldUpdate).toBe(true);
});
it('causes function comboBoxClick() with disabled prop', () => {
const wrapper = shallow(<ComboBox {...baseProps} isDisabled={true} />);
const instance = wrapper.instance();
instance.comboBoxClick();
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function comboBoxClick()', () => {
const wrapper = shallow(<ComboBox {...baseProps} />);
const instance = wrapper.instance();
instance.comboBoxClick();
expect(wrapper.state('isOpen')).toBe(true);
});
it('causes function optionClick()', () => {
const onSelect = jest.fn();
const selectedOption = {
key: 1,
label: "Select"
};
const wrapper = shallow(<ComboBox {...baseProps} opened={true} onSelect={onSelect} />);
const instance = wrapper.instance();
instance.optionClick(selectedOption);
expect(wrapper.state('isOpen')).toBe(false);
expect(onSelect).toHaveBeenCalledWith(selectedOption);
});
it('causes function stopAction()', () => {
const wrapper = mount(<ComboBox {...baseProps} />);
const instance = wrapper.instance();
instance.stopAction(new Event('click'));
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function handleClick() with opened prop', () => {
const wrapper = mount(<ComboBox {...baseProps} opened={true} />);
const instance = wrapper.instance();
instance.handleClick(new Event('click'));
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function handleClick()', () => {
const wrapper = mount(<ComboBox {...baseProps} />);
const instance = wrapper.instance();
instance.handleClick(new Event('click'));
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function handleClick() with simulate', () => {
const wrapper = mount(<ComboBox {...baseProps} opened={true} />);
wrapper.simulate('click');
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function handleClick() with simulate and ComboBox not opened', () => {
const wrapper = mount(<ComboBox {...baseProps} />);
wrapper.simulate('click');
expect(wrapper.state('isOpen')).toBe(true);
});
it('componentDidUpdate() state lifecycle test', () => {
const wrapper = shallow(<ComboBox {...baseProps} />);
const instance = wrapper.instance();
wrapper.setState({ isOpen: false });
instance.componentDidUpdate(wrapper.props(), wrapper.state());
expect(wrapper.state()).toBe(wrapper.state());
});
it('componentDidUpdate() props lifecycle test', () => {
const wrapper = shallow(<ComboBox {...baseProps} />);
const instance = wrapper.instance();
instance.componentDidUpdate({
opened: true,
selectedOption: {
key: 1,
label: "Select"
}
}, wrapper.state());
expect(wrapper.props()).toBe(wrapper.props());
});
it('componentWillUnmount() lifecycle test', () => {
const wrapper = mount(<ComboBox {...baseProps} opened={true} />);
const componentWillUnmount = jest.spyOn(wrapper.instance(), 'componentWillUnmount');
wrapper.unmount();
expect(componentWillUnmount).toHaveBeenCalled();
});
});

View File

@ -3,12 +3,11 @@ import PropTypes from 'prop-types'
import styled from 'styled-components';
import DropDownItem from '../drop-down-item'
import DropDown from '../drop-down'
import { Icons } from '../icons'
import { handleAnyClick } from '../../utils/event';
import isEqual from 'lodash/isEqual';
import ComboButton from './sub-components/combo-button'
const StyledComboBox = styled.div`
color: ${props => props.isDisabled ? '#D0D5DA' : '#333333'};
width: ${props =>
(props.scaled && '100%') ||
(props.size === 'base' && '173px') ||
@ -19,92 +18,6 @@ const StyledComboBox = styled.div`
};
position: relative;
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background: #FFFFFF;
${props => !props.noBorder && `
border: 1px solid #D0D5DA;
border-radius: 3px;
`}
${props => props.isDisabled && !props.noBorder && `
border-color: #ECEEF1;
background: #F8F9F9;
`}
${props => !props.noBorder && `
height: 32px;
`}
:hover{
border-color: ${state => state.isOpen ? '#2DA7DB' : '#A3A9AE' };
cursor: ${props => (props.isDisabled || !props.options.length ) ? (props.advancedOptions) ? 'pointer' : 'default' : 'pointer'};
${props => props.isDisabled && `
border-color: #ECEEF1;
`}
}
`;
const StyledComboButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: ${props => props.noBorder ? `18px` : `30px`};
margin-left: 8px;
`;
const StyledIcon = styled.div`
width: 16px;
margin-right: 8px;
margin-top: -2px;
`;
const StyledOptionalItem = styled.div`
margin-right: 8px;
`;
const StyledLabel = styled.div`
font-family: Open Sans;
font-style: normal;
font-weight: 600;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8px;
${props => props.noBorder && `
line-height: 11px;
border-bottom: 1px dashed transparent;
:hover{
border-bottom: 1px dashed;
}
`};
`;
const StyledArrowIcon = styled.div`
display: flex;
align-self: start;
width: ${props => props.needDisplay ? '8px' : '0px'};
flex: 0 0 ${props => props.needDisplay ? '8px' : '0px'};
margin-top: ${props => props.noBorder ? `5px` : `12px`};
margin-right: ${props => props.needDisplay ? '8px' : '0px'};
margin-left: ${props => props.needDisplay ? 'auto' : '0px'};
${props => props.isOpen && `
transform: scale(1, -1);
`}
`;
class ComboBox extends React.Component {
@ -122,26 +35,27 @@ class ComboBox extends React.Component {
handleAnyClick(true, this.handleClick);
}
handleClick = (e) =>
this.state.isOpen
&& !this.ref.current.contains(e.target)
&& this.toggle(false);
handleClick = (e) =>{
if (this.state.isOpen && !this.ref.current.contains(e.target)) {
this.toggle(false);
}
}
stopAction = (e) => e.preventDefault();
toggle = (isOpen) => this.setState({ isOpen: isOpen });
comboBoxClick = (e) => {
if (this.props.isDisabled || e.target.closest('.optionalBlock')) return;
if (this.props.isDisabled || e && e.target.closest('.optionalBlock')) return;
this.toggle(!this.state.isOpen);
};
optionClick = (option) => {
this.toggle(!this.state.isOpen);
this.setState({
isOpen: !this.state.isOpen,
selectedOption: option
});
this.props.onSelect && this.props.onSelect(option);
};
@ -149,7 +63,7 @@ class ComboBox extends React.Component {
handleAnyClick(false, this.handleClick);
}
shouldComponentUpdate(nextProps,nextState) {
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
@ -170,62 +84,57 @@ class ComboBox extends React.Component {
render() {
//console.log("ComboBox render");
const {
dropDownMaxHeight,
isDisabled,
directionX,
directionY,
scaled,
children,
options,
noBorder,
advancedOptions
} = this.props;
dropDownMaxHeight,
directionX,
directionY,
scaled,
size,
options,
advancedOptions,
isDisabled,
children,
noBorder,
scaledOptions } = this.props;
const { isOpen, selectedOption } = this.state;
const dropDownMaxHeightProp = dropDownMaxHeight ? { maxHeight: dropDownMaxHeight } : {};
const dropDownManualWidthProp = scaled ? { manualWidth: '100%' } : {};
const boxIconColor = isDisabled ? '#D0D5DA' : '#333333';
const arrowIconColor = isDisabled ? '#D0D5DA' : '#A3A9AE';
const dropDownMaxHeightProp = dropDownMaxHeight
? { maxHeight: dropDownMaxHeight }
: {};
const dropDownManualWidthProp = scaledOptions
? { manualWidth: '100%' }
: {};
const optionsLength = options.length
? options.length
: 0;
const advancedOptionsLength = advancedOptions
? advancedOptions.props.children.length
: 0;
return (
<StyledComboBox ref={this.ref}
{...this.props}
{...this.state}
<StyledComboBox
ref={this.ref}
isDisabled={isDisabled}
scaled={scaled}
size={size}
data={selectedOption}
onClick={this.comboBoxClick}
onSelect={this.stopAction}
{...this.props}
>
<StyledComboButton noBorder={noBorder}>
{children &&
<StyledOptionalItem className='optionalBlock'>
{children}
</StyledOptionalItem>
}
{selectedOption && selectedOption.icon &&
<StyledIcon>
{React.createElement(Icons[selectedOption.icon],
{
size: 'scale',
color: boxIconColor,
isfill: true
})
}
</StyledIcon>
}
<StyledLabel noBorder={noBorder}>
{selectedOption.label}
</StyledLabel>
<StyledArrowIcon needDisplay={options.length > 0 || advancedOptions !== undefined} noBorder={noBorder} isOpen={isOpen}>
{(options.length > 0 || advancedOptions !== undefined) &&
React.createElement(Icons['ExpanderDownIcon'],
{
size: 'scale',
color: arrowIconColor,
isfill: true
})
}
</StyledArrowIcon>
</StyledComboButton>
<ComboButton
noBorder={noBorder}
isDisabled={isDisabled}
selectedOption={selectedOption}
withOptions={optionsLength > 0}
optionsLength={optionsLength}
withAdvancedOptions={advancedOptionsLength > 0}
innerContainer={children}
innerContainerClassName='optionalBlock'
isOpen={isOpen}
size={size}
scaled={scaled}
/>
<DropDown
directionX={directionX}
directionY={directionY}
@ -234,15 +143,17 @@ class ComboBox extends React.Component {
{...dropDownMaxHeightProp}
{...dropDownManualWidthProp}
>
{advancedOptions
? advancedOptions
: options.map((option) =>
<DropDownItem {...option}
key={option.key}
disabled={option.disabled || (option.label === selectedOption.label)}
onClick={this.optionClick.bind(this, option)}
/>
)}
{advancedOptions
? advancedOptions
: options.map((option) =>
<DropDownItem {...option}
key={option.key}
disabled={
option.disabled
|| (option.label === selectedOption.label)}
onClick={this.optionClick.bind(this, option)}
/>
)}
</DropDown>
</StyledComboBox>
);
@ -264,14 +175,16 @@ ComboBox.propTypes = {
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge', 'content']),
directionX: PropTypes.oneOf(['left', 'right']),
directionY: PropTypes.oneOf(['bottom', 'top']),
scaled: PropTypes.bool
scaled: PropTypes.bool,
scaledOptions: PropTypes.bool
}
ComboBox.defaultProps = {
noBorder: false,
isDisabled: false,
size: 'base',
scaled: true
scaled: true,
scaledOptions: false
}
export default ComboBox;

View File

@ -0,0 +1,205 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components';
import { Icons } from '../../icons'
const StyledComboButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: ${props => props.noBorder ? `18px` : `30px`};
width: ${props =>
(props.scaled && '100%') ||
(props.size === 'base' && '173px') ||
(props.size === 'middle' && '300px') ||
(props.size === 'big' && '350px') ||
(props.size === 'huge' && '500px') ||
(props.size === 'content' && 'fit-content')
};
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
padding-left: 8px;
background: ${props => !props.noBorder ? '#FFFFFF' : 'none'};
color: ${props => props.isDisabled ? '#D0D5DA' : '#333333'};
${props => !props.noBorder && `
border: 1px solid #D0D5DA;
border-radius: 3px;
`}
${props => props.isDisabled && !props.noBorder && `
border-color: #ECEEF1;
background: #F8F9F9;
`}
${props => !props.noBorder && `
height: 32px;
`}
:hover{
border-color: ${props => props.isOpen ? '#2DA7DB' : '#A3A9AE'};
cursor: ${props => (props.isDisabled || (!props.containOptions && !props.withAdvancedOptions))
? 'default'
: 'pointer'};
${props => props.isDisabled && `
border-color: #ECEEF1;
`}
}
`;
const StyledOptionalItem = styled.div`
margin-right: 8px;
`;
const StyledIcon = styled.div`
width: 16px;
margin-right: 8px;
margin-top: -2px;
`;
const StyledLabel = styled.div`
font-family: Open Sans;
font-style: normal;
font-weight: 600;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 8px;
${props => props.noBorder && `
line-height: 11px;
border-bottom: 1px dashed transparent;
:hover{
border-bottom: 1px dashed;
}
`};
`;
const StyledArrowIcon = styled.div`
display: flex;
align-self: start;
width: ${props => props.needDisplay ? '8px' : '0px'};
flex: 0 0 ${props => props.needDisplay ? '8px' : '0px'};
margin-top: ${props => props.noBorder ? `5px` : `12px`};
margin-right: ${props => props.needDisplay ? '8px' : '0px'};
margin-left: ${props => props.needDisplay ? 'auto' : '0px'};
${props => props.isOpen && `
transform: scale(1, -1);
`}
`;
class ComboButton extends React.Component {
render() {
const {
noBorder,
onClick,
isDisabled,
innerContainer,
innerContainerClassName,
selectedOption,
optionsLength,
withOptions,
withAdvancedOptions,
isOpen,
scaled,
size } = this.props;
const boxIconColor = isDisabled ? '#D0D5DA' : '#333333';
const arrowIconColor = isDisabled ? '#D0D5DA' : '#A3A9AE';
return (
<StyledComboButton
isOpen={isOpen}
isDisabled={isDisabled}
noBorder={noBorder}
containOptions={optionsLength}
withAdvancedOptions={withAdvancedOptions}
onClick={onClick}
scaled={scaled}
size={size}
>
{innerContainer &&
<StyledOptionalItem className={innerContainerClassName}>
{innerContainer}
</StyledOptionalItem>
}
{selectedOption && selectedOption.icon &&
<StyledIcon>
{React.createElement(Icons[selectedOption.icon],
{
size: 'scale',
color: boxIconColor,
isfill: true
})
}
</StyledIcon>
}
<StyledLabel noBorder={noBorder}>
{selectedOption.label}
</StyledLabel>
<StyledArrowIcon
needDisplay={withOptions || withAdvancedOptions}
noBorder={noBorder}
isOpen={isOpen}>
{(withOptions || withAdvancedOptions) &&
React.createElement(Icons['ExpanderDownIcon'],
{
size: 'scale',
color: arrowIconColor,
isfill: true
})
}
</StyledArrowIcon>
</StyledComboButton>
);
}
}
ComboButton.propTypes = {
noBorder: PropTypes.bool,
isDisabled: PropTypes.bool,
selectedOption: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.object),
PropTypes.object
]),
withOptions: PropTypes.bool,
optionsLength: PropTypes.number,
withAdvancedOptions: PropTypes.bool,
innerContainer: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]),
innerContainerClassName: PropTypes.string,
isOpen: PropTypes.bool,
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge', 'content']),
scaled: PropTypes.bool,
onClick: PropTypes.func
}
ComboButton.defaultProps = {
noBorder: false,
isDisabled: false,
withOptions: true,
withAdvancedOptions: false,
innerContainerClassName: 'innerContainer',
isOpen: false,
size: 'content',
scaled: false
}
export default ComboButton;

View File

@ -1,29 +0,0 @@
# DateInput
#### Description
Custom date input
#### Usage
```js
import { DateInput } from 'asc-web-components';
<DateInput selected={new Date()} dateFormat="dd.MM.yyyy" onChange={date => {}}/>
```
#### Properties
https://reactdatepicker.com/
| Props | Type | Required | Values | Default | Description |
| ------------ | -------- | :------: | ------ | ------- | --------------------------------------- |
| `id` | `string` | - | - | - | Used as HTML `id` property |
| `name` | `string` | - | - | - | Used as HTML `name` property |
| `disabled` | `bool` | - | - | - | Used as HTML `disabled` property |
| `readOnly` | `bool` | - | - | - | Used as HTML `readOnly` property |
| `selected` | `date` | - | - | - | Selected date value |
| `onChange` | `func` | - | - | - | OnChange event |
| `dateFormat` | `string` | - | - | - | Date format string |
| `hasError` | `bool` | - | - | - | Indicates the input field has an error |
| `hasWarning` | `bool` | - | - | - | Indicates the input field has a warning |

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