This commit is contained in:
NikolayRechkin 2019-12-26 11:02:07 +03:00
commit d046fc738d
54 changed files with 1954 additions and 1136 deletions

View File

@ -44,6 +44,7 @@ namespace ASC.Core.Data
{
FromDbQuotaToTenantQuota = r => new TenantQuota()
{
Id = r.Tenant,
Name = r.Name,
ActiveUsers = r.ActiveUsers != 0 ? r.ActiveUsers : int.MaxValue,
AvangateId = r.AvangateId,

View File

@ -632,11 +632,11 @@ namespace ASC.Core.Data
if (!string.IsNullOrEmpty(text))
{
q.Where(
u => u.FirstName.Contains(text) |
u.LastName.Contains(text) |
u.Title.Contains(text) |
u.Location.Contains(text) |
q = q.Where(
u => u.FirstName.Contains(text) ||
u.LastName.Contains(text) ||
u.Title.Contains(text) ||
u.Location.Contains(text) ||
u.Email.Contains(text));
}

View File

@ -44,7 +44,7 @@ namespace ASC.Core.Tenants
};
[DataMember(Name = "Id", Order = 10)]
public int Id { get; private set; }
public int Id { get; set; }
[DataMember(Name = "Name", Order = 20)]
public string Name { get; set; }

View File

@ -7,7 +7,7 @@ import {
DropDownItem,
toastr
} from "asc-web-components";
import InviteDialog from './../../dialogs/Invite';
import { InviteDialog } from './../../dialogs';
import { withTranslation, I18nextProvider } from 'react-i18next';
import i18n from '../i18n';
import { typeUser, typeGuest, department } from './../../../helpers/customNames';

View File

@ -0,0 +1,56 @@
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/ChangeEmailDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
},
};
newInstance.init({
resources: resources,
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,178 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Button,
Text,
EmailInput,
FieldContainer
} from "asc-web-components";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import ModalDialogContainer from '../ModalDialogContainer';
import { api } from "asc-web-common";
const { sendInstructionsToChangeEmail } = api.people;
class ChangeEmailDialogComponent extends React.Component {
constructor(props) {
super(props);
const { user, language } = props;
const { email } = user;
this.state = {
isEmailValid: true,
isRequestRunning: false,
email,
hasError: false,
errorMessage: '',
emailErrors: []
};
i18n.changeLanguage(language);
}
componentDidMount() {
window.addEventListener("keyup", this.onKeyPress);
}
componentDidUpdate(prevProps) {
const { user } = this.props;
const { email } = user;
if (prevProps.user.email !== email) {
this.setState({ email });
}
}
componentWillUnmount() {
window.removeEventListener("keyup", this.onKeyPress);
}
onValidateEmailInput = result => this.setState({ isEmailValid: result.isValid, emailErrors: result.errors });
onChangeEmailInput = e => {
const { hasError } = this.state;
const email = e.target.value;
hasError && this.setState({ hasError: false });
this.setState({ email });
};
onSendEmailChangeInstructions = () => {
const { email } = this.state;
const { user } = this.props;
const { id } = user;
this.setState({ isRequestRunning: true }, () => {
sendInstructionsToChangeEmail(id, email)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(() => {
this.setState({ isRequestRunning: false }, () => this.props.onClose());
});
})
};
onValidateEmail = () => {
const { isEmailValid, email, emailErrors } = this.state;
const { t, user } = this.props;
if (isEmailValid) {
const sameEmailError = email.toLowerCase() === user.email.toLowerCase();
if (sameEmailError) {
this.setState({ errorMessage: t('SameEmail'), hasError: true });
}
else {
this.setState({ errorMessage: '', hasError: false });
this.onSendEmailChangeInstructions();
}
}
else {
const translatedErrors = emailErrors.map((errorKey => t(errorKey)));
const errorMessage = translatedErrors[0];
this.setState({ errorMessage, hasError: true });
}
};
onKeyPress = event => {
const { isRequestRunning } = this.state;
if (event.key === "Enter" && !isRequestRunning) {
this.onValidateEmail();
}
};
render() {
console.log("ChangeEmailDialog render");
const { t, visible, onClose } = this.props;
const { isRequestRunning, email, errorMessage, hasError } = this.state;
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
onClose={onClose}
headerContent={t('EmailChangeTitle')}
bodyContent={
<>
<FieldContainer
isVertical
labelText={t('EnterEmail')}
errorMessage={errorMessage}
hasError={hasError}
>
<EmailInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={email}
onChange={this.onChangeEmailInput}
onValidateInput={this.onValidateEmailInput}
onKeyUp={this.onKeyPress}
hasError={hasError}
/>
</FieldContainer>
<Text
className='text-dialog'
>
{t('EmailActivationDescription')}
</Text>
</>
}
footerContent={
<Button
key="SendBtn"
label={t('SendButton')}
size="medium"
primary={true}
onClick={this.onValidateEmail}
isLoading={isRequestRunning}
/>
}
/>
</ModalDialogContainer>
);
}
}
const ChangeEmailDialogTranslated = withTranslation()(ChangeEmailDialogComponent);
const ChangeEmailDialog = props => (
<ChangeEmailDialogTranslated i18n={i18n} {...props} />
);
ChangeEmailDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
user: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName,
};
}
export default connect(mapStateToProps, {})(withRouter(ChangeEmailDialog));

View File

@ -0,0 +1,19 @@
{
"EmailChangeTitle": "Email change",
"EnterEmail": "Enter a new email address",
"EmailActivationDescription": "The activation instructions will be sent to the entered email",
"SendButton": "Send",
"SameEmail": "Email is same",
"LocalDomain": "Local domains are not supported",
"IncorrectDomain": "Incorrect domain",
"DomainIpAddress": "Domains as ip address are not supported",
"PunycodeDomain": "Punycode domains are not supported",
"PunycodeLocalPart": "Punycode local part are not supported",
"IncorrectLocalPart": "Incorrect local part",
"SpacesInLocalPart": "Incorrect, local part contains spaces",
"MaxLengthExceeded": "The maximum total length of a user name or other local part is 64 characters.",
"IncorrectEmail": "Incorrect email",
"ManyEmails": "Too many email parsed",
"EmptyEmail": "No one email parsed"
}

View File

@ -0,0 +1,19 @@
{
"EmailChangeTitle": "Изменение адреса электронной почты",
"EnterEmail": "Введите новый адрес электронной почты",
"EmailActivationDescription": "Инструкции по активации будут отправлены на введённый адрес электронной почты",
"SendButton": "Отправить",
"SameEmail": "Email совпадает с текущим",
"LocalDomain": "Псевдодомен верхнего уровня не поддерживается",
"IncorrectDomain": "Некорректный домен",
"DomainIpAddress": "IP адрес в качестве домена не поддерживается",
"PunycodeDomain": "Домен содержит запрещенные символы",
"PunycodeLocalPart": "Имя почтового ящика содержит запрещенные символы",
"IncorrectLocalPart": "Некорректное имя почтового ящика",
"SpacesInLocalPart": "Имя почтового ящика содержит пробелы",
"MaxLengthExceeded": "Максимальная длина имени почтового ящика - 64 символа.",
"IncorrectEmail": "Некорректный email",
"ManyEmails": "Поле содержит больше одного email",
"EmptyEmail": "Поле email пустое"
}

View File

@ -0,0 +1,56 @@
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/ChangePasswordDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
},
};
newInstance.init({
resources: resources,
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,100 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Button,
Link,
Text
} from "asc-web-components";
import { withTranslation, Trans } from "react-i18next";
import i18n from "./i18n";
import { api } from "asc-web-common";
const { sendInstructionsToChangePassword } = api.people;
class ChangePasswordDialogComponent extends React.Component {
constructor(props) {
super(props);
const { language } = props;
this.state = {
isRequestRunning: false
};
i18n.changeLanguage(language);
}
onSendPasswordChangeInstructions = () => {
const { email, onClose } = this.props;
this.setState({ isRequestRunning: true }, () => {
sendInstructionsToChangePassword(email)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(() => {
this.setState({ isRequestRunning: false }, () => onClose());
});
})
}
render() {
console.log("ChangePasswordDialog render");
const { t, visible, email, onClose } = this.props;
const { isRequestRunning } = this.state;
return (
<ModalDialog
visible={visible}
onClose={onClose}
headerContent={t('PasswordChangeTitle')}
bodyContent={
<Text fontSize='13px'>
<Trans i18nKey="MessageSendPasswordChangeInstructionsOnEmail" i18n={i18n}>
Send the password change instructions to the
<Link type="page" href={`mailto:${email}`} isHovered title={email}>
{{ email }}
</Link>
email address
</Trans>
</Text>
}
footerContent={
<Button
key="SendBtn"
label={t('SendButton')}
size="medium"
primary={true}
onClick={this.onSendPasswordChangeInstructions}
isLoading={isRequestRunning}
/>
}
/>
);
}
}
const ChangePasswordDialogTranslated = withTranslation()(ChangePasswordDialogComponent);
const ChangePasswordDialog = props => (
<ChangePasswordDialogTranslated i18n={i18n} {...props} />
);
ChangePasswordDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
email: PropTypes.string.isRequired,
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName,
};
}
export default connect(mapStateToProps, {})(withRouter(ChangePasswordDialog));

View File

@ -0,0 +1,5 @@
{
"PasswordChangeTitle": "Password change",
"MessageSendPasswordChangeInstructionsOnEmail": "Send the password change instructions to the <1>{{email}}</1> email address",
"SendButton": "Send"
}

View File

@ -0,0 +1,5 @@
{
"PasswordChangeTitle": "Изменение пароля",
"MessageSendPasswordChangeInstructionsOnEmail": "Отправить инструкции по смене пароля на адрес <1>{{email}}</1>",
"SendButton": "Отправить"
}

View File

@ -0,0 +1,56 @@
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/ChangePhoneDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
},
};
newInstance.init({
resources: resources,
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,87 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Button,
Text
} from "asc-web-components";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import { api } from "asc-web-common";
class ChangePhoneDialogComponent extends React.Component {
constructor(props) {
super(props);
const { language } = props;
this.state = {
isRequestRunning: false
};
i18n.changeLanguage(language);
}
// TODO: add real api request for executing change phone
onChangePhone = () => {
const { onClose } = this.props;
this.setState({ isRequestRunning: true }, () => {
toastr.success("Context action: Change phone");
this.setState({ isRequestRunning: false }, () => onClose());
});
};
render() {
console.log("ChangePhoneDialog render");
const { t, visible, onClose } = this.props;
const { isRequestRunning } = this.state;
return (
<ModalDialog
visible={visible}
onClose={onClose}
headerContent={t('MobilePhoneChangeTitle')}
bodyContent={
<Text>
{t('MessageChangePhone')}
</Text>
}
footerContent={
<Button
key="SendBtn"
label={t('SendButton')}
size="medium"
primary={true}
onClick={this.onChangePhone}
isLoading={isRequestRunning}
/>
}
/>
);
}
}
const ChangePhoneDialogTranslated = withTranslation()(ChangePhoneDialogComponent);
const ChangePhoneDialog = props => (
<ChangePhoneDialogTranslated i18n={i18n} {...props} />
);
ChangePhoneDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName,
};
}
export default connect(mapStateToProps, {})(withRouter(ChangePhoneDialog));

View File

@ -0,0 +1,6 @@
{
"SendButton": "Send",
"MobilePhoneChangeTitle": "Change mobile phone",
"MessageChangePhone": "The instructions on how to change the user mobile number will be sent to the user email address"
}

View File

@ -0,0 +1,6 @@
{
"SendButton": "Отправить",
"MobilePhoneChangeTitle": "Изменение номера телефона",
"MessageChangePhone": "Инструкция по смене номера мобильного телефона будет отправлена на Вашу почту"
}

View File

@ -0,0 +1,56 @@
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/DeleteProfileEverDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
},
};
newInstance.init({
resources: resources,
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,127 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Button,
Text
} from "asc-web-components";
import { withTranslation, Trans } from "react-i18next";
import i18n from "./i18n";
import { api } from "asc-web-common";
import { typeUser } from "../../../helpers/customNames";
import { fetchPeople } from '../../../store/people/actions';
import ModalDialogContainer from '../ModalDialogContainer';
const { deleteUser } = api.people;
const { Filter } = api;
class DeleteProfileEverDialogComponent extends React.Component {
constructor(props) {
super(props);
const { language } = props;
this.state = {
isRequestRunning: false
};
i18n.changeLanguage(language);
}
onDeleteProfileEver = () => {
const { onClose, filter, fetchPeople, user, t } = this.props;
this.setState({ isRequestRunning: true }, () => {
deleteUser(user.id)
.then((res) => {
toastr.success(t('SuccessfullyDeleteUserInfoMessage'));
return fetchPeople(filter);
})
.catch((error) => toastr.error(error))
.finally(() => {
this.setState({ isRequestRunning: false }, () => onClose());
});
})
};
onReassignDataClick = () => {
const { history, settings, user } = this.props;
history.push(`${settings.homepage}/reassign/${user.userName}`)
};
render() {
console.log("DeleteProfileEverDialog render");
const { t, visible, user, onClose } = this.props;
const { isRequestRunning } = this.state;
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
onClose={onClose}
headerContent={t('Confirmation')}
bodyContent={
<>
<Text>
<Trans i18nKey='DeleteUserConfirmation' i18n={i18n}>
{{ typeUser }} <strong>{{ user: user.displayName }}</strong> will be deleted.
</Trans>
</Text>
<Text>{t('NotBeUndone')}</Text>
<Text color="#c30" fontSize="18px" className='warning-text'>
{t('Warning')}
</Text>
<Text>
{t('DeleteUserDataConfirmation')}
</Text>
</>
}
footerContent={
<>
<Button
key="OKBtn"
label={t('OKButton')}
size="medium"
primary={true}
onClick={this.onDeleteProfileEver}
isLoading={isRequestRunning}
/>
<Button
className='button-dialog'
key="ReassignBtn"
label={t('ReassignData')}
size="medium"
onClick={this.onReassignDataClick}
isDisabled={isRequestRunning}
/>
</>
}
/>
</ModalDialogContainer>
);
}
}
const DeleteProfileEverDialogTranslated = withTranslation()(DeleteProfileEverDialogComponent);
const DeleteProfileEverDialog = props => (
<DeleteProfileEverDialogTranslated i18n={i18n} {...props} />
);
DeleteProfileEverDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
user: PropTypes.object.isRequired,
filter: PropTypes.instanceOf(Filter).isRequired,
fetchPeople: PropTypes.func.isRequired,
settings: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName,
};
}
export default connect(mapStateToProps, { fetchPeople })(withRouter(DeleteProfileEverDialog));

View File

@ -0,0 +1,10 @@
{
"Confirmation": "Confirmation",
"DeleteUserConfirmation": "{{typeUser}} <strong>{{user}}</strong> will be deleted.",
"NotBeUndone": "Note: this action cannot be undone.",
"DeleteUserDataConfirmation": "User's personal documents that are available to others will be deleted. To avoid this, you must start the data reassign process before deleting.",
"OKButton": "OK",
"ReassignData": "Reassign data",
"Warning": "Warning!",
"SuccessfullyDeleteUserInfoMessage": "User has been successfully deleted"
}

View File

@ -0,0 +1,10 @@
{
"Confirmation": "Подтверждение",
"DeleteUserConfirmation": "{{typeUser}} <strong>{{user}}</strong> будет удален.",
"NotBeUndone": "Внимание: это действие необратимо.",
"DeleteUserDataConfirmation": "Будут удалены личные документы пользователя, доступные для других. Чтобы избежать этого, нужно перед удалением запустить процесс передачи данных.",
"OKButton": "OK",
"ReassignData": "Передать данные",
"Warning": "Внимание!",
"SuccessfullyDeleteUserInfoMessage": "Пользователь успешно удален"
}

View File

@ -0,0 +1,56 @@
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/DeleteSelfProfileDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
},
};
newInstance.init({
resources: resources,
lng: 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
react: {
useSuspense: true
}
});
}
export default newInstance;

View File

@ -0,0 +1,107 @@
import React from "react";
import { connect } from "react-redux";
import PropTypes from "prop-types";
import { withRouter } from "react-router";
import {
toastr,
ModalDialog,
Button,
Link,
Text
} from "asc-web-components";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import ModalDialogContainer from '../ModalDialogContainer';
import { api } from "asc-web-common";
const { sendInstructionsToDelete } = api.people;
class DeleteSelfProfileDialogComponent extends React.Component {
constructor(props) {
super(props);
const { language } = props;
this.state = {
isRequestRunning: false
};
i18n.changeLanguage(language);
}
onDeleteSelfProfileInstructions = () => {
const { onClose } = this.props;
this.setState({ isRequestRunning: true }, () => {
sendInstructionsToDelete()
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(() => {
this.setState({ isRequestRunning: false }, () => onClose());
});
})
}
render() {
console.log("DeleteSelfProfileDialog render");
const { t, visible, email, onClose } = this.props;
const { isRequestRunning } = this.state;
return (
<ModalDialogContainer>
<ModalDialog
visible={visible}
onClose={onClose}
headerContent={t('DeleteProfileTitle')}
bodyContent={
<Text fontSize='13px'>
{t('DeleteProfileInfo')} <Link type="page" href={`mailto:${email}`} isHovered title={email}>
{email}
</Link>
</Text>
}
footerContent={
<>
<Button
key="SendBtn"
label={t('SendButton')}
size="medium"
primary={true}
onClick={this.onDeleteSelfProfileInstructions}
isLoading={isRequestRunning}
/>
<Button
className='button-dialog'
key="CloseBtn"
label={t('CloseButton')}
size="medium"
onClick={onClose}
isDisabled={isRequestRunning}
/>
</>
}
/>
</ModalDialogContainer>
);
}
}
const DeleteSelfProfileDialogTranslated = withTranslation()(DeleteSelfProfileDialogComponent);
const DeleteSelfProfileDialog = props => (
<DeleteSelfProfileDialogTranslated i18n={i18n} {...props} />
);
DeleteSelfProfileDialog.propTypes = {
visible: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
email: PropTypes.string.isRequired,
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName,
};
}
export default connect(mapStateToProps, {})(withRouter(DeleteSelfProfileDialog));

View File

@ -0,0 +1,6 @@
{
"DeleteProfileTitle": "Delete profile dialog",
"DeleteProfileInfo": "Send the profile deletion instructions to the email address",
"SendButton": "Send",
"CloseButton": "Close"
}

View File

@ -0,0 +1,6 @@
{
"DeleteProfileTitle": "Диалог удаления профиля",
"DeleteProfileInfo": "Отправить инструкции по удалению профиля на адрес электронной почты",
"SendButton": "Отправить",
"CloseButton": "Закрыть"
}

View File

@ -23,7 +23,7 @@ if (process.env.NODE_ENV === "production") {
useSuspense: true
},
backend: {
loadPath: `${config.homepage}/locales/Invite/{{lng}}/{{ns}}.json`
loadPath: `${config.homepage}/locales/InviteDialog/{{lng}}/{{ns}}.json`
}
});
} else if (process.env.NODE_ENV === "development") {
@ -31,6 +31,9 @@ if (process.env.NODE_ENV === "production") {
const resources = {
en: {
translation: require("./locales/en/translation.json")
},
ru: {
translation: require("./locales/ru/translation.json")
}
};

View File

@ -11,46 +11,31 @@ import {
Textarea,
Text
} from "asc-web-components";
import { withTranslation, I18nextProvider } from "react-i18next";
import { withTranslation } from "react-i18next";
import i18n from "./i18n";
import { typeGuests } from "./../../../helpers/customNames";
import styled from "styled-components";
import ModalDialogContainer from '../ModalDialogContainer';
import copy from "copy-to-clipboard";
import { api } from "asc-web-common";
const { getShortenedLink } = api.portal;
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 {
class InviteDialogComponent extends React.Component {
constructor(props) {
super(props);
const { language, userInvitationLink, guestInvitationLink } = this.props;
this.state = {
isGuest: false,
userInvitationLink: this.props.userInvitationLink,
guestInvitationLink: this.props.guestInvitationLink,
userInvitationLink,
guestInvitationLink,
isLoading: false,
isLinkShort: false,
visible: false
};
i18n.changeLanguage(language);
}
onCopyLinkToClipboard = () => {
@ -94,7 +79,7 @@ class PureInviteDialog extends React.Component {
};
componentDidMount() {
this.onCopyLinkToClipboard();
this.onCopyLinkToClipboard();
}
onClickToCloseButton = () =>
@ -113,16 +98,16 @@ class PureInviteDialog extends React.Component {
headerContent={t("InviteLinkTitle")}
bodyContent={
<>
<Text className="margin-text" as="p">
<Text as="p">
{t("HelpAnswerLinkInviteSettings")}
</Text>
<Text className="margin-text" as="p">
<Text className="text-dialog" as="p">
{t("InviteLinkValidInterval", { count: 7 })}
</Text>
<div className="flex">
<div>
<Link
className="margin-link"
className="link-dialog"
type="action"
isHovered={true}
onClick={this.onCopyLinkToClipboard}
@ -147,7 +132,7 @@ class PureInviteDialog extends React.Component {
/>
</div>
<Textarea
className="margin-textarea"
className="textarea-dialog"
isReadOnly={true}
isDisabled={this.state.isLoading}
name={textAreaName}
@ -185,16 +170,15 @@ const mapStateToProps = state => {
return {
settings: state.auth.settings.hasShortenService,
userInvitationLink: state.portal.inviteLinks.userLink,
guestInvitationLink: state.portal.inviteLinks.guestLink
guestInvitationLink: state.portal.inviteLinks.guestLink,
language: state.auth.user.cultureName,
};
};
const InviteDialogContainer = withTranslation()(PureInviteDialog);
const InviteDialogTranslated = withTranslation()(InviteDialogComponent);
const InviteDialog = props => (
<I18nextProvider i18n={i18n}>
<InviteDialogContainer {...props} />
</I18nextProvider>
<InviteDialogTranslated i18n={i18n} {...props} />
);
InviteDialog.propTypes = {

View File

@ -0,0 +1,13 @@
{
"InviteLinkTitle": "Пригласительная ссылка",
"HelpAnswerLinkInviteSettings": "Поделитесь ссылкой, чтобы пригласить коллег на портал.",
"InviteLinkValidInterval": "Эта ссылка действительна только в течение {{ count }} дня.",
"CopyToClipboard": "Копировать ссылку",
"CloseButton": "Закрыть",
"LinkCopySuccess": "Ссылка скопирована в буфер обмена",
"GetShortenLink": "Получить сокращенную ссылку",
"InviteUsersAsCollaborators": "Добавить со статусом {{typeGuests, lowercase}}",
"LoadingProcessing": "Загрузка...",
"InviteLinkValidInterval_plural": "Эта ссылка действительна только в течение {{ count }} дней."
}

View File

@ -0,0 +1,44 @@
import styled from 'styled-components';
const ModalDialogContainer = styled.div`
.flex {
display: flex;
justify-content: space-between;
}
.text-dialog {
margin: 16px 0;
}
.input-dialog {
margin-top: 16px;
}
.button-dialog {
margin-left: 8px;
}
.warning-text {
margin: 20px 0;
}
.textarea-dialog {
margin-top: 12px;
}
.link-dialog {
margin-right: 12px;
}
.error-label {
position: absolute;
max-width: 100%;
}
.field-body {
position: relative;
}
`;
export default ModalDialogContainer;

View File

@ -0,0 +1,15 @@
import ChangeEmailDialog from "./ChangeEmailDialog";
import ChangePasswordDialog from "./ChangePasswordDialog";
import ChangePhoneDialog from "./ChangePhoneDialog";
import DeleteProfileEverDialog from "./DeleteProfileEverDialog";
import DeleteSelfProfileDialog from "./DeleteSelfProfileDialog";
import InviteDialog from './InviteDialog';
export {
ChangeEmailDialog,
ChangePasswordDialog,
ChangePhoneDialog,
DeleteProfileEverDialog,
DeleteSelfProfileDialog,
InviteDialog,
};

View File

@ -11,10 +11,7 @@ import {
Link,
RowContainer,
ModalDialog,
Button,
Text,
Label,
TextInput
} from "asc-web-components";
import UserContent from "./userContent";
import {
@ -34,8 +31,9 @@ import { isMobileOnly } from "react-device-detect";
import isEqual from "lodash/isEqual";
import { store, api, constants } from 'asc-web-common';
import i18n from '../../i18n';
import { ChangeEmailDialog, ChangePasswordDialog, DeleteSelfProfileDialog, DeleteProfileEverDialog } from '../../../../dialogs';
const { isAdmin, isMe } = store.auth.selectors;
const { resendUserInvites, sendInstructionsToDelete, sendInstructionsToChangePassword, deleteUser } = api.people;
const { resendUserInvites } = api.people;
const { EmployeeStatus } = constants;
@ -44,13 +42,19 @@ class SectionBodyContent extends React.PureComponent {
super(props);
this.state = {
newEmail: null,
dialog: {
visible: false,
header: "",
body: "",
buttons: []
}
buttons: [],
},
dialogsVisible: {
changeEmail: false,
changePassword: false,
deleteSelfProfile: false,
deleteProfileEver: false,
},
isEmailValid: false
};
}
@ -67,100 +71,21 @@ class SectionBodyContent extends React.PureComponent {
history.push(`${settings.homepage}/edit/${user.userName}`);
};
onChangePasswordClick = email => {
toggleChangePasswordDialog = (email) => {
const checkedEmail = typeof (email) === 'string' ? email : undefined;
this.setState({
dialog: {
visible: true,
header: "Password change",
body: (
<Text>
Send the password change instructions to the{" "}
<Link type="page" href={`mailto:${email}`} isHovered title={email}>
{email}
</Link>{" "}
email address
</Text>
),
buttons: [
<Button
key="OkBtn"
label="Send"
size="medium"
primary={true}
onClick={() => {
const { onLoading } = this.props;
onLoading(true);
sendInstructionsToChangePassword(email)
.then(() =>
toastr.success(
<Text>
The password change instructions have been sent to the{" "}
<b>{email}</b> email address
</Text>
)
)
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
}
dialogsVisible: { ...this.state.dialogsVisible, changePassword: !this.state.dialogsVisible.changePassword },
user: { email: checkedEmail }
});
};
onChangeEmailClick = email => {
toggleChangeEmailDialog = (user) => {
const checkedUser = user ? user : {};
this.setState({
dialog: {
visible: true,
header: "Email change",
body: (
<>
<Label htmlFor="new-email" text="Enter a new email address" />
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={this.state.newEmail}
onChange={e => {
this.setState({ newEmail: e.target.value });
}}
/>
<Text style={{ marginTop: "16px" }}>
The activation instructions will be sent to the entered email
</Text>
</>
),
buttons: [
<Button
key="OkBtn"
label="Send"
size="medium"
primary={true}
onClick={() => {
toastr.success(
`Context action: Change e-mail from ${email} to ${this.state.newEmail}`
);
this.onDialogClose();
}}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
dialogsVisible: { ...this.state.dialogsVisible, changeEmail: !this.state.dialogsVisible.changeEmail },
user: {
email: checkedUser.email,
id: checkedUser.id
}
});
};
@ -194,116 +119,23 @@ class SectionBodyContent extends React.PureComponent {
toastr.success("Context action: Delete personal data");
};
onDeleteProfileEverClick = user => {
toggleDeleteProfileEverDialog = user => {
const checkedUser = user ? user : {};
this.setState({
dialog: {
visible: true,
header: "Confirmation",
body: (
<>
<Text>
User <b>{user.displayName}</b> will be deleted.
</Text>
<Text>Note: this action cannot be undone.</Text>
<Text color="#c30" fontSize="18" style={{ margin: "20px 0" }}>
Warning!
</Text>
<Text>
User personal documents which are available to others will be
deleted. To avoid this, you must start the data reassign process
before deleting.
</Text>
</>
),
buttons: [
<Button
key="OkBtn"
label="OK"
size="medium"
primary={true}
onClick={() => {
const { onLoading, filter, fetchPeople } = this.props;
onLoading(true);
deleteUser(user.id)
.then(() => {
toastr.success("User has been removed successfully");
return fetchPeople(filter);
})
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
/>,
<Button
key="ReassignBtn"
label="Reassign data"
size="medium"
primary={true}
onClick={() => {
toastr.success("Context action: Reassign profile");
this.onDialogClose();
}}
style={{ marginLeft: "8px" }}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
dialogsVisible: { ...this.state.dialogsVisible, deleteProfileEver: !this.state.dialogsVisible.deleteProfileEver },
user: {
id: checkedUser.id,
displayName: checkedUser.displayName,
userName: checkedUser.userName,
}
});
};
onDeleteSelfProfileClick = email => {
toggleDeleteSelfProfileDialog = email => {
const checkedEmail = typeof (email) === 'string' ? email : undefined;
this.setState({
dialog: {
visible: true,
header: "Delete profile dialog",
body: (
<Text>
Send the profile deletion instructions to the email address{" "}
<Link type="page" href={`mailto:${email}`} isHovered title={email}>
{email}
</Link>
</Text>
),
buttons: [
<Button
key="OkBtn"
label="Send"
size="medium"
primary={true}
onClick={() => {
const { onLoading } = this.props;
onLoading(true);
sendInstructionsToDelete()
.then(() =>
toastr.success(
<Text>
Instructions to delete your profile has been sent to{" "}
<b>{email}</b> email address
</Text>
)
)
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
this.onDialogClose();
}}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
}
dialogsVisible: { ...this.state.dialogsVisible, deleteSelfProfile: !this.state.dialogsVisible.deleteSelfProfile },
user: { email: checkedEmail }
});
};
@ -347,11 +179,11 @@ class SectionBodyContent extends React.PureComponent {
onClick: this.onEmailSentClick.bind(this, user.email)
},
user.mobilePhone &&
isMobileOnly && {
key: "send-message",
label: t("LblSendMessage"),
onClick: this.onSendMessageClick.bind(this, user.mobilePhone)
},
isMobileOnly && {
key: "send-message",
label: t("LblSendMessage"),
onClick: this.onSendMessageClick.bind(this, user.mobilePhone)
},
{ key: "separator", isSeparator: true },
{
key: "edit",
@ -361,26 +193,26 @@ class SectionBodyContent extends React.PureComponent {
{
key: "change-password",
label: t("PasswordChangeButton"),
onClick: this.onChangePasswordClick.bind(this, user.email)
onClick: this.toggleChangePasswordDialog.bind(this, user.email)
},
{
key: "change-email",
label: t("EmailChangeButton"),
onClick: this.onChangeEmailClick.bind(this, user.email)
onClick: this.toggleChangeEmailDialog.bind(this, user)
},
isSelf
? viewer.isOwner
? {}
? viewer.isOwner
? {}
: {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteSelfProfileClick.bind(this, user.email)
onClick: this.toggleDeleteSelfProfileDialog.bind(this, user.email)
}
: {
key: "disable",
label: t("DisableUserButton"),
onClick: this.onDisableClick.bind(this, user)
}
key: "disable",
label: t("DisableUserButton"),
onClick: this.onDisableClick.bind(this, user)
}
];
case "disabled":
return [
@ -402,7 +234,7 @@ class SectionBodyContent extends React.PureComponent {
{
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteProfileEverClick.bind(this, user)
onClick: this.toggleDeleteProfileEverDialog.bind(this, user)
}
];
case "pending":
@ -418,21 +250,21 @@ class SectionBodyContent extends React.PureComponent {
onClick: this.onInviteAgainClick.bind(this, user)
},
!isSelf &&
(user.status === EmployeeStatus.Active
? {
key: "disable",
label: t("DisableUserButton"),
onClick: this.onDisableClick.bind(this, user)
}
: {
key: "enable",
label: t("EnableUserButton"),
onClick: this.onEnableClick.bind(this, user)
}),
(user.status === EmployeeStatus.Active
? {
key: "disable",
label: t("DisableUserButton"),
onClick: this.onDisableClick.bind(this, user)
}
: {
key: "enable",
label: t("EnableUserButton"),
onClick: this.onEnableClick.bind(this, user)
}),
isSelf && {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteSelfProfileClick.bind(this, user.email)
onClick: this.toggleDeleteSelfProfileDialog.bind(this, user.email)
}
];
default:
@ -456,9 +288,8 @@ class SectionBodyContent extends React.PureComponent {
};
onDialogClose = () => {
this.setState({
newEmail: null,
dialog: { visible: false }
this.setState({
dialog: { visible: false }
});
};
@ -477,8 +308,8 @@ class SectionBodyContent extends React.PureComponent {
render() {
console.log("Home SectionBodyContent render()");
const { users, viewer, selection, history, settings, t } = this.props;
const { dialog } = this.state;
const { users, viewer, selection, history, settings, t, filter } = this.props;
const { dialog, dialogsVisible, user } = this.state;
return users.length > 0 ? (
<>
@ -526,23 +357,57 @@ class SectionBodyContent extends React.PureComponent {
footerContent={dialog.buttons}
onClose={this.onDialogClose}
/>
{dialogsVisible.changeEmail &&
<ChangeEmailDialog
visible={dialogsVisible.changeEmail}
onClose={this.toggleChangeEmailDialog}
user={user}
/>
}
{dialogsVisible.changePassword &&
<ChangePasswordDialog
visible={dialogsVisible.changePassword}
onClose={this.toggleChangePasswordDialog}
email={user.email}
/>
}
{dialogsVisible.deleteSelfProfile &&
<DeleteSelfProfileDialog
visible={dialogsVisible.deleteSelfProfile}
onClose={this.toggleDeleteSelfProfileDialog}
email={user.email}
/>
}
{dialogsVisible.deleteProfileEver &&
<DeleteProfileEverDialog
visible={dialogsVisible.deleteProfileEver}
onClose={this.toggleDeleteProfileEverDialog}
user={user}
filter={filter}
settings={settings}
history={history}
/>
}
</>
) : (
<EmptyScreenContainer
imageSrc="images/empty_screen_filter.png"
imageAlt="Empty Screen Filter image"
headerText={t("NotFoundTitle")}
descriptionText={t("NotFoundDescription")}
buttons={
<>
<Icons.CrossIcon size="small" style={{ marginRight: "4px" }} />
<Link type="action" isHovered={true} onClick={this.onResetFilter}>
{t("ClearButton")}
</Link>
</>
}
/>
);
<EmptyScreenContainer
imageSrc="images/empty_screen_filter.png"
imageAlt="Empty Screen Filter image"
headerText={t("NotFoundTitle")}
descriptionText={t("NotFoundDescription")}
buttons={
<>
<Icons.CrossIcon size="small" style={{ marginRight: "4px" }} />
<Link type="action" isHovered={true} onClick={this.onResetFilter}>
{t("ClearButton")}
</Link>
</>
}
/>
);
}
}

View File

@ -2,14 +2,10 @@ import React from "react";
import { connect } from "react-redux";
import {
Text,
Link,
IconButton,
ContextMenuButton,
toastr,
utils,
TextInput,
Button,
ModalDialog,
AvatarEditor,
} from "asc-web-components";
import { Headline } from 'asc-web-common';
@ -20,22 +16,18 @@ import {
} from "../../../../../store/people/selectors";
import { withTranslation } from "react-i18next";
import {
updateUserStatus,
fetchPeople
updateUserStatus
} from "../../../../../store/people/actions";
import { fetchProfile, getUserPhoto } from "../../../../../store/profile/actions";
import styled from "styled-components";
import { store, api, constants } from "asc-web-common";
import { DeleteSelfProfileDialog, ChangePasswordDialog, ChangeEmailDialog, DeleteProfileEverDialog } from '../../../../dialogs';
const { isAdmin, isMe } = store.auth.selectors;
const {
resendUserInvites,
sendInstructionsToDelete,
sendInstructionsToChangePassword,
sendInstructionsToChangeEmail,
createThumbnailsAvatar,
loadAvatar,
deleteAvatar,
deleteUser
deleteAvatar
} = api.people;
const { EmployeeStatus } = constants;
@ -83,19 +75,18 @@ class SectionHeaderContent extends React.PureComponent {
const newState = {
profile: profile,
visibleAvatarEditor: false,
dialog: {
visible: false,
header: "",
body: "",
buttons: [],
newEmail: profile.email
},
avatar: {
tmpFile: "",
image: null,
defaultWidth: 0,
defaultHeight: 0
}
},
dialogsVisible: {
deleteSelfProfile: false,
changePassword: false,
changeEmail: false,
deleteProfileEver: false
},
};
return newState;
@ -198,96 +189,9 @@ class SectionHeaderContent extends React.PureComponent {
});
};
onEmailChange = event => {
const emailRegex = /.+@.+\..+/;
const newEmail =
(event && event.target.value) || this.state.dialog.newEmail;
const hasError = !emailRegex.test(newEmail);
toggleChangePasswordDialog = () => this.setState({ dialogsVisible: { ...this.state.dialogsVisible, changePassword: !this.state.dialogsVisible.changePassword } });
const dialog = {
visible: true,
header: "Change email",
body: (
<Text>
<span style={{ display: "block", marginBottom: "8px" }}>
The activation instructions will be sent to the entered email
</span>
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={newEmail}
onChange={this.onEmailChange}
hasError={hasError}
/>
</Text>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendEmailChangeInstructions}
isDisabled={hasError}
/>
],
newEmail: newEmail
};
this.setState({ dialog: dialog });
};
onSendEmailChangeInstructions = () => {
sendInstructionsToChangeEmail(
this.state.profile.id,
this.state.dialog.newEmail
)
.then(res => {
toastr.success(res);
})
.catch(error => toastr.error(error))
.finally(this.onDialogClose);
};
onPasswordChange = () => {
const dialog = {
visible: true,
header: "Change password",
body: (
<Text>
Send the password change instructions to the{" "}
<a href={`mailto:${this.state.profile.email}`}>
{this.state.profile.email}
</a>{" "}
email address
</Text>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendPasswordChangeInstructions}
/>
]
};
this.setState({ dialog: dialog });
};
onSendPasswordChangeInstructions = () => {
sendInstructionsToChangePassword(this.state.profile.email)
.then(res => {
toastr.success(res);
})
.catch(error => toastr.error(error))
.finally(this.onDialogClose);
};
onDialogClose = () => {
const dialog = { visible: false, newEmail: this.state.profile.email };
this.setState({ dialog: dialog });
};
toggleChangeEmailDialog = () => this.setState({ dialogsVisible: { ...this.state.dialogsVisible, changeEmail: !this.state.dialogsVisible.changeEmail } });
onEditClick = () => {
const { history, settings } = this.props;
@ -317,112 +221,14 @@ class SectionHeaderContent extends React.PureComponent {
toastr.success("Context action: Delete personal data");
};
onDeleteProfileClick = user => {
this.setState({
dialog: {
visible: true,
header: "Confirmation",
body: (
<>
<Text>
User <b>{user.displayName}</b> will be deleted.
</Text>
<Text>Note: this action cannot be undone.</Text>
<Text color="#c30" fontSize="18px" style={{ margin: "20px 0" }}>
Warning!
</Text>
<Text>
User personal documents which are available to others will be
deleted. To avoid this, you must start the data reassign process
before deleting.
</Text>
</>
),
buttons: [
<Button
key="OkBtn"
label="OK"
size="medium"
primary={true}
onClick={() => {
deleteUser(user.id)
.then(() => {
const { filter, fetchPeople } = this.props;
toastr.success("User has been removed successfully");
return fetchPeople(filter);
})
.catch(error => toastr.error(error));
this.onDialogClose();
}}
/>,
<Button
key="ReassignBtn"
label="Reassign data"
size="medium"
primary={true}
onClick={() => {
toastr.success("Context action: Reassign profile");
this.onDialogClose();
}}
style={{ marginLeft: "8px" }}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
}
});
};
toggleDeleteProfileEverDialog = () => this.setState({ dialogsVisible: { ...this.state.dialogsVisible, deleteProfileEver: !this.state.dialogsVisible.deleteProfileEver } });
onDeleteSelfProfileClick = email => {
this.setState({
dialog: {
visible: true,
header: "Delete profile dialog",
body: (
<Text>
Send the profile deletion instructions to the email address{" "}
<Link type="page" href={`mailto:${email}`} isHovered title={email}>
{email}
</Link>
</Text>
),
buttons: [
<Button
key="OkBtn"
label="Send"
size="medium"
primary={true}
onClick={() => {
sendInstructionsToDelete()
.then(() =>
toastr.success(
<Text>
Instructions to delete your profile has been sent to{" "}
<b>{email}</b> email address
</Text>
)
)
.catch(error => toastr.error(error));
this.onDialogClose();
}}
/>,
<Button
key="CancelBtn"
label="Cancel"
size="medium"
primary={false}
onClick={this.onDialogClose}
style={{ marginLeft: "8px" }}
/>
]
}
});
toggleDeleteSelfProfileDialog = () => {
this.setState(
{
dialogsVisible: { ...this.state.dialogsVisible, deleteSelfProfile: !this.state.dialogsVisible.deleteSelfProfile }
});
};
onInviteAgainClick = () => {
@ -458,12 +264,12 @@ class SectionHeaderContent extends React.PureComponent {
{
key: "change-password",
label: t("PasswordChangeButton"),
onClick: this.onPasswordChange
onClick: this.toggleChangePasswordDialog
},
{
key: "change-email",
label: t("EmailChangeButton"),
onClick: this.onEmailChange
onClick: this.toggleChangeEmailDialog
},
{
key: "edit-photo",
@ -476,7 +282,7 @@ class SectionHeaderContent extends React.PureComponent {
: {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteSelfProfileClick.bind(this, user.email)
onClick: this.toggleDeleteSelfProfileDialog
}
: {
key: "disable",
@ -509,7 +315,7 @@ class SectionHeaderContent extends React.PureComponent {
{
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteProfileClick.bind(this, user)
onClick: this.toggleDeleteProfileEverDialog
}
];
case "pending":
@ -544,7 +350,7 @@ class SectionHeaderContent extends React.PureComponent {
isMe(user, viewer.userName) && {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: this.onDeleteSelfProfileClick.bind(this, user.email)
onClick: this.toggleDeleteSelfProfileDialog
}
];
default:
@ -557,9 +363,8 @@ class SectionHeaderContent extends React.PureComponent {
};
render() {
const { profile, isAdmin, viewer, t } = this.props;
const { dialog, avatar, visibleAvatarEditor } = this.state;
const { profile, isAdmin, viewer, t, filter, settings, history } = this.props;
const { avatar, visibleAvatarEditor, dialogsVisible } = this.state;
const contextOptions = () => this.getUserContextOptions(profile, viewer);
return (
@ -588,13 +393,7 @@ class SectionHeaderContent extends React.PureComponent {
isDisabled={false}
/>
)}
<ModalDialog
visible={dialog.visible}
headerContent={dialog.header}
bodyContent={dialog.body}
footerContent={dialog.buttons}
onClose={this.onDialogClose}
/>
<AvatarEditor
image={avatar.image}
visible={visibleAvatarEditor}
@ -607,6 +406,41 @@ class SectionHeaderContent extends React.PureComponent {
maxSizeFileError={t("maxSizeFileError")}
unknownError={t("unknownError")}
/>
{dialogsVisible.deleteSelfProfile &&
<DeleteSelfProfileDialog
visible={dialogsVisible.deleteSelfProfile}
onClose={this.toggleDeleteSelfProfileDialog}
email={this.state.profile.email}
/>
}
{dialogsVisible.changePassword &&
<ChangePasswordDialog
visible={dialogsVisible.changePassword}
onClose={this.toggleChangePasswordDialog}
email={this.state.profile.email}
/>
}
{dialogsVisible.changeEmail &&
<ChangeEmailDialog
visible={dialogsVisible.changeEmail}
onClose={this.toggleChangeEmailDialog}
user={this.state.profile}
/>
}
{dialogsVisible.deleteProfileEver &&
<DeleteProfileEverDialog
visible={dialogsVisible.deleteProfileEver}
onClose={this.toggleDeleteProfileEverDialog}
user={this.state.profile}
filter={filter}
settings={settings}
history={history}
/>
}
</StyledContainer>
);
}
@ -624,5 +458,5 @@ const mapStateToProps = state => {
export default connect(
mapStateToProps,
{ updateUserStatus, fetchProfile, fetchPeople }
{ updateUserStatus, fetchProfile }
)(withRouter(withTranslation()(SectionHeaderContent)));

View File

@ -49,6 +49,7 @@ class EmailField extends React.Component {
autoComplete='email'
isDisabled={inputIsDisabled}
onValidateInput={onValidateInput}
hasError={hasError}
/>
</FieldContainer>
);

View File

@ -33,7 +33,8 @@ class TextChangeField extends React.Component {
buttonTabIndex,
tooltipContent,
helpButtonHeaderContent
helpButtonHeaderContent,
dataDialog
} = this.props;
return (
@ -59,6 +60,7 @@ class TextChangeField extends React.Component {
size="medium"
style={{ marginLeft: "8px" }}
tabIndex={buttonTabIndex}
data-dialog={dataDialog}
/>
</InputContainer>
</FieldContainer>

View File

@ -202,7 +202,7 @@ class CreateUserForm extends React.Component {
const errors = {
firstName: !profile.firstName.trim(),
lastName: !profile.lastName.trim(),
email: stateErrors.email,
email: stateErrors.email || !profile.email.trim(),
password: profile.passwordType === "temp" && !profile.password.trim()
};
const hasError = errors.firstName || errors.lastName || errors.email || errors.password;
@ -315,7 +315,7 @@ class CreateUserForm extends React.Component {
this.setState(stateCopy)
}
onValidateEmailField = (value) => this.setState({errors: { ...this.state.errors, email:!value }});
onValidateEmailField = (value) => this.setState({errors: { ...this.state.errors, email:!value.isValid }});
render() {
const { isLoading, errors, profile, selector } = this.state;

View File

@ -1,7 +1,7 @@
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { Avatar, Button, Textarea, Text, toastr, ModalDialog, TextInput, AvatarEditor, Link } from 'asc-web-components'
import { Avatar, Button, Textarea, Text, toastr, AvatarEditor, Link } from 'asc-web-components'
import { withTranslation, Trans } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts, mapGroupsToGroupSelectorOptions, mapGroupSelectorOptionsToGroups, filterGroupSelectorOptions } from "../../../../../store/people/selectors";
import { updateProfile, getUserPhoto } from '../../../../../store/profile/actions'
@ -16,7 +16,14 @@ import InfoFieldContainer from './FormFields/InfoFieldContainer'
import { departments, department, position, employedSinceDate, typeGuest, typeUser } from '../../../../../helpers/customNames';
import styled from "styled-components";
import { api } from "asc-web-common";
const {sendInstructionsToChangePassword, sendInstructionsToChangeEmail, createThumbnailsAvatar, loadAvatar, deleteAvatar} = api.people;
import { ChangeEmailDialog, ChangePasswordDialog, ChangePhoneDialog } from '../../../../dialogs';
const { createThumbnailsAvatar, loadAvatar, deleteAvatar } = api.people;
const dialogsDataset = {
changeEmail: 'changeEmail',
changePassword: 'changePassword',
changePhone: 'changePhone'
};
const Table = styled.table`
width: 100%;
@ -45,14 +52,6 @@ class UpdateUserForm extends React.Component {
this.onWorkFromDateChange = this.onWorkFromDateChange.bind(this);
this.onCancel = this.onCancel.bind(this);
this.onEmailChange = this.onEmailChange.bind(this);
this.onSendEmailChangeInstructions = this.onSendEmailChangeInstructions.bind(this);
this.onPasswordChange = this.onPasswordChange.bind(this);
this.onSendPasswordChangeInstructions = this.onSendPasswordChangeInstructions.bind(this);
this.onPhoneChange = this.onPhoneChange.bind(this);
this.onSendPhoneChangeInstructions = this.onSendPhoneChangeInstructions.bind(this);
this.onDialogClose = this.onDialogClose.bind(this);
this.onContactsItemAdd = this.onContactsItemAdd.bind(this);
this.onContactsItemTypeChange = this.onContactsItemTypeChange.bind(this);
this.onContactsItemTextChange = this.onContactsItemTextChange.bind(this);
@ -81,10 +80,10 @@ class UpdateUserForm extends React.Component {
mapPropsToState = (props) => {
var profile = toEmployeeWrapper(props.profile);
var allOptions = mapGroupsToGroupSelectorOptions(props.groups);
var selected = mapGroupsToGroupSelectorOptions(profile.groups);
var selected = mapGroupsToGroupSelectorOptions(profile.groups);
getUserPhoto(profile.id).then(userPhotoData => {
if(userPhotoData.original){
if (userPhotoData.original) {
let avatarDefaultSizes = /_(\d*)-(\d*)./g.exec(userPhotoData.original);
if (avatarDefaultSizes !== null && avatarDefaultSizes.length > 2) {
this.setState({
@ -107,13 +106,6 @@ class UpdateUserForm extends React.Component {
},
profile: profile,
visibleAvatarEditor: false,
dialog: {
visible: false,
header: "",
body: "",
buttons: [],
newEmail: profile.email,
},
selector: {
visible: false,
allOptions: allOptions,
@ -121,10 +113,16 @@ class UpdateUserForm extends React.Component {
selected: selected
},
avatar: {
tmpFile:"",
tmpFile: "",
image: null,
defaultWidth: 0,
defaultHeight: 0
},
dialogsVisible: {
[dialogsDataset.changePassword]: false,
[dialogsDataset.changePhone]: false,
[dialogsDataset.changeEmail]: false,
currentDialog: ''
}
};
@ -144,6 +142,20 @@ class UpdateUserForm extends React.Component {
this.setState(stateCopy)
}
toggleDialogsVisible = (e) => {
const stateCopy = Object.assign({}, {}, this.state.dialogsVisible);
const selectedDialog = e ? e.target.dataset.dialog : e;
if (selectedDialog) {
stateCopy[selectedDialog] = true;
stateCopy.currentDialog = selectedDialog;
}
else {
stateCopy[stateCopy.currentDialog] = false;
stateCopy.currentDialog = '';
}
this.setState({ dialogsVisible: stateCopy });
};
onUserTypeChange(event) {
var stateCopy = Object.assign({}, this.state);
stateCopy.profile.isVisitor = event.target.value === "true";
@ -181,10 +193,10 @@ class UpdateUserForm extends React.Component {
}
handleSubmit() {
if(!this.validate())
if (!this.validate())
return false;
this.setState({isLoading: true});
this.setState({ isLoading: true });
this.props.updateProfile(this.state.profile)
.then((profile) => {
@ -193,7 +205,7 @@ class UpdateUserForm extends React.Component {
})
.catch((error) => {
toastr.error(error);
this.setState({isLoading: false});
this.setState({ isLoading: false });
});
}
@ -201,114 +213,6 @@ class UpdateUserForm extends React.Component {
this.props.history.goBack();
}
onEmailChange(event) {
const emailRegex = /.+@.+\..+/;
const newEmail = event.target.value || this.state.dialog.newEmail;
const hasError = !emailRegex.test(newEmail);
const dialog = {
visible: true,
header: "Change email",
body: (
<Text>
<span style={{display: "block", marginBottom: "8px"}}>The activation instructions will be sent to the entered email</span>
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={newEmail}
onChange={this.onEmailChange}
hasError={hasError}
/>
</Text>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendEmailChangeInstructions}
isDisabled={hasError}
/>
],
newEmail: newEmail
};
this.setState({ dialog: dialog })
}
onSendEmailChangeInstructions() {
sendInstructionsToChangeEmail(this.state.profile.id, this.state.dialog.newEmail)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(this.onDialogClose);
}
onPasswordChange() {
const dialog = {
visible: true,
header: "Change password",
body: (
<Text>
Send the password change instructions to the <a href={`mailto:${this.state.profile.email}`}>{this.state.profile.email}</a> email address
</Text>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendPasswordChangeInstructions}
/>
]
};
this.setState({ dialog: dialog })
}
onSendPasswordChangeInstructions() {
sendInstructionsToChangePassword(this.state.profile.email)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(this.onDialogClose);
}
onPhoneChange() {
const dialog = {
visible: true,
header: "Change phone",
body: (
<Text>
The instructions on how to change the user mobile number will be sent to the user email address
</Text>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={this.onSendPhoneChangeInstructions}
/>
]
};
this.setState({ dialog: dialog })
}
onSendPhoneChangeInstructions() {
toastr.success("Context action: Change phone");
this.onDialogClose();
}
onDialogClose() {
const dialog = { visible: false, newEmail: this.state.profile.email };
this.setState({ dialog: dialog })
}
onContactsItemAdd(item) {
var stateCopy = Object.assign({}, this.state);
stateCopy.profile.contacts.push({
@ -360,7 +264,7 @@ class UpdateUserForm extends React.Component {
defaultWidth: avatarDefaultSizes[1],
defaultHeight: avatarDefaultSizes[2]
}
})
})
}
this.setState({
visibleAvatarEditor: true,
@ -376,14 +280,14 @@ class UpdateUserForm extends React.Component {
.then((response) => {
var img = new Image();
img.onload = function () {
var stateCopy = Object.assign({}, _this.state);
stateCopy.avatar = {
tmpFile: response.data,
image: response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
_this.setState(stateCopy);
var stateCopy = Object.assign({}, _this.state);
stateCopy.avatar = {
tmpFile: response.data,
image: response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
_this.setState(stateCopy);
};
img.src = response.data;
})
@ -391,33 +295,33 @@ class UpdateUserForm extends React.Component {
}
onSaveAvatar(isUpdate, result) {
if(isUpdate){
if (isUpdate) {
createThumbnailsAvatar(this.state.profile.id, {
x: Math.round(result.x*this.state.avatar.defaultWidth - result.width/2),
y: Math.round(result.y*this.state.avatar.defaultHeight - result.height/2),
x: Math.round(result.x * this.state.avatar.defaultWidth - result.width / 2),
y: Math.round(result.y * this.state.avatar.defaultHeight - result.height / 2),
width: result.width,
height: result.height,
tmpFile: this.state.avatar.tmpFile
})
.then((response) => {
.then((response) => {
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.avatar.tmpFile = '';
stateCopy.profile.avatarMax = response.max + '?_='+Math.floor(Math.random() * Math.floor(10000));
stateCopy.profile.avatarMax = response.max + '?_=' + Math.floor(Math.random() * Math.floor(10000));
toastr.success(this.props.t("ChangesSavedSuccessfully"));
this.setState(stateCopy);
})
.catch((error) => toastr.error(error));
}else{
})
.catch((error) => toastr.error(error));
} else {
deleteAvatar(this.state.profile.id)
.then((response) => {
.then((response) => {
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.profile.avatarMax = response.big;
toastr.success(this.props.t("ChangesSavedSuccessfully"));
this.setState(stateCopy);
})
.catch((error) => toastr.error(error));
})
.catch((error) => toastr.error(error));
}
}
@ -461,17 +365,17 @@ class UpdateUserForm extends React.Component {
}
render() {
const { isLoading, errors, profile, dialog, selector } = this.state;
const { isLoading, errors, profile, selector, dialogsVisible } = this.state;
const { t, i18n } = this.props;
const pattern = getUserContactsPattern();
const contacts = getUserContacts(profile.contacts);
const tooltipTypeContent =
const tooltipTypeContent =
<>
<Text
style={{paddingBottom: 17}}
<Text
style={{ paddingBottom: 17 }}
fontSize='13px'>
{t("ProfileTypePopupHelper")}
{t("ProfileTypePopupHelper")}
</Text>
<Text fontSize='12px' as="div">
@ -486,8 +390,8 @@ class UpdateUserForm extends React.Component {
<Th>
<Text isBold fontSize='13px'>
{t("Employee")}
</Text>
</Th>
</Text>
</Th>
<Th>
<Text isBold fontSize='13px'>
{t("GuestCaption")}
@ -528,16 +432,16 @@ class UpdateUserForm extends React.Component {
<Td>{t("Calendar")}</Td>
<Td>review</Td>
<Td>review</Td>
</tr>
</tr>
</tbody>
</Table>
</Text>
<Link
color="#316DAA"
isHovered={true}
href="https://helpcenter.onlyoffice.com/ru/gettingstarted/people.aspx#ManagingAccessRights_block"
style={{marginTop: 23}}>
{t("TermsOfUsePopupHelperLink")}
<Link
color="#316DAA"
isHovered={true}
href="https://helpcenter.onlyoffice.com/ru/gettingstarted/people.aspx#ManagingAccessRights_block"
style={{ marginTop: 23 }}>
{t("TermsOfUsePopupHelperLink")}
</Link>
</>;
@ -561,10 +465,10 @@ class UpdateUserForm extends React.Component {
onSave={this.onSaveAvatar}
onLoadFile={this.onLoadFileAvatar}
headerLabel={t("editAvatar")}
chooseFileLabel ={t("chooseFileLabel")}
chooseFileLabel={t("chooseFileLabel")}
unknownTypeError={t("unknownTypeError")}
maxSizeFileError={t("maxSizeFileError")}
unknownError ={t("unknownError")}
unknownError={t("unknownError")}
/>
</AvatarContainer>
<MainFieldsContainer ref={this.mainFieldsContainerRef}>
@ -574,7 +478,7 @@ class UpdateUserForm extends React.Component {
inputValue={profile.email}
buttonText={t("ChangeButton")}
buttonIsDisabled={isLoading}
buttonOnClick={this.onEmailChange}
buttonOnClick={this.toggleDialogsVisible}
buttonTabIndex={1}
helpButtonHeaderContent={t("Mail")}
@ -582,7 +486,7 @@ class UpdateUserForm extends React.Component {
<Text fontSize='13px' as="div">
<Trans i18nKey="EmailPopupHelper" i18n={i18n}>
The main e-mail is needed to restore access to the portal in case of loss of the password and send notifications.
<p style={{margin: "1rem 0"/*, height: "0", visibility: "hidden"*/}}>
<p style={{ margin: "1rem 0"/*, height: "0", visibility: "hidden"*/ }}>
You can create a new mail on the domain as the primary.
In this case, you must set a one-time password so that the user can log in to the portal for the first time.
</p>
@ -590,6 +494,7 @@ class UpdateUserForm extends React.Component {
</Trans>
</Text>
}
dataDialog={dialogsDataset.changeEmail}
/>
<TextChangeField
labelText={`${t("Password")}:`}
@ -597,8 +502,9 @@ class UpdateUserForm extends React.Component {
inputValue={profile.password}
buttonText={t("ChangeButton")}
buttonIsDisabled={isLoading}
buttonOnClick={this.onPasswordChange}
buttonOnClick={this.toggleDialogsVisible}
buttonTabIndex={2}
dataDialog={dialogsDataset.changePassword}
/>
<TextChangeField
labelText={`${t("Phone")}:`}
@ -606,8 +512,9 @@ class UpdateUserForm extends React.Component {
inputValue={profile.phone}
buttonText={t("ChangeButton")}
buttonIsDisabled={isLoading}
buttonOnClick={this.onPhoneChange}
buttonOnClick={this.toggleDialogsVisible}
buttonTabIndex={3}
dataDialog={dialogsDataset.changePhone}
/>
<TextField
isRequired={true}
@ -644,8 +551,8 @@ class UpdateUserForm extends React.Component {
radioName="sex"
radioValue={profile.sex}
radioOptions={[
{ value: 'male', label: t("SexMale")},
{ value: 'female', label: t("SexFemale")}
{ value: 'male', label: t("SexMale") },
{ value: 'female', label: t("SexFemale") }
]}
radioIsDisabled={isLoading}
radioOnChange={this.onInputChange}
@ -655,8 +562,8 @@ class UpdateUserForm extends React.Component {
radioName="isVisitor"
radioValue={profile.isVisitor.toString()}
radioOptions={[
{ value: "true", label: t("CustomTypeGuest", { typeGuest })},
{ value: "false", label: t("CustomTypeUser", { typeUser })}
{ value: "true", label: t("CustomTypeGuest", { typeGuest }) },
{ value: "false", label: t("CustomTypeUser", { typeUser }) }
]}
radioIsDisabled={isLoading}
radioOnChange={this.onUserTypeChange}
@ -735,16 +642,33 @@ class UpdateUserForm extends React.Component {
/>
</InfoFieldContainer>
<div>
<Button label={t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={isLoading} size="big" tabIndex={11}/>
<Button label={t("CancelButton")} onClick={this.onCancel} isDisabled={isLoading} size="big" style={{ marginLeft: "8px" }} tabIndex={12}/>
<Button label={t("SaveButton")} onClick={this.handleSubmit} primary isDisabled={isLoading} size="big" tabIndex={11} />
<Button label={t("CancelButton")} onClick={this.onCancel} isDisabled={isLoading} size="big" style={{ marginLeft: "8px" }} tabIndex={12} />
</div>
<ModalDialog
visible={dialog.visible}
headerContent={dialog.header}
bodyContent={dialog.body}
footerContent={dialog.buttons}
onClose={this.onDialogClose}
/>
{dialogsVisible.changeEmail &&
<ChangeEmailDialog
visible={dialogsVisible.changeEmail}
onClose={this.toggleDialogsVisible}
user={profile}
/>
}
{dialogsVisible.changePassword &&
<ChangePasswordDialog
visible={dialogsVisible.changePassword}
onClose={this.toggleDialogsVisible}
email={profile.email}
/>
}
{dialogsVisible.changePhone &&
<ChangePhoneDialog
visible={dialogsVisible.changePhone}
onClose={this.toggleDialogsVisible}
user={profile}
/>
}
</>
);
};

View File

@ -10,7 +10,7 @@
]
},
"dialogs": {
"Invite": {
"InviteDialog": {
"Resource": [
"HelpAnswerLinkInviteSettings",
"CopyToClipboard",
@ -23,6 +23,49 @@
"ResourceJS": [
"LinkCopySuccess"
]
},
"ChangeEmailDialog":{
"Resource": [
"EmailChangeTitle",
"EnterEmail",
"EmailActivationDescription",
"SendButton"
]
},
"DeleteSelfProfileDialog":{
"Resource": [
"DeleteProfileTitle",
"DeleteProfileInfo",
"CloseButton",
"SendButton"
]
},
"ChangePhoneDialog":{
"Resource": [
"SendButton",
"MobilePhoneChangeTitle"
]
},
"DeleteProfileEverDialog":{
"Resource": [
"Confirmation",
"DeleteUserConfirmation",
"Warning",
"DeleteUserDataConfirmation"
],
"PeopleResource": [
"SuccessfullyDeleteUserInfoMessage"
],
"UserControlsCommonResource":[
"NotBeUndone"
]
},
"ChangePasswordDialog":{
"Resource": [
"PasswordChangeTitle",
"MessageSendPasswordChangeInstructionsOnEmail",
"SendButton"
]
}
},
"pages": {

View File

@ -1384,21 +1384,7 @@ namespace ASC.Employee.Core.Controllers
PermissionContext.DemandPermissions(new UserSecurityProvider(user.ID), Constants.Action_EditUser);
if (contacts == null) return;
if (user.ContactsList == null)
{
user.ContactsList = new List<string>();
}
else
{
user.ContactsList.Clear();
}
foreach (var contact in contacts.Where(c => !string.IsNullOrEmpty(c.Value)))
{
user.ContactsList.Add(contact.Type);
user.ContactsList.Add(contact.Value);
}
user.Contacts = contacts.Select(r => $"{r.Type}|{r.Value}").Aggregate((a, b) => $"{a}|{b}");
}
private void DeleteContacts(IEnumerable<Contact> contacts, UserInfo user)

View File

@ -186,7 +186,7 @@ class Confirm extends React.PureComponent {
this.state.errorText && this.setState({ errorText: "" });;
}
onValidateEmail = value => this.setState({emailValid: value });
onValidateEmail = value => this.setState({emailValid: value.isValid });
onChangePassword = event => {
this.setState({ password: event.target.value });

View File

@ -2473,7 +2473,7 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-common@file:../../packages/asc-web-common":
version "1.0.20"
version "1.0.24"
dependencies:
axios "^0.19.0"
faker "^4.1.0"
@ -2482,7 +2482,7 @@ asap@~2.0.6:
react-window-infinite-loader "^1.0.5"
"asc-web-components@file:../../packages/asc-web-components":
version "1.0.260"
version "1.0.264"
dependencies:
email-addresses "^3.1.0"
html-to-react "^1.4.2"

View File

@ -4,7 +4,6 @@ import styled from "styled-components";
import { Scrollbar } from "asc-web-components";
const StyledArticleBody = styled.div`
margin: 16px 0;
${props => props.displayBorder && `outline: 1px dotted;`}
flex-grow: 1;
height: 100%;
@ -14,13 +13,19 @@ const StyledArticleBody = styled.div`
}
`;
const StyledArticleWrapper = styled.div`
margin: 16px 0;
`;
const ArticleBody = React.memo(props => {
//console.log("PageLayout ArticleBody render");
const { children } = props;
return (
<StyledArticleBody>
<Scrollbar>{children}</Scrollbar>
<Scrollbar>
<StyledArticleWrapper>{children}</StyledArticleWrapper>
</Scrollbar>
</StyledArticleBody>
);
});

View File

@ -5,10 +5,13 @@ import { utils, Scrollbar } from "asc-web-components";
const { tablet } = utils.device;
const StyledSectionBody = styled.div`
margin: 16px 8px 16px 0;
${props => props.displayBorder && `outline: 1px dotted;`}
flex-grow: 1;
height: 100%;
`;
const StyledSectionWrapper = styled.div`
margin: 16px 8px 16px 0;
@media ${tablet} {
margin: 16px 0;
@ -32,14 +35,16 @@ const SectionBody = React.memo(props => {
<StyledSectionBody>
{withScroll ? (
<Scrollbar stype="mediumBlack">
{children}
<StyledSpacer pinned={pinned}/>
<StyledSectionWrapper>
{children}
<StyledSpacer pinned={pinned}/>
</StyledSectionWrapper>
</Scrollbar>
) : (
<>
<StyledSectionWrapper>
{children}
<StyledSpacer pinned={pinned}/>
</>
</StyledSectionWrapper>
)}
</StyledSectionBody>
);

View File

@ -2841,7 +2841,7 @@ asap@~2.0.3:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../packages/asc-web-components":
version "1.0.260"
version "1.0.264"
dependencies:
email-addresses "^3.1.0"
html-to-react "^1.4.2"
@ -10010,9 +10010,9 @@ rc-tree@^2.1.3:
warning "^4.0.3"
rc-util@^4.15.3, rc-util@^4.5.1:
version "4.17.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.17.0.tgz#1991a3194ac5c88819e57fa924a8ef4b42544a37"
integrity sha512-nMtU4tmPhhpRkTWHGkbEYQpuqRMAws/OUS4aqumJkRIOEj6gA6LkmjJZkDLyLgmLpzHM4VAIQhodIYu+Z+yylg==
version "4.18.0"
resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-4.18.0.tgz#b014712709ea2f777e5c207dca23ecd211cf73dd"
integrity sha512-hftkePUmXu2AeaYQRqMdEJ+bkqNRKZEk59pUmqMKD68+69Csc1xeIc74P73leuZEYij23yG4OMnutejVjc8Jdg==
dependencies:
add-dom-event-listener "^1.1.0"
babel-runtime "6.x"

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-components",
"version": "1.0.265",
"version": "1.0.266",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",

View File

@ -5,17 +5,23 @@ Email entry field with advanced capabilities for validation based on settings
### Usage
```js
import { EmailInput } from "asc-web-components";
import { EmailInput, utils } from "asc-web-components";
const { EmailSettings } = utils.email;
const settings = new EmailSettings();
settings.allowDomainPunycode = true;
```
```jsx
<EmailInput
name="email"
placeholder="email"
onValidateInput={isValidEmail =>
console.log("isValidEmail = ", isValidEmail);
}
emailSettings={settings}
onValidateInput={result =>
console.log("onValidateInput", result.value, result.isValid, result.errors);
}
/>;
```
@ -23,22 +29,22 @@ import { EmailInput } from "asc-web-components";
You can apply all properties of the `TextInput` component to the component
| Props | Type | Required | Values | Default | Description |
| -------------------- | :-----------------------------------: | :------: | :----: | :-------------: | ------------------------------------------------------------------------ |
| `className` | `string` | - | - | - | Accepts class |
| `customValidateFunc` | `func` | - | - | - | Function for your custom validation input value |
| `emailSettings` | `Object`, `Instance of EmailSettings` | - | - | `EmailSettings` | Settings for validating email |
| `id` | `string` | - | - | - | Accepts id |
| `isValid` | `bool` | - | - | - | Used in your custom validation function for change border-color of input |
| `onChange` | `func` | - | - | - | Function for your custom handling changes in input |
| `onValidateInput` | `func` | - | - | - | Will be validate our value, return boolean validation result |
| `style` | `obj`, `array` | - | - | - | Accepts css style |
| Props | Type | Required | Values | Default | Description |
| -------------------- | :-----------------------------------: | :------: | :-----------------------------: | :-------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `className` | `string` | - | - | - | Accepts class |
| `customValidate` | `func` | - | - | - | Function for your custom validation input value. Function must return object with following parameters: `value`: string value of input, `isValid`: boolean result of validating, `errors`(optional): array of errors |
| `emailSettings` | `Object`, `EmailSettings` | - | - | { allowDomainPunycode: false, allowLocalPartPunycode: false, allowDomainIp: false, allowStrictLocalPart: true, allowSpaces: false, allowName: false, allowLocalDomainName: false } | Settings for validating email |
| `hasError` | `bool` | - | - | - | Used in your custom validation |
| `id` | `string` | - | - | - | Accepts id |
| `onChange` | `func` | - | - | - | Function for your custom handling changes in input |
| `onValidateInput` | `func` | - | { isValid: bool, errors: array} | - | Will be validate our value, return object with following parameters: `isValid`: boolean result of validating, `errors`: array of errors |
| `style` | `obj`, `array` | - | - | - | Accepts css style |
### Validate email
Our validation algorithm based on [An RFC 5322 email address parser](https://www.npmjs.com/package/email-addresses).
Our validation algorithm based on [RFC 5322 email address parser](https://www.npmjs.com/package/email-addresses).
For email validating you should use plain Object or our email utility with following settings:
For email validating you should use plain Object or EmailSettings with following settings:
| Props | Type | Required | Default | Description |
| ------------------------ | :----: | :------: | :-----: | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
@ -74,7 +80,7 @@ const { EmailSettings } = utils.email;
const emailSettings = new EmailSettings();
emailSettings.getSettings(); /* returned Object with default settings:
emailSettings.toObject(); /* returned Object with default settings:
{
allowDomainPunycode: false,
allowLocalPartPunycode: false,
@ -87,7 +93,7 @@ emailSettings.getSettings(); /* returned Object with default settings:
*/
email.allowName = true; // set allowName setting to true
emailSettings.getSettings(); /* returned Object with NEW settings:
emailSettings.toObject(); /* returned Object with NEW settings:
{
allowDomainPunycode: false,
@ -104,62 +110,48 @@ emailSettings.getSettings(); /* returned Object with NEW settings:
### Custom validate email
You should use custom validation with the `customValidateFunc` prop. Also you can change state of validation with the help of `isValid` prop.
`isValid` prop allow you to change border-color of input.
You should use custom validation with the `customValidate` prop. This prop contains function for your custom validation input value. Function must return object with following parameters: `value`: string value of input, `isValid`: boolean result of validating, `errors`(optional): array of errors.
How are applied colors in component:
Base colors:
| Сomponent actions | isValid | border-color |
| ----------------- | :-----: | :----------: |
| `:focus` | `false` | #c30 |
| `:focus` | `true` | #2DA7DB |
| `:hover` | `false` | #c30 |
| `:hover` | `true` | #D0D5DA |
| `default` | `false` | #c30 |
| `default` | `true` | #D0D5DA |
| `:focus` | `false` | ![#c30](https://placehold.it/15/c30/000000?text=+) #c30 |
| `:focus` | `true` | ![#2DA7DB](https://placehold.it/15/2DA7DB/000000?text=+) #2DA7DB |
| `:hover` | `false` | ![#c30](https://placehold.it/15/c30/000000?text=+) #c30 |
| `:hover` | `true` | ![#D0D5DA](https://placehold.it/15/D0D5DA/000000?text=+) #D0D5DA |
| `default` | `false` | ![#c30](https://placehold.it/15/c30/000000?text=+) #c30 |
| `default` | `true` | ![#D0D5DA](https://placehold.it/15/D0D5DA/000000?text=+) #D0D5DA |
```js
import React, { useState } from "react";
import React from "react";
import { EmailInput } from "asc-web-components";
const [emailValid, setEmailValid] = useState(true);
const customChangeFunc = (e) => {
const onChange = (e) => {
// your event handling
customValidateFunc(e.target.value);
customValidate(e.target.value);
}
const customValidateFunc = (value) => {
let validationResult;
// your validating function
setEmailValid(validationResult);
const customValidate = (value) => {
const isValid = !!(value && value.length > 0);
return {
value,
isValid: isValid,
errors: isValid ? [] : ["incorrect email"]
}
}
const onValidateInput = (isValidEmail) => {
console.log(`isValidEmail = ${isValidEmail}`);
const onValidateInput = (result) => {
console.log("onValidateInput", result);
}
return (
```
```jsx
<EmailInput
isValid={emailValid}
onChange={customChangeFunc}
customValidateFunc={customValidateFunc}
customValidate={customValidate}
onChange={onChange}
onValidateInput={onValidateInput}
/>;
);
```
#### Email settings RFC 5321
```js
{
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: true,
allowStrictLocalPart: false,
allowSpaces: true,
allowName: false,
allowLocalDomainName: true
}
```

View File

@ -53,7 +53,7 @@ storiesOf('Components|Input', module)
id={id}
name={name}
emailSettings={settings}
onValidateInput={(isEmailValid) => action('isValidEmail')(isEmailValid)}
onValidateInput={(isEmailValid) => action('onValidateInput')(isEmailValid)}
/>
</Section>
);

View File

@ -19,6 +19,73 @@ const baseProps = {
}
describe('<EmailInput />', () => {
it('Init invalid value test', () => {
const email = "zzz";
const wrapper = shallow(<EmailInput value={email} />).instance();
expect(wrapper.state.isValidEmail.isValid).toBe(false);
});
it('Clean valid value test', () => {
const email = "zzz";
const wrapper = shallow(<EmailInput value={email} />).instance();
let event = { target: { value: "simple@example.com" } };
wrapper.onChange(event);
expect(wrapper.state.isValidEmail.isValid).toBe(true);
event = { target: { value: "" } };
wrapper.onChange(event);
expect(wrapper.state.isValidEmail.isValid).toBe(false);
});
it('Change value prop test', () => {
const email = "zzz";
const wrapper = mount(<EmailInput value={email} />);
expect(wrapper.state().inputValue).toBe(email);
wrapper.setProps({ value: 'bar' });
expect(wrapper.state().isValidEmail.isValid).toBe(false);
expect(wrapper.state().inputValue).toBe("bar");
});
it('Custom validation test', () => {
const email = "zzz";
const customValidate = (value) => {
const isValid = !!(value && value.length > 0);
return {
isValid: isValid,
errors: isValid ? [] : ["incorrect email"]
}
}
const wrapper = mount(<EmailInput value={email} customValidate={customValidate} />);
expect(wrapper.state().inputValue).toBe(email);
expect(wrapper.state().isValidEmail.isValid).toBe(true);
wrapper.setProps({ value: 'bar' });
expect(wrapper.state().isValidEmail.isValid).toBe(true);
expect(wrapper.state().inputValue).toBe("bar");
wrapper.setProps({ value: '' });
expect(wrapper.state().isValidEmail.isValid).toBe(false);
});
it('renders without error', () => {
const wrapper = mount(<EmailInput {...baseProps} />);
@ -58,7 +125,7 @@ describe('<EmailInput />', () => {
expect(shouldUpdate).toBe(true);
expect(wrapper.state('emailSettings')).toBe(emailSettings);
});
it('isValidEmail is "true" after deleting value', () => {
it('isValidEmail is "false" after deleting value', () => {
const wrapper = mount(<EmailInput {...baseProps} />);
@ -66,13 +133,13 @@ describe('<EmailInput />', () => {
wrapper.simulate('change', event);
expect(wrapper.state('isValidEmail')).toBe(false);
expect(wrapper.state().isValidEmail.isValid).toBe(false);
const emptyValue = { target: { value: "" } };
wrapper.simulate('change', emptyValue);
expect(wrapper.state('isValidEmail')).toBe(true);
expect(wrapper.state().isValidEmail.isValid).toBe(false);
});
it('not re-render test', () => {
@ -86,7 +153,7 @@ describe('<EmailInput />', () => {
it('passed valid email: simple@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -98,7 +165,7 @@ describe('<EmailInput />', () => {
it('passed valid email: disposable.style.email.with+symbol@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -110,7 +177,7 @@ describe('<EmailInput />', () => {
it('passed valid email: user.name+tag+sorting@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -122,7 +189,7 @@ describe('<EmailInput />', () => {
it('passed valid email with one-letter local-part: x@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -135,7 +202,7 @@ describe('<EmailInput />', () => {
it('passed valid email, local domain name with no TLD: admin@mailserver1', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
@ -148,7 +215,7 @@ describe('<EmailInput />', () => {
it('passed valid email, local domain name with no TLD: admin@mailserver1 (settings: allowLocalDomainName = true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
@ -162,7 +229,7 @@ describe('<EmailInput />', () => {
it('passed valid email (one-letter domain name): example@s.example', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -175,7 +242,7 @@ describe('<EmailInput />', () => {
it('passed valid email (space between the quotes): " "@example.org', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -187,7 +254,7 @@ describe('<EmailInput />', () => {
it('passed valid email (space between the quotes): " "@example.org (settings: allowSpaces = true, allowStrictLocalPart = false)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
emailSettings.allowSpaces = true;
@ -202,7 +269,7 @@ describe('<EmailInput />', () => {
it('passed valid email (quoted double dot): "john..doe"@example.org)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -214,7 +281,7 @@ describe('<EmailInput />', () => {
it('passed valid email (quoted double dot): "john..doe"@example.org (settings: allowSpaces = true, allowStrictLocalPart = false)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
emailSettings.allowSpaces = true;
@ -229,7 +296,7 @@ describe('<EmailInput />', () => {
it('passed valid email (bangified host route used for uucp mailers): mailhost!username@example.org', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -241,7 +308,7 @@ describe('<EmailInput />', () => {
it('passed valid email (bangified host route used for uucp mailers): mailhost!username@example.org (object settings: allowStrictLocalPart = false)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = {
allowStrictLocalPart: false
@ -255,7 +322,7 @@ describe('<EmailInput />', () => {
it('passed valid email (% escaped mail route to user@example.com via example.org): user%example.com@example.org)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
emailSettings.allowStrictLocalPart = false;
@ -268,7 +335,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in domain: example@джpумлатест.bрфa', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -281,7 +348,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part: mañana@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -294,7 +361,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part and domain: mañana@mañana.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -306,7 +373,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part and domain: mañana@mañana.com (settings: allowDomainPunycode=true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
emailSettings.allowDomainPunycode = true;
@ -319,7 +386,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part and domain: mañana@mañana.com (settings: allowLocalPartPunycode=true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
emailSettings.allowLocalPartPunycode = true;
@ -332,7 +399,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part and domain: mañana@mañana.com (settings: allowDomainPunycode=true, allowLocalPartPunycode=true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
emailSettings.allowLocalPartPunycode = true;
@ -346,7 +413,7 @@ describe('<EmailInput />', () => {
it('passed valid email with punycode symbols in local part and domain: mañana@mañana.com (settings: allowDomainPunycode=true, allowLocalPartPunycode=true, allowStrictLocalPart=false)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
emailSettings.allowLocalPartPunycode = true;
@ -361,7 +428,7 @@ describe('<EmailInput />', () => {
it('passed valid email with IP address in domain: user@[127.0.0.1]', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -373,7 +440,7 @@ describe('<EmailInput />', () => {
it('passed valid email with IP address in domain: user@[127.0.0.1] (settings: allowDomainIp = true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = { allowDomainIp: true };
@ -386,7 +453,7 @@ describe('<EmailInput />', () => {
it('passed valid email with Name (RFC 5322): "Jack Bowman" <jack@fogcreek.com>', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -398,7 +465,7 @@ describe('<EmailInput />', () => {
it('passed valid email with Name (RFC 5322): "Jack Bowman" <jack@fogcreek.com> (instance of EmailSettings: allowName = true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
@ -412,7 +479,7 @@ describe('<EmailInput />', () => {
it('passed valid email with Name (RFC 5322): Bob <bob@example.com>', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -424,7 +491,7 @@ describe('<EmailInput />', () => {
it('passed valid email with Name (RFC 5322): Bob <bob@example.com> (instance of EmailSettings: allowName = true)', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(true);
expect(isValidEmail.isValid).toEqual(true);
});
const emailSettings = new EmailSettings();
@ -439,7 +506,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (no @ character): Abc.example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -451,7 +518,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (only one @ is allowed outside quotation marks): A@b@c@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -463,7 +530,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (none of the special characters in this local-part are allowed outside quotation marks): a"b(c)d,e:f;g<h>i[j\k]l@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -475,7 +542,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (none of the special characters in this local-part are allowed outside quotation marks): a"b(c)d,e:f;g<h>i[j\k]l@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);
@ -487,7 +554,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (quoted strings must be dot separated or the only element making up the local-part): just"not"right@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
@ -502,7 +569,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (spaces, quotes, and backslashes may only exist when within quoted strings and preceded by a backslash): this is"not\allowed@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
@ -517,7 +584,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (even if escaped (preceded by a backslash), spaces, quotes, and backslashes must still be contained by quotes): this\ still\"not\\allowed@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const emailSettings = new EmailSettings();
@ -532,7 +599,7 @@ describe('<EmailInput />', () => {
it('passed invalid email (local part is longer than 64 characters): 1234567890123456789012345678901234567890123456789012345678901234+x@example.com', () => {
const onValidateInput = jest.fn(isValidEmail => {
expect(isValidEmail).toEqual(false);
expect(isValidEmail.isValid).toEqual(false);
});
const wrapper = mount(<EmailInput {...baseProps} onValidateInput={onValidateInput} />);

View File

@ -1,139 +1,156 @@
import React from 'react'
import styled from 'styled-components'
import PropTypes from 'prop-types'
import React from "react";
import PropTypes from "prop-types";
import isEqual from "lodash/isEqual";
import TextInput from '../text-input'
import { EmailSettings, parseAddress, checkAndConvertEmailSettings, isEqualEmailSettings } from '../../utils/email/';
import TextInput from "../text-input";
import {
EmailSettings,
parseAddress
} from "../../utils/email/";
const borderColor = {
default: '#D0D5DA',
isValid: '#2DA7DB',
isNotValid: '#c30'
};
// eslint-disable-next-line no-unused-vars
const SimpleInput = ({ onValidateInput, isValidEmail, emailSettings, ...props }) => <TextInput {...props}></TextInput>;
SimpleInput.propTypes = {
onValidateInput: PropTypes.func,
isValidEmail: PropTypes.bool,
emailSettings: PropTypes.oneOfType([PropTypes.instanceOf(EmailSettings), PropTypes.objectOf(PropTypes.bool)])
}
const StyledTextInput = styled(SimpleInput)`
border-color: ${props => (props.isValidEmail ? borderColor.default : borderColor.isNotValid)};
:hover {
border-color: ${props => (props.isValidEmail ? borderColor.default : borderColor.isNotValid)};
}
:focus {
border-color: ${props => (props.isValidEmail ? borderColor.isValid : borderColor.isNotValid)};
}
`;
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
const TextInputWrapper = ({
onValidateInput,
isValidEmail,
emailSettings,
...props
}) => <TextInput {...props}></TextInput>;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
class EmailInput extends React.Component {
constructor(props) {
super(props);
const { value, emailSettings } = this.props;
const validatedSettings = checkAndConvertEmailSettings(emailSettings);
const validatedSettings = EmailSettings.parse(emailSettings);
const isValidEmail = this.checkEmail(value, validatedSettings);
this.state = {
isValidEmail: true,
isValidEmail,
emailSettings: validatedSettings,
inputValue: value
}
}
componentDidUpdate() {
const { emailSettings } = this.props;
if (isEqualEmailSettings(this.state.emailSettings, emailSettings)) return;
const validatedSettings = checkAndConvertEmailSettings(emailSettings);
this.setState({ emailSettings: validatedSettings }, function () {
this.checkEmail(this.state.inputValue);
});
}
checkEmail = (value) => {
if (!value.length) {
!this.state.isValidEmail && this.setState({ isValidEmail: true, inputValue: value });
return;
}
const emailObj = parseAddress(value, this.state.emailSettings);
const isValidEmail = emailObj.isValid();
this.props.onValidateInput
&& this.props.onValidateInput(isValidEmail);
this.setState({ isValidEmail, inputValue: value });
}
onChangeAction = (e) => {
this.props.onChange && this.props.onChange(e);
this.props.customValidateFunc ? this.props.customValidateFunc(e) : this.checkEmail(e.target.value);
};
}
shouldComponentUpdate(nextProps, nextState) {
return !isEqual(this.props, nextProps) || !isEqual(this.state, nextState);
}
componentDidUpdate(prevProps) {
const { emailSettings, value } = this.props;
if (!EmailSettings.equals(emailSettings, prevProps.emailSettings)) {
const validatedSettings = EmailSettings.parse(emailSettings);
this.setState({ emailSettings: validatedSettings }, () => {
this.validate(this.state.inputValue);
});
}
if (value !== prevProps.value) {
this.validate(value);
}
}
validate = (value) => {
const { onValidateInput } = this.props;
const isValidEmail = this.checkEmail(value);
this.setState({
inputValue: value,
isValidEmail
});
onValidateInput && onValidateInput(isValidEmail);
}
checkEmail = (value, emailSettings = this.state.emailSettings) => {
const { customValidate } = this.props;
if (customValidate) {
return customValidate(value);
} else {
const emailObj = parseAddress(value, emailSettings);
const isValidEmail = emailObj.isValid();
const parsedErrors = emailObj.parseErrors;
const errors = parsedErrors
? parsedErrors.map(error => error.errorKey)
: [];
return {
value,
isValid: isValidEmail,
errors
};
}
};
onChange = e => {
const { onChange, onValidateInput } = this.props;
onChange ? onChange(e) : this.setState({ inputValue: e.target.value });
const isValidEmail = this.checkEmail(e.target.value);
this.setState({ isValidEmail });
onValidateInput && onValidateInput(isValidEmail);
};
render() {
//console.log('EmailInput render()');
// eslint-disable-next-line no-unused-vars
const { onValidateInput, emailSettings, onChange, isValid, value, ...rest } = this.props;
const {
onValidateInput,
hasError
} = this.props;
const { isValidEmail, inputValue } = this.state;
const isError =
typeof hasError === "boolean"
? hasError
: Boolean(inputValue && !isValidEmail.isValid);
return (
<StyledTextInput
isValidEmail={isValid || isValidEmail}
<TextInputWrapper
{...this.props}
hasError={isError}
value={inputValue}
onChange={this.onChangeAction}
type='text'
onChange={this.onChange}
type="text"
onValidateInput={onValidateInput}
{...rest}
/>
);
}
}
EmailInput.propTypes = {
onValidateInput: PropTypes.func,
onChange: PropTypes.func,
customValidateFunc: PropTypes.func,
value: PropTypes.string,
isValid: PropTypes.bool,
emailSettings: PropTypes.oneOfType([PropTypes.instanceOf(EmailSettings), PropTypes.objectOf(PropTypes.bool)]),
className: PropTypes.string,
customValidate: PropTypes.func,
emailSettings: PropTypes.oneOfType([
PropTypes.instanceOf(EmailSettings),
PropTypes.objectOf(PropTypes.bool)
]),
hasError: PropTypes.bool,
id: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
}
onChange: PropTypes.func,
onValidateInput: PropTypes.func,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
value: PropTypes.string
};
EmailInput.defaultProps = {
id: '',
name: '',
autoComplete: 'email',
maxLength: 255,
value: '',
autoComplete: "email",
className: "",
hasError: undefined,
id: "",
isDisabled: false,
isReadOnly: false,
size: 'base',
maxLength: 255,
name: "",
placeholder: "",
scale: false,
size: "base",
title: "",
value: "",
withBorder: true,
placeholder: '',
className: '',
title: '',
isValid: undefined,
emailSettings: new EmailSettings()
}
};
export default EmailInput;

View File

@ -113,12 +113,20 @@ class ModalDialog extends React.Component {
componentDidMount() {
window.addEventListener("resize", this.throttledResize);
window.addEventListener("keyup", this.onKeyPress);
}
componentWillUnmount() {
window.removeEventListener("resize", this.throttledResize);
window.removeEventListener("keyup", this.onKeyPress);
}
onKeyPress = event => {
if (event.key === "Esc" || event.key === "Escape") {
this.props.onClose();
}
};
render() {
const {
visible,

View File

@ -7,3 +7,17 @@ export const parseErrorTypes = Object.freeze({
EmptyRecipients: 1,
IncorrectEmail: 2
});
export const errorKeys = Object.freeze({
LocalDomain: "LocalDomain",
IncorrectDomain: "IncorrectDomain",
DomainIpAddress: "DomainIpAddress",
PunycodeDomain: "PunycodeDomain",
PunycodeLocalPart: "PunycodeLocalPart",
IncorrectLocalPart: "IncorrectLocalPart",
SpacesInLocalPart: "SpacesInLocalPart",
MaxLengthExceeded: "MaxLengthExceeded",
IncorrectEmail: "IncorrectEmail",
ManyEmails: "ManyEmails",
EmptyEmail: "EmptyEmail"
});

View File

@ -1,6 +1,6 @@
import emailAddresses from "email-addresses";
import punycode from "punycode";
import { parseErrorTypes } from "./../constants";
import { parseErrorTypes, errorKeys } from "./../constants";
import { EmailSettings } from './emailSettings';
const getParts = string => {
@ -70,7 +70,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Local domains are not supported",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.LocalDomain
});
}
@ -79,7 +80,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Incorrect domain",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.IncorrectDomain
});
}
@ -91,7 +93,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Domains as ip address are not supported",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.DomainIpAddress
});
}
@ -99,7 +102,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Punycode domains are not supported",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.PunycodeDomain
});
}
@ -107,7 +111,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Punycode local part are not supported",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.PunycodeLocalPart
});
}
@ -119,7 +124,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Incorrect localpart",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.IncorrectLocalPart
});
}
@ -131,7 +137,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "Incorrect, localpart contains spaces",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.SpacesInLocalPart
});
}
@ -139,7 +146,8 @@ const checkErrors = (parsedAddress, options) => {
errors.push({
message: "The maximum total length of a user name or other local-part is 64 characters. See RFC2821",
type: parseErrorTypes.IncorrectEmail,
errorItem: parsedAddress
errorItem: parsedAddress,
errorKey: errorKeys.MaxLengthExceeded
});
}
@ -168,7 +176,8 @@ export const parseAddresses = (str, options = new EmailSettings()) => {
if (!parsedAddress || (parsedAddress.name && !options.allowName)) {
errors.push({
message: "Incorrect email",
type: parseErrorTypes.IncorrectEmail
type: parseErrorTypes.IncorrectEmail,
errorKey: errorKeys.IncorrectEmail
});
} else {
const checkOptionErrors = checkErrors(parsedAddress, options)
@ -195,13 +204,13 @@ export const parseAddress = (str, options = new EmailSettings()) => {
if (!parsedEmails.length) {
return new Email("", str, [
{ message: "No one email parsed", type: parseErrorTypes.EmptyRecipients }
{ message: "No one email parsed", type: parseErrorTypes.EmptyRecipients, errorKey: errorKeys.EmptyEmail }
]);
}
if (parsedEmails.length > 1) {
return new Email("", str, [
{ message: "Too many email parsed", type: parseErrorTypes.IncorrectEmail }
{ message: "Too many email parsed", type: parseErrorTypes.IncorrectEmail, errorKey: errorKeys.ManyEmails }
]);
}

View File

@ -9,6 +9,28 @@ export class EmailSettings {
this.allowLocalDomainName = false;
}
static equals = (settings1, settings2) => {
const instance1 = EmailSettings.parse(settings1);
const instance2 = EmailSettings.parse(settings2);
const comparedProperties = [
'allowDomainPunycode',
'allowLocalPartPunycode',
'allowDomainIp',
'allowStrictLocalPart',
'allowSpaces',
'allowName',
'allowLocalDomainName'
];
const propLength = comparedProperties.length;
for (let i = 0; i < propLength; i++) {
const comparedProp = comparedProperties[i]
if (instance1[comparedProp] !== instance2[comparedProp]) {
return false;
}
}
return true;
}
get allowDomainPunycode() {
return this._allowDomainPunycode;
}
@ -18,7 +40,7 @@ export class EmailSettings {
this._allowDomainPunycode = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowDomainPunycode option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowDomainPunycode option. Use boolean value`);
}
}
@ -31,7 +53,7 @@ export class EmailSettings {
this._allowLocalPartPunycode = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowLocalPartPunycode option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowLocalPartPunycode option. Use boolean value`);
}
}
@ -44,7 +66,7 @@ export class EmailSettings {
this._allowDomainIp = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowDomainIp option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowDomainIp option. Use boolean value`);
}
}
@ -57,7 +79,7 @@ export class EmailSettings {
this._allowStrictLocalPart = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowStrictLocalPart option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowStrictLocalPart option. Use boolean value`);
}
}
@ -70,7 +92,7 @@ export class EmailSettings {
this._allowSpaces = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowSpaces option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowSpaces option. Use boolean value`);
}
}
@ -83,7 +105,7 @@ export class EmailSettings {
this._allowName = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowName option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowName option. Use boolean value`);
}
}
@ -96,11 +118,11 @@ export class EmailSettings {
this._allowLocalDomainName = value;
}
else {
throw new TypeError (`Invalid value ${value} for allowLocalDomainName option. Use boolean value`);
throw new TypeError(`Invalid value ${value} for allowLocalDomainName option. Use boolean value`);
}
}
getSettings() {
toObject() {
return {
allowDomainPunycode: this.allowDomainPunycode,
allowLocalPartPunycode: this.allowLocalPartPunycode,
@ -121,40 +143,22 @@ export class EmailSettings {
this.allowName = true;
this.allowLocalDomainName = true;
}
}
export const checkAndConvertEmailSettings = (settings) => {
if (typeof settings === 'object' && !(settings instanceof EmailSettings)) {
static parse = (settings) => {
if(settings instanceof EmailSettings)
return settings;
if(typeof settings !== 'object')
throw new Error("Invalid argument");
const defaultSettings = new EmailSettings();
Object.keys(settings).map((item) => {
if (defaultSettings[item] !== null && defaultSettings[item] != settings[item]) {
defaultSettings[item] = settings[item];
}
Object.keys(settings).map((key) => {
if (!(key in defaultSettings) || defaultSettings[key] === settings[key])
return;
defaultSettings[key] = settings[key];
});
return defaultSettings;
}
else if (typeof settings === 'object' && settings instanceof EmailSettings) {
return settings;
}
}
export const isEqualEmailSettings = (settings1, settings2) => {
const comparedProperties = [
'allowDomainPunycode',
'allowLocalPartPunycode',
'allowDomainIp',
'allowStrictLocalPart',
'allowSpaces',
'allowName',
'allowLocalDomainName'
];
const propLength = comparedProperties.length;
for (let i = 0; i < propLength; i++) {
const comparedProp = comparedProperties[i]
if (settings1[comparedProp] !== settings2[comparedProp]) {
return false;
}
}
return true;
}

View File

@ -1,203 +1,214 @@
import { EmailSettings, isEqualEmailSettings, checkAndConvertEmailSettings } from './index';
import { EmailSettings } from "./index";
const defaultEmailSettingsObj = {
allowDomainPunycode: false,
allowLocalPartPunycode: false,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: false
allowDomainPunycode: false,
allowLocalPartPunycode: false,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: false
};
describe('emailSettings', () => {
describe("emailSettings", () => {
it("get default settings from instance", () => {
const email = new EmailSettings();
const settings = email.toObject();
expect(settings).toStrictEqual(defaultEmailSettingsObj);
});
it('get default settings from instance', () => {
it("change and get settings from instance", () => {
const emailSettingsObj = {
allowDomainPunycode: false,
allowLocalPartPunycode: false,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: true
};
const email = new EmailSettings();
const settings = email.getSettings();
expect(settings).toStrictEqual(defaultEmailSettingsObj);
});
const emailSettings = new EmailSettings();
emailSettings.allowLocalDomainName = true;
const settings = emailSettings.toObject();
it('change and get settings from instance', () => {
expect(settings).toStrictEqual(emailSettingsObj);
});
const emailSettingsObj = {
allowDomainPunycode: false,
allowLocalPartPunycode: false,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: true
};
it("set and get allowStrictLocalPart setting", () => {
const emailSettings = new EmailSettings();
emailSettings.allowStrictLocalPart = false;
const emailSettings = new EmailSettings();
emailSettings.allowLocalDomainName = true;
const settings = emailSettings.getSettings();
expect(emailSettings.allowStrictLocalPart).toBe(false);
});
expect(settings).toStrictEqual(emailSettingsObj);
});
it("disable settings", () => {
const disabledSettings = {
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: true,
allowStrictLocalPart: false,
allowSpaces: true,
allowName: true,
allowLocalDomainName: true
};
const emailSettings = new EmailSettings();
emailSettings.disableAllSettings();
const newSettings = emailSettings.toObject();
it('set and get allowStrictLocalPart setting', () => {
const emailSettings = new EmailSettings();
emailSettings.allowStrictLocalPart = false;
expect(newSettings).toStrictEqual(disabledSettings);
});
expect(emailSettings.allowStrictLocalPart).toBe(false);
});
it("set invalid (non-boolean) value for allowLocalDomainName setting", () => {
const emailSettings = new EmailSettings();
it('disable settings', () => {
try {
emailSettings.allowLocalDomainName = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
const disabledSettings = {
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: true,
allowStrictLocalPart: false,
allowSpaces: true,
allowName: true,
allowLocalDomainName: true
};
const emailSettings = new EmailSettings();
emailSettings.disableAllSettings();
const newSettings = emailSettings.getSettings();
it("set invalid (non-boolean) value for allowDomainPunycode setting", () => {
const emailSettings = new EmailSettings();
expect(newSettings).toStrictEqual(disabledSettings);
});
try {
emailSettings.allowDomainPunycode = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
it("set invalid (non-boolean) value for allowLocalPartPunycode setting", () => {
const emailSettings = new EmailSettings();
it('set invalid (non-boolean) value for allowLocalDomainName setting', () => {
const emailSettings = new EmailSettings();
try {
emailSettings.allowLocalPartPunycode = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
try {
emailSettings.allowLocalDomainName = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
it("set invalid (non-boolean) value for allowDomainIp setting", () => {
const emailSettings = new EmailSettings();
it('set invalid (non-boolean) value for allowDomainPunycode setting', () => {
const emailSettings = new EmailSettings();
try {
emailSettings.allowDomainIp = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
try {
emailSettings.allowDomainPunycode = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
it("set invalid (non-boolean) value for allowStrictLocalPart setting", () => {
const emailSettings = new EmailSettings();
it('set invalid (non-boolean) value for allowLocalPartPunycode setting', () => {
const emailSettings = new EmailSettings();
try {
emailSettings.allowStrictLocalPart = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
try {
emailSettings.allowLocalPartPunycode = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
it("set invalid (non-boolean) value for allowSpaces setting", () => {
const emailSettings = new EmailSettings();
it('set invalid (non-boolean) value for allowDomainIp setting', () => {
const emailSettings = new EmailSettings();
try {
emailSettings.allowSpaces = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
try {
emailSettings.allowDomainIp = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
it("set invalid (non-boolean) value for allowName setting", () => {
const emailSettings = new EmailSettings();
it('set invalid (non-boolean) value for allowStrictLocalPart setting', () => {
const emailSettings = new EmailSettings();
try {
emailSettings.allowName = "1";
} catch (err) {
expect(err.name).toBe("TypeError");
}
});
try {
emailSettings.allowStrictLocalPart = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
// test EmailSettings.equals function
it('set invalid (non-boolean) value for allowSpaces setting', () => {
const emailSettings = new EmailSettings();
it("is not equal email settings", () => {
const emailSettings = new EmailSettings();
const emailSettings2 = new EmailSettings();
try {
emailSettings.allowSpaces = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
emailSettings.allowStrictLocalPart = false;
const isEqual = EmailSettings.equals(emailSettings, emailSettings2);
it('set invalid (non-boolean) value for allowName setting', () => {
const emailSettings = new EmailSettings();
expect(isEqual).toBe(false);
});
try {
emailSettings.allowName = '1';
} catch (err) {
expect(err.name).toBe('TypeError');
}
});
it("is equal email settings", () => {
const emailSettings = new EmailSettings();
const emailSettings2 = new EmailSettings();
const isEqual = EmailSettings.equals(emailSettings, emailSettings2);
// test isEqualEmailSettings function
expect(isEqual).toBe(true);
});
it('is not equal email settings', () => {
const emailSettings = new EmailSettings();
const emailSettings2 = new EmailSettings();
// test checkAndEmailSettings.parse function
emailSettings.allowStrictLocalPart = false;
const isEqual = isEqualEmailSettings(emailSettings, emailSettings2);
it("passed instance of default EmailSettings, return same instance", () => {
const emailSettings = new EmailSettings();
const convertedSettings = EmailSettings.parse(emailSettings);
expect(isEqual).toBe(false);
});
expect(convertedSettings).toStrictEqual(emailSettings);
});
it('is equal email settings', () => {
const emailSettings = new EmailSettings();
const emailSettings2 = new EmailSettings();
const isEqual = isEqualEmailSettings(emailSettings, emailSettings2);
it("passed object with default settings, return instance of default EmailSettings", () => {
const convertedSettings = EmailSettings.parse(defaultEmailSettingsObj);
const emailSettings = new EmailSettings();
expect(isEqual).toBe(true);
});
expect(convertedSettings).toStrictEqual(emailSettings);
});
// test checkAndConvertEmailSettings function
it("passed instance of EmailSettings, return same instance", () => {
const emailSettings = new EmailSettings();
emailSettings.allowLocalDomainName = true;
const convertedSettings = EmailSettings.parse(emailSettings);
it('passed instance of default EmailSettings, return same instance', () => {
expect(convertedSettings).toStrictEqual(emailSettings);
});
const emailSettings = new EmailSettings();
const convertedSettings = checkAndConvertEmailSettings(emailSettings);
it("passed object with settings, return instance of EmailSettings", () => {
const emailSettingsObj = {
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: false
};
expect(convertedSettings).toStrictEqual(emailSettings);
});
const convertedSettings = EmailSettings.parse(emailSettingsObj);
const emailSettings = new EmailSettings();
emailSettings.allowDomainPunycode = true;
emailSettings.allowLocalPartPunycode = true;
it('passed object with default settings, return instance of default EmailSettings', () => {
expect(convertedSettings).toStrictEqual(emailSettings);
});
const convertedSettings = checkAndConvertEmailSettings(defaultEmailSettingsObj);
const emailSettings = new EmailSettings();
it("passed invalid object with settings, return instance of EmailSettings", () => {
const emailSettingsObj = {
temp: "temp",
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: false
};
expect(convertedSettings).toStrictEqual(emailSettings);
});
const convertedSettings = EmailSettings.parse(emailSettingsObj);
it('passed instance of EmailSettings, return same instance', () => {
const emailSettings = new EmailSettings();
emailSettings.allowLocalDomainName = true;
const convertedSettings = checkAndConvertEmailSettings(emailSettings);
expect(convertedSettings).toStrictEqual(emailSettings);
});
it('passed object with settings, return instance of EmailSettings', () => {
const emailSettingsObj = {
allowDomainPunycode: true,
allowLocalPartPunycode: true,
allowDomainIp: false,
allowStrictLocalPart: true,
allowSpaces: false,
allowName: false,
allowLocalDomainName: false
};
const convertedSettings = checkAndConvertEmailSettings(emailSettingsObj);
const emailSettings = new EmailSettings();
emailSettings.allowDomainPunycode = true;
emailSettings.allowLocalPartPunycode = true;
expect(convertedSettings).toStrictEqual(emailSettings);
});
const emailSettings = new EmailSettings();
emailSettings.allowDomainPunycode = true;
emailSettings.allowLocalPartPunycode = true;
expect(convertedSettings).toStrictEqual(emailSettings);
});
});

View File

@ -1,2 +1,2 @@
export { parseAddress, Email, isEqualEmail, isValidDomainName, parseAddresses } from './email';
export { EmailSettings, checkAndConvertEmailSettings, isEqualEmailSettings } from './emailSettings';
export { EmailSettings } from './emailSettings';