Merge branch 'master' into feature/elastic

This commit is contained in:
pavelbannov 2020-02-11 14:08:26 +03:00
commit 3fa9046bec
64 changed files with 621 additions and 504 deletions

View File

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
@ -101,23 +100,24 @@ namespace ASC.Common.DependencyInjection
void LoadAssembly(string type)
{
var dll = type.Substring(type.IndexOf(",") + 1).Trim();
var productPath = Path.Combine(productsDir, dll, subfolder);
var path = GetPath(Path.Combine(productPath, "bin"), dll, SearchOption.AllDirectories) ?? GetPath(productPath, dll, SearchOption.TopDirectoryOnly);
var path = GetFullPath(dll);
if (!string.IsNullOrEmpty(path))
{
AssemblyLoadContext.Default.Resolving += Default_Resolving(path);
Func<AssemblyLoadContext, AssemblyName, Assembly> Default_Resolving(string path)
AssemblyLoadContext.Default.Resolving += (c, n) =>
{
return (c, n) =>
{
return c.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(path), $"{n.Name}.dll"));
};
}
var path = GetFullPath(n.Name);
return c.LoadFromAssemblyPath(Path.Combine(Path.GetDirectoryName(path), $"{n.Name}.dll"));
};
}
}
string GetFullPath(string n)
{
var productPath = Path.Combine(productsDir, n, subfolder);
return GetPath(Path.Combine(productPath, "bin"), n, SearchOption.AllDirectories) ?? GetPath(productPath, n, SearchOption.TopDirectoryOnly);
}
string GetPath(string dirPath, string dll, SearchOption searchOption)
{
if (!Directory.Exists(dirPath)) return null;

View File

@ -303,12 +303,13 @@ namespace ASC.Core.Data
public void RemoveGroup(int tenant, Guid id, bool immediate)
{
var ids = CollectGroupChilds(tenant, id);
var stringIds = ids.Select(r => r.ToString()).ToList();
using var tr = UserDbContext.Database.BeginTransaction();
UserDbContext.Acl.RemoveRange(UserDbContext.Acl.Where(r => r.Tenant == tenant && ids.Any(i => i == r.Subject)));
UserDbContext.Subscriptions.RemoveRange(UserDbContext.Subscriptions.Where(r => r.Tenant == tenant && ids.Any(i => i.ToString() == r.Recipient)));
UserDbContext.SubscriptionMethods.RemoveRange(UserDbContext.SubscriptionMethods.Where(r => r.Tenant == tenant && ids.Any(i => i.ToString() == r.Recipient)));
UserDbContext.Subscriptions.RemoveRange(UserDbContext.Subscriptions.Where(r => r.Tenant == tenant && stringIds.Any(i => i == r.Recipient)));
UserDbContext.SubscriptionMethods.RemoveRange(UserDbContext.SubscriptionMethods.Where(r => r.Tenant == tenant && stringIds.Any(i => i == r.Recipient)));
var userGroups = UserDbContext.UserGroups.Where(r => r.Tenant == tenant && ids.Any(i => i == r.GroupId));
var groups = UserDbContext.Groups.Where(r => r.Tenant == tenant && ids.Any(i => i == r.Id));
@ -633,11 +634,11 @@ namespace ASC.Core.Data
if (!string.IsNullOrEmpty(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));
u => u.FirstName.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
u.LastName.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
u.Title.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
u.Location.Contains(text, StringComparison.InvariantCultureIgnoreCase) ||
u.Email.Contains(text, StringComparison.InvariantCultureIgnoreCase));
}
return q;

View File

@ -125,7 +125,8 @@ class SectionBodyContent extends React.Component {
key: GUID_EMPTY,
label: t("LblSelect"),
default: true
}
},
nameError: null
};
return newState;
@ -193,7 +194,10 @@ class SectionBodyContent extends React.Component {
const { group, t, groupCaption } = this.props;
const { groupName, groupManager, groupMembers } = this.state;
if (!groupName || !groupName.trim().length) return false;
if (!groupName || !groupName.trim().length) {
this.setState({nameError: t('EmptyFieldError')});
return false;
}
this.setState({ inLoading: true });
@ -253,6 +257,11 @@ class SectionBodyContent extends React.Component {
}
}
onFocusName = () => {
if(this.state.nameError)
this.setState({ nameError: null });
}
render() {
const { t, groupHeadCaption, groupsCaption, me } = this.props;
const {
@ -264,14 +273,16 @@ class SectionBodyContent extends React.Component {
error,
searchValue,
groupManager,
buttonLabel
buttonLabel,
nameError
} = this.state;
return (
<MainContainer>
<FieldContainer
className="group-name_container"
isRequired={true}
hasError={false}
hasError={!!nameError}
errorMessage={nameError}
isVertical={true}
labelText={t("Name")}
>
@ -283,9 +294,11 @@ class SectionBodyContent extends React.Component {
isBold={true}
tabIndex={1}
value={groupName}
hasError={!!nameError}
onChange={this.onGroupChange}
isDisabled={inLoading}
onKeyUp={this.onKeyPress}
onFocus={this.onFocusName}
/>
</FieldContainer>
<FieldContainer

View File

@ -12,7 +12,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -11,5 +11,6 @@
"CustomNewDepartment": "{{groupCaption}} (creation)",
"SearchAddedMembers": "Search added members",
"MeLabel": "Me"
"MeLabel": "Me",
"EmptyFieldError": "Empty field"
}

View File

@ -11,5 +11,6 @@
"CustomNewDepartment": "{{groupCaption}} (создание)",
"SearchAddedMembers": "Поиск добавленных участников",
"MeLabel": "Я"
"MeLabel": "Я",
"EmptyFieldError": "Пустое поле"
}

View File

@ -39,7 +39,7 @@ const { resendUserInvites } = api.people;
const { EmployeeStatus } = constants;
const { Filter } = api;
const isRefetchPeople = true;
class SectionBodyContent extends React.PureComponent {
constructor(props) {
super(props);
@ -102,7 +102,7 @@ class SectionBodyContent extends React.PureComponent {
const { updateUserStatus, onLoading, t } = this.props;
onLoading(true);
updateUserStatus(EmployeeStatus.Disabled, [user.id])
updateUserStatus(EmployeeStatus.Disabled, [user.id], isRefetchPeople)
.then(() => toastr.success(t('SuccessChangeUserStatus')))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
@ -112,7 +112,7 @@ class SectionBodyContent extends React.PureComponent {
const { updateUserStatus, onLoading, t } = this.props;
onLoading(true);
updateUserStatus(EmployeeStatus.Active, [user.id])
updateUserStatus(EmployeeStatus.Active, [user.id], isRefetchPeople)
.then(() => toastr.success(t('SuccessChangeUserStatus')))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
@ -153,12 +153,10 @@ class SectionBodyContent extends React.PureComponent {
resendUserInvites([user.id])
.then(() =>
toastr.success(
<Text>
<Trans i18nKey='MessageEmailActivationInstuctionsSentOnEmail' i18n={i18n}>
The email activation instructions have been sent to the
<strong>{{email: user.email}}</strong> email address
<Trans i18nKey='MessageEmailActivationInstuctionsSentOnEmail' i18n={i18n}>
The email activation instructions have been sent to the
<strong>{{ email: user.email }}</strong> email address
</Trans>
</Text>
)
)
.catch(error => toastr.error(error))
@ -317,97 +315,97 @@ class SectionBodyContent extends React.PureComponent {
const { dialogsVisible, user } = this.state;
return users == null
? (<Loader className="pageLoader" type="rombs" size='40px' />)
: users.length > 0 ? (
<>
<RowContainer useReactWindow={false}>
{users.map(user => {
const contextOptions = this.getUserContextOptions(user, viewer);
const contextOptionsProps = !contextOptions.length
? {}
: { contextOptions };
const checked = isUserSelected(selection, user.id);
const checkedProps = isAdmin(viewer) ? { checked } : {};
const element = (
<Avatar
size="small"
role={getUserRole(user)}
userName={user.displayName}
source={user.avatar}
/>
);
return (
<Row
key={user.id}
status={getUserStatus(user)}
data={user}
element={element}
onSelect={this.onContentRowSelect}
{...checkedProps}
{...contextOptionsProps}
needForUpdate={this.needForUpdate}
>
<UserContent
user={user}
history={history}
settings={settings}
? (<Loader className="pageLoader" type="rombs" size='40px' />)
: users.length > 0 ? (
<>
<RowContainer useReactWindow={false}>
{users.map(user => {
const contextOptions = this.getUserContextOptions(user, viewer);
const contextOptionsProps = !contextOptions.length
? {}
: { contextOptions };
const checked = isUserSelected(selection, user.id);
const checkedProps = isAdmin(viewer) ? { checked } : {};
const element = (
<Avatar
size="small"
role={getUserRole(user)}
userName={user.displayName}
source={user.avatar}
/>
</Row>
);
})}
</RowContainer>
);
{dialogsVisible.changeEmail &&
<ChangeEmailDialog
visible={dialogsVisible.changeEmail}
onClose={this.toggleChangeEmailDialog}
user={user}
/>
}
{dialogsVisible.changePassword &&
<ChangePasswordDialog
visible={dialogsVisible.changePassword}
onClose={this.toggleChangePasswordDialog}
email={user.email}
/>
}
return (
<Row
key={user.id}
status={getUserStatus(user)}
data={user}
element={element}
onSelect={this.onContentRowSelect}
{...checkedProps}
{...contextOptionsProps}
needForUpdate={this.needForUpdate}
>
<UserContent
user={user}
history={history}
settings={settings}
/>
</Row>
);
})}
</RowContainer>
{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>
</>
{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>
</>
}
/>
);
}
}

View File

@ -3,13 +3,12 @@ import { withRouter } from "react-router";
import { RowContent, Link, LinkWithDropdown, Icons, Text } from "asc-web-components";
import { connect } from "react-redux";
import { getUserStatus } from "../../../../../store/people/selectors";
import { useTranslation } from 'react-i18next';
import { history } from "asc-web-common";
const getFormatedGroups = (user, status) => {
let temp = [];
const groups = user.groups;
const linkColor = status === 'pending' ? '#D0D5DA' : '#A3A9AE';
const linkColor = status === 'disabled' ? '#D0D5DA' : '#A3A9AE';
if (!groups) temp.push({ key: 0, label: '' });
@ -73,9 +72,8 @@ const UserContent = ({ user, history, settings }) => {
[email]
);
const nameColor = status === 'pending' ? '#A3A9AE' : '#333333';
const sideInfoColor = ((status === 'pending') || (status === 'disabled')) ? '#D0D5DA' : '#A3A9AE';
//const { t } = useTranslation();
const nameColor = status === 'disabled' ? '#A3A9AE' : '#333333';
const sideInfoColor = status === 'disabled' ? '#D0D5DA' : '#A3A9AE';
const headDepartmentStyle = {
width: '110px'

View File

@ -27,6 +27,8 @@ const { isAdmin } = store.auth.selectors;
const { resendUserInvites, deleteUsers } = api.people;
const { EmployeeStatus, EmployeeType } = constants;
const isRefetchPeople = true;
const StyledContainer = styled.div`
@media (min-width: 1024px) {
@ -35,6 +37,15 @@ const StyledContainer = styled.div`
.group-button-menu-container {
margin: 0 -16px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@media (max-width: 1024px) {
& > div:first-child {
position: absolute;
top: 56px;
z-index: 180;
}
}
@media (min-width: 1024px) {
margin: 0 -24px;
@ -90,14 +101,20 @@ const SectionHeaderContent = props => {
//console.log("SectionHeaderContent render", selection, selectedUserIds);
const onSetActive = useCallback(() => {
updateUserStatus(EmployeeStatus.Active, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
}, [selectedUserIds, updateUserStatus, t]);
onLoading(true);
updateUserStatus(EmployeeStatus.Active, selectedUserIds, isRefetchPeople)
.then(() => toastr.success(t("SuccessChangeUserStatus")))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
}, [selectedUserIds, updateUserStatus, t, onLoading]);
const onSetDisabled = useCallback(() => {
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds);
toastr.success(t("SuccessChangeUserStatus"));
}, [selectedUserIds, updateUserStatus, t]);
onLoading(true);
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds, isRefetchPeople)
.then(() => toastr.success(t("SuccessChangeUserStatus")))
.catch(error => toastr.error(error))
.finally(() => onLoading(false));
}, [selectedUserIds, updateUserStatus, t, onLoading]);
const onSetEmployee = useCallback(() => {
updateUserType(EmployeeType.User, selectedUserIds);
@ -215,7 +232,7 @@ const SectionHeaderContent = props => {
history.push(`${settings.homepage}/group/create`);
}, [history, settings]);
const onInvitationDialogClick = useCallback(() =>
const onInvitationDialogClick = useCallback(() =>
setDialogVisible(!dialogVisible), [dialogVisible]
);
@ -268,51 +285,51 @@ const SectionHeaderContent = props => {
/>
</div>
) : (
<div className="header-container">
{group ? (
<>
<Headline className='headline-header' type="content" truncate={true}>{group.name}</Headline>
{isAdmin && (
<ContextMenuButton
className="action-button"
directionX="right"
title={t("Actions")}
iconName="VerticalDotsIcon"
size={16}
color="#A3A9AE"
getData={getContextOptionsGroup}
isDisabled={false}
/>
)}
</>
) : (
<>
<Headline className='headline-header' truncate={true} type="content">{settings.customNames.groupsCaption}</Headline>
{isAdmin && (
<>
<ContextMenuButton
className="action-button"
directionX="left"
title={t("Actions")}
iconName="PlusIcon"
size={16}
color="#657077"
getData={getContextOptionsPlus}
isDisabled={false}
/>
{dialogVisible &&
<InviteDialog
visible={dialogVisible}
onClose={onInvitationDialogClick}
onCloseButton={onInvitationDialogClick}
<div className="header-container">
{group ? (
<>
<Headline className='headline-header' type="content" truncate={true}>{group.name}</Headline>
{isAdmin && (
<ContextMenuButton
className="action-button"
directionX="right"
title={t("Actions")}
iconName="VerticalDotsIcon"
size={16}
color="#A3A9AE"
getData={getContextOptionsGroup}
isDisabled={false}
/>
}
)}
</>
) : (
<>
<Headline className='headline-header' truncate={true} type="content">{settings.customNames.groupsCaption}</Headline>
{isAdmin && (
<>
<ContextMenuButton
className="action-button"
directionX="right"
title={t("Actions")}
iconName="PlusIcon"
size={16}
color="#657077"
getData={getContextOptionsPlus}
isDisabled={false}
/>
{dialogVisible &&
<InviteDialog
visible={dialogVisible}
onClose={onInvitationDialogClick}
onCloseButton={onInvitationDialogClick}
/>
}
</>
)}
</>
)}
</>
)}
</div>
)}
</div>
)}
</StyledContainer>
);
};

View File

@ -12,7 +12,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,8 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import config from "../../../../package.json";
import { constants } from 'asc-web-common';
const { LANGUAGE } = constants;
const newInstance = i18n.createInstance();
@ -8,9 +10,8 @@ if (process.env.NODE_ENV === "production") {
newInstance
.use(Backend)
.init({
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
@ -33,7 +34,7 @@ if (process.env.NODE_ENV === "production") {
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,

View File

@ -1,7 +1,6 @@
import React from "react";
import { connect } from "react-redux";
import {
Text,
IconButton,
ContextMenuButton,
toastr,
@ -13,14 +12,18 @@ import {
getUserStatus,
toEmployeeWrapper
} from "../../../../../store/people/selectors";
import { withTranslation } from "react-i18next";
import { withTranslation, Trans } from "react-i18next";
import {
updateUserStatus
} from "../../../../../store/people/actions";
import {
updateProfile
} from "../../../../../store/profile/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';
import i18n from '../../i18n';
const { isAdmin, isMe } = store.auth.selectors;
const {
resendUserInvites,
@ -43,7 +46,7 @@ const StyledContainer = styled.div`
margin-left: auto;
& > div:first-child {
padding: 8px 16px 8px 16px;
padding: 8px 16px 8px 0px;
margin-right: -16px;
}
}
@ -171,18 +174,22 @@ class SectionHeaderContent extends React.PureComponent {
response.max +
"?_=" +
Math.floor(Math.random() * Math.floor(10000));
toastr.success("Success");
this.setState(stateCopy);
})
.catch(error => toastr.error(error))
.then(() => this.props.fetchProfile(this.state.profile.id));
.then(() => this.props.updateProfile(this.props.profile))
.then(() => this.props.fetchProfile(this.state.profile.id))
.then(() => toastr.success(this.props.t("ChangesApplied")))
.catch((error) => {
toastr.error(error);
});
} else {
deleteAvatar(this.state.profile.id)
.then(response => {
let stateCopy = Object.assign({}, this.state);
stateCopy.visibleAvatarEditor = false;
stateCopy.profile.avatarMax = response.big;
toastr.success("Success");
toastr.success(this.props.t('ChangesApplied'));
this.setState(stateCopy);
})
.catch(error => toastr.error(error));
@ -205,11 +212,13 @@ class SectionHeaderContent extends React.PureComponent {
};
onUpdateUserStatus = (status, userId) => {
const { fetchProfile, updateUserStatus } = this.props;
const { fetchProfile, updateUserStatus, t } = this.props;
updateUserStatus(status, new Array(userId)).then(() =>
fetchProfile(userId)
);
updateUserStatus(status, new Array(userId))
.then(() => this.props.updateProfile(this.props.profile))
.then(() => fetchProfile(userId))
.then(() => toastr.success(t('SuccessChangeUserStatus')))
.catch(error => toastr.error(error))
};
onDisableClick = () =>
@ -241,10 +250,10 @@ class SectionHeaderContent extends React.PureComponent {
resendUserInvites(new Array(this.state.profile.id))
.then(() =>
toastr.success(
<Text>
The email activation instructions have been sent to the{" "}
<b>{this.state.profile.email}</b> email address
</Text>
<Trans i18nKey='MessageEmailActivationInstuctionsSentOnEmail' i18n={i18n}>
The email activation instructions have been sent to the
<strong>{{ email: this.state.profile.email }}</strong> email address
</Trans>
)
)
.catch(error => toastr.error(error));
@ -415,6 +424,7 @@ class SectionHeaderContent extends React.PureComponent {
unknownTypeError={t("ErrorUnknownFileImageType")}
maxSizeFileError={t("maxSizeFileError")}
unknownError={t("Error")}
saveButtonLabel={t('SaveButton')}
/>
{dialogsVisible.deleteSelfProfile &&
@ -468,5 +478,5 @@ const mapStateToProps = state => {
export default connect(
mapStateToProps,
{ updateUserStatus, fetchProfile }
{ updateUserStatus, fetchProfile, updateProfile }
)(withRouter(withTranslation()(SectionHeaderContent)));

View File

@ -10,7 +10,6 @@ if (process.env.NODE_ENV === "production") {
newInstance.use(Backend).init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default

View File

@ -5,7 +5,7 @@ import { Loader, toastr } from "asc-web-components";
import { PageLayout, utils } from "asc-web-common";
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
import { SectionHeaderContent, SectionBodyContent } from './Section';
import { fetchProfile } from '../../../store/profile/actions';
import { fetchProfile, resetProfile } from '../../../store/profile/actions';
import i18n from "./i18n";
import { I18nextProvider, withTranslation } from "react-i18next";
const { changeLanguage } = utils;
@ -44,6 +44,10 @@ class PureProfile extends React.Component {
}
}
componentWillUnmount(){
this.props.resetProfile();
}
render() {
//console.log("Profile render")
@ -91,5 +95,5 @@ function mapStateToProps(state) {
}
export default connect(mapStateToProps, {
fetchProfile
fetchProfile, resetProfile
})(Profile);

View File

@ -27,6 +27,10 @@
"ErrorUnknownFileImageType": "Unknown image file type",
"Error": "Error",
"SocialProfiles": "Social profiles",
"SaveButton": "Save",
"ChangesApplied": "Changes are applied",
"SuccessChangeUserStatus": "The user status was successfully changed",
"MessageEmailActivationInstuctionsSentOnEmail": "The email activation instructions have been sent to the <strong>{{ email }}</strong> email address",
"PhoneChange": "Change phone",
"PhoneLbl": "Phone",

View File

@ -27,6 +27,10 @@
"ErrorUnknownFileImageType": "Неизвестный тип файла изображения",
"Error": "Ошибка",
"SocialProfiles": "Социальные профили",
"SaveButton": "Сохранить",
"ChangesApplied": "Изменения успешно применены",
"SuccessChangeUserStatus": "Статус пользователя успешно изменен",
"MessageEmailActivationInstuctionsSentOnEmail": "Инструкция по активации почты пользователя была отправлена по адресу <strong>{{ email }}</strong>",
"PhoneChange": "Изменить номер телефона",
"PhoneLbl": "Основной телефон",

View File

@ -348,6 +348,7 @@ class CreateUserForm extends React.Component {
unknownTypeError={t("ErrorUnknownFileImageType")}
maxSizeFileError={t("maxSizeFileError")}
unknownError ={t("Error")}
saveButtonLabel={t('SaveButton')}
/>
</AvatarContainer>
<MainFieldsContainer ref={this.mainFieldsContainerRef}>

View File

@ -4,7 +4,7 @@ import { connect } from 'react-redux'
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'
import { updateProfile, getUserPhoto, fetchProfile } from '../../../../../store/profile/actions'
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
import TextField from './FormFields/TextField'
import TextChangeField from './FormFields/TextChangeField'
@ -310,7 +310,9 @@ class UpdateUserForm extends React.Component {
toastr.success(this.props.t("ChangesSavedSuccessfully"));
this.setState(stateCopy);
})
.catch((error) => toastr.error(error));
.catch(error => toastr.error(error))
.then(() => this.props.updateProfile(this.props.profile))
.then(() => this.props.fetchProfile(this.state.profile.id))
} else {
deleteAvatar(this.state.profile.id)
.then((response) => {
@ -471,6 +473,7 @@ class UpdateUserForm extends React.Component {
unknownTypeError={t("ErrorUnknownFileImageType")}
maxSizeFileError={t("maxSizeFileError")}
unknownError={t("Error")}
saveButtonLabel={t('SaveButton')}
/>
</AvatarContainer>
<MainFieldsContainer ref={this.mainFieldsContainerRef}>
@ -687,6 +690,6 @@ const mapStateToProps = (state) => {
export default connect(
mapStateToProps,
{
updateProfile
updateProfile, fetchProfile
}
)(withRouter(withTranslation()(UpdateUserForm)));

View File

@ -12,7 +12,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,8 @@
import i18n from "i18next";
import Backend from "i18next-xhr-backend";
import config from "../../../../package.json";
import { constants } from 'asc-web-common';
const { LANGUAGE } = constants;
const newInstance = i18n.createInstance();
@ -8,9 +10,8 @@ if (process.env.NODE_ENV === "production") {
newInstance
.use(Backend)
.init({
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false // not needed for react as it escapes by default
@ -33,7 +34,7 @@ if (process.env.NODE_ENV === "production") {
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,

View File

@ -13,7 +13,7 @@ html, body {
.pageLoader {
position: fixed;
left: calc(50% - 32px);
left: calc(50% - 20px);
top: 35%;
}
}

View File

@ -99,7 +99,15 @@
"NotFoundLanguage",
"LearnMore",
"ErrorUnknownFileImageType",
"SaveButton",
"MessageEmailActivationInstuctionsSentOnEmail",
"Error"
],
"ResourceJS": [
"ChangesApplied"
],
"PeopleJSResource": [
"SuccessChangeUserStatus"
]
},
"ProfileAction": {

View File

@ -11,7 +11,6 @@ import {
PAGE,
PAGE_COUNT
} from "../../helpers/constants";
import unionBy from 'lodash/unionBy';
const { EmployeeStatus } = constants;
const { Filter } = api;
@ -119,7 +118,8 @@ export function setFilterUrl(filter) {
params.push(`${SORT_BY}=${filter.sortBy}`);
params.push(`${SORT_ORDER}=${filter.sortOrder}`);
if (params.length > 0) {
const isProfileView = history.location.pathname.includes('/people/view') || history.location.pathname.includes('/people/edit');
if (params.length > 0 && !isProfileView) {
history.push(`${config.homepage}/filter?${params.join("&")}`);
}
}
@ -160,14 +160,14 @@ export function fetchPeople(filter, dispatchFunc = null) {
return dispatchFunc
? fetchPeopleByFilter(dispatchFunc, filter)
: (dispatch, getState) => {
if (filter) {
return fetchPeopleByFilter(dispatch, filter);
} else {
const { people } = getState();
const { filter } = people;
return fetchPeopleByFilter(dispatch, filter);
}
};
if (filter) {
return fetchPeopleByFilter(dispatch, filter);
} else {
const { people } = getState();
const { filter } = people;
return fetchPeopleByFilter(dispatch, filter);
}
};
}
function fetchPeopleByFilter(dispatch, filter) {
@ -189,15 +189,15 @@ function fetchPeopleByFilter(dispatch, filter) {
});
}
export function updateUserStatus(status, userIds) {
export function updateUserStatus(status, userIds, isRefetchPeople = false) {
return (dispatch, getState) => {
return api.people.updateUserStatus(status, userIds).then(users => {
const { people } = getState();
const { users: currentUsers } = people;
const newUsers = unionBy(users, currentUsers, "id");
dispatch(setUsers(newUsers));
return api.people.updateUserStatus(status, userIds)
.then(users => {
const { people } = getState();
const { filter } = people;
return isRefetchPeople
? fetchPeople(filter, dispatch)
: Promise.resolve();
});
};
}

View File

@ -12,7 +12,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -11,7 +11,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -11,7 +11,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: false,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -11,7 +11,6 @@ if (process.env.NODE_ENV === "production") {
.init({
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -12,7 +12,7 @@ html, body {
.pageLoader {
position: fixed;
left: calc(50% - 32px);
left: calc(50% - 20px);
top: 35%;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-common",
"version": "1.0.63",
"version": "1.0.72",
"description": "Ascensio System SIA common components and solutions library",
"license": "AGPL-3.0",
"files": [

View File

@ -16,6 +16,7 @@ import Section from "../../../.storybook/decorators/section";
import {Button, Avatar, Text} from "asc-web-components";
import isEqual from "lodash/isEqual";
import { name, image, internet } from "faker";
import UserTooltip from "../PeopleSelector/sub-components/UserTooltip";
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
@ -223,39 +224,7 @@ class ADSelectorExample extends React.Component {
// console.log("onOptionTooltipShow", index, user);
return (
<div
style={{
width: 253,
minHeight: 63,
display: "grid",
gridTemplateColumns: "30px 1fr",
gridTemplateRows: "1fr",
gridColumnGap: 8
}}
>
<Avatar
size="small"
role="user"
source={user.avatarUrl}
userName=""
editing={false}
/>
<div>
<Text isBold={true} fontSize="13px" fontWeight={700}>
{user.label}
</Text>
<Text
color="#A3A9AE"
fontSize="13px"
style={{ paddingBottom: 8 }}
>
{user.email}
</Text>
<Text fontSize="13px" fontWeight={600}>{user.position}</Text>
</div>
</div>
);
return (<UserTooltip avatarUrl={user.avatarUrl} label={user.label} email={user.email} position={user.position} />);
}}
/>
</div>

View File

@ -317,7 +317,10 @@ const Selector = props => {
color="#D8D8D8"
getContent={getOptionTooltipContent}
place="top"
offsetLeft={160}
offsetLeft={150}
offsetRight={0}
offsetTop={60}
offsetBottom={0}
dataTip={`${index}`}
displayType="dropdown"
/>
@ -342,7 +345,10 @@ const Selector = props => {
color="#D8D8D8"
getContent={getOptionTooltipContent}
place="top"
offsetLeft={160}
offsetLeft={150}
offsetRight={0}
offsetTop={60}
offsetBottom={0}
dataTip={`${index}`}
displayType="dropdown"
/>
@ -651,36 +657,39 @@ const Selector = props => {
</Body>
</Column>
{displayType === "dropdown" && groups && groups.length > 0 && (
<Column className="column-groups" displayType={displayType} size={size}>
<Header className="header-groups">
<Text
as="p"
className="group_header"
fontSize="15px"
fontWeight={600}
>
{groupsHeaderLabel}
</Text>
</Header>
<Body className="body-groups">
<AutoSizer>
{({ height, width }) => (
<List
className="group_list"
height={height}
width={width + 8}
itemSize={32}
itemCount={groups.length}
itemData={groups}
outerElementType={CustomScrollbarsVirtualList}
ref={listGroupsRef}
>
{renderGroup}
</List>
)}
</AutoSizer>
</Body>
</Column>
<>
<div className="splitter"></div>
<Column className="column-groups" displayType={displayType} size={size}>
<Header className="header-groups">
<Text
as="p"
className="group_header"
fontSize="15px"
fontWeight={600}
>
{groupsHeaderLabel}
</Text>
</Header>
<Body className="body-groups">
<AutoSizer>
{({ height, width }) => (
<List
className="group_list"
height={height}
width={width + 8}
itemSize={32}
itemCount={groups.length}
itemData={groups}
outerElementType={CustomScrollbarsVirtualList}
ref={listGroupsRef}
>
{renderGroup}
</List>
)}
</AutoSizer>
</Body>
</Column>
</>
)}
<Footer
className="footer"

View File

@ -17,22 +17,20 @@ const Container = ({
const dropdownStyles = css`
grid-auto-rows: max-content;
grid-template-areas: "column-options column-groups" "footer footer";
${props =>
props.groups && props.groups.length > 0
? css`grid-template-areas: "column-options splitter column-groups" "footer footer footer"`
: css`grid-template-areas: "column-options column-groups" "footer footer"`
};
.column-groups {
box-sizing: border-box;
grid-area: column-groups;
${props =>
props.groups && props.groups.length > 0
? css`
border-left: 1px solid #eceef1;
`
: ""}
display: grid;
/* background-color: gold; */
padding: 0 16px 0 16px;
padding: 16px 16px 0 16px;
grid-row-gap: 2px;
grid-template-columns: 1fr;
@ -72,6 +70,17 @@ const dropdownStyles = css`
}
}
}
${props =>
props.groups && props.groups.length > 0 &&
css `
.splitter {
grid-area: splitter;
border-left: 1px solid #eceef1;
margin-top: 16px;
}
`
}
`;
const asideStyles = css`
@ -94,15 +103,13 @@ const StyledSelector = styled(Container)`
${props => (props.displayType === "dropdown" ? dropdownStyles : asideStyles)}
padding-top: 16px;
.column-options {
grid-area: column-options;
box-sizing: border-box;
display: grid;
/* background-color: red; */
padding: 0 16px 0 16px;
padding: 16px 16px 0 16px;
grid-row-gap: 2px;
grid-template-columns: 1fr;
@ -193,11 +200,21 @@ const StyledSelector = styled(Container)`
height: 32px;
cursor: pointer;
.option_checkbox {
width: 265px;
}
.option-info {
position: absolute;
top: 10px;
right: 10px;
padding: 8px 0 8px 8px;
margin-top: -8px;
}
/* .__react_component_tooltip {
left: 8px !important;
} */
}
}
}

View File

@ -18,7 +18,6 @@ newInstance.init({
resources: resources,
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -17,6 +17,11 @@ const StyledNav = styled.nav`
.profile-menu {
right: 12px;
@media ${tablet} {
right: 6px;
top: 66px;
}
}
& > div {

View File

@ -1,8 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { Avatar, DropDown, DropDownItem, utils, Link } from "asc-web-components";
import { Avatar, DropDownItem, Link } from "asc-web-components";
import ProfileMenu from "./../../ProfileMenu";
const { handleAnyClick } = utils.event;
class ProfileActions extends React.PureComponent {
constructor(props) {
@ -14,42 +13,20 @@ class ProfileActions extends React.PureComponent {
opened: props.opened,
user: props.user
};
this.handleClick = this.handleClick.bind(this);
this.toggle = this.toggle.bind(this);
this.getUserRole = this.getUserRole.bind(this);
this.onAvatarClick = this.onAvatarClick.bind(this);
this.onDropDownItemClick = this.onDropDownItemClick.bind(this);
if (props.opened) handleAnyClick(true, this.handleClick);
}
handleClick = e => {
this.state.opened &&
!this.ref.current.contains(e.target) &&
this.toggle(false);
};
toggle = opened => {
setOpened = opened => {
this.setState({ opened: opened });
};
componentWillUnmount() {
handleAnyClick(false, this.handleClick);
}
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps) {
if (this.props.user !== prevProps.user) {
this.setState({ user: this.props.user })
}
if (this.props.opened !== prevProps.opened) {
this.toggle(this.props.opened);
}
if (this.state.opened !== prevState.opened) {
handleAnyClick(this.state.opened, this.handleClick);
this.setOpened(this.props.opened);
}
}
@ -62,56 +39,52 @@ class ProfileActions extends React.PureComponent {
return "user";
};
onAvatarClick = () => {
this.toggle(!this.state.opened);
};
onClose = (e) => {
if (this.ref.current.contains(e.target)) return;
onDropDownItemClick = action => {
action.onClick && action.onClick();
this.toggle(!this.state.opened);
};
this.setOpened(!this.state.opened);
}
onClick = (action, e) => {
action.onClick && action.onClick(e);
this.setOpened(!this.state.opened);
}
render() {
//console.log("Layout sub-component ProfileActions render");
const { user, opened } = this.state;
const userRole = this.getUserRole(user);
return (
<div ref={this.ref}>
<Avatar
onClick={this.onClick}
role={userRole}
size="small"
role={this.getUserRole(this.state.user)}
source={this.state.user.avatarSmall}
userName={this.state.user.displayName}
onClick={this.onAvatarClick}
source={user.avatarSmall}
userName={user.displayName}
/>
<DropDown
className='profile-menu'
directionX="right"
open={this.state.opened}
clickOutsideAction={this.onAvatarClick}
<ProfileMenu
className="profile-menu"
avatarRole={userRole}
avatarSource={user.avatarMedium}
displayName={user.displayName}
email={user.email}
open={opened}
clickOutsideAction={this.onClose}
>
<ProfileMenu
avatarRole={this.getUserRole(this.state.user)}
avatarSource={this.state.user.avatarMedium}
displayName={this.state.user.displayName}
email={this.state.user.email}
/>
{this.props.userActions.map(action => (
<Link
noHover={true}
key={action.key}
href={action.url}
onClick={(e) => {
if (e) {
this.onDropDownItemClick.bind(this, action);
e.preventDefault();
}
}
}
>
<DropDownItem
{...action} />
//onClick={this.onClick.bind(this, action)}
>
<DropDownItem {...action} />
</Link>
))}
</DropDown>
</ProfileMenu>
</div>
);
}

View File

@ -18,7 +18,6 @@ newInstance.init({
resources: resources,
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -24,6 +24,32 @@ class PageLayoutComponent extends React.PureComponent {
this.state = this.mapPropsToState(props);
}
componentDidMount() {
window.addEventListener("orientationchange", this.orientationChangeHandler);
}
componentWillUnmount() {
window.removeEventListener("orientationchange", this.orientationChangeHandler);
}
orientationChangeHandler = () => {
const articleElement = document.getElementsByTagName('article')[0];
if (!articleElement) return;
const isOrientationVertical = !(screen.orientation ? screen.orientation.angle % 180 : window.matchMedia("(orientation: portrait)"));
const isValueExist = !!localStorage.getItem(ARTICLE_PINNED_KEY);
const articleWidth = articleElement.offsetWidth;
const isArticleWide = articleWidth > screen.availWidth - articleWidth;
if (isOrientationVertical && isArticleWide && isValueExist) {
this.backdropClick();
}
if (!isOrientationVertical && isValueExist) {
this.pinArticle();
}
}
componentDidUpdate(prevProps) {
if (this.hasChanges(this.props, prevProps)) {
this.setState(this.mapPropsToState(this.props));
@ -197,7 +223,7 @@ const PageLayout = props => {
}
PageLayout.propTypes = {
language:PropTypes.string,
language: PropTypes.string,
}
PageLayoutComponent.propTypes = {

View File

@ -9,6 +9,7 @@ const StyledSectionToggler = styled.div`
position: fixed;
bottom: 0;
display: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@media ${tablet} {
display: ${props => (props.visible ? "block" : "none")};

View File

@ -18,7 +18,6 @@ newInstance.init({
resources: resources,
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,26 +1,20 @@
import styled from "styled-components";
const StyledUserTooltip = styled.div`
width: 253px;
width: 233px;
min-height: 63px;
display: "grid";
grid-template-columns: "30px 1fr";
grid-template-rows: "1fr";
grid-column-gap: 8px;
display: grid;
grid-template-columns: 33px 1fr;
grid-template-rows: 1fr;
grid-column-gap: 12px;
.email-text {
padding-bottom: 8px;
}
.block-info {
display: grid;
grid-template-rows: 1fr;
.block-avatar-name {
display: flex;
align-items: center;
padding-bottom: 8px;
}
.user-avatar {
margin-right: 10px;
min-width: 32px;
.email-text {
padding-bottom: 8px;
}
}
`;

View File

@ -6,7 +6,7 @@ import StyledUserTooltip from "./StyledUserTooltip";
const UserTooltip = ({ avatarUrl, label, email, position }) => (
<StyledUserTooltip>
<div className='block-avatar-name'>
<div className='block-avatar'>
<Avatar
className='user-avatar'
size="small"
@ -15,16 +15,16 @@ const UserTooltip = ({ avatarUrl, label, email, position }) => (
userName=""
editing={false}
/>
<Text isBold={true} fontSize="13px" fontWeight={700}>
</div>
<div className='block-info'>
<Text isBold={true} fontSize="13px" fontWeight={700} truncate={true} title={label}>
{label}
</Text>
</div>
<div>
<Text color="#A3A9AE" fontSize="13px" className="email-text">
<Text color="#A3A9AE" fontSize="13px" className="email-text" truncate={true} title={email}>
{email}
</Text>
<Text fontSize="13px" fontWeight={600}>
<Text fontSize="13px" fontWeight={600} truncate={true} title={position}>
{position}
</Text>
</div>

View File

@ -1,55 +1,78 @@
import React, { memo } from "react";
import React from "react";
import PropTypes from 'prop-types';
import { Avatar } from 'asc-web-components';
import { Avatar, DropDown } from 'asc-web-components';
import {
StyledProfileMenu,
MenuContainer,
AvatarContainer,
MainLabelContainer,
LabelContainer,
MainLabelContainer,
MenuContainer,
StyledProfileMenu,
TopArrow
} from "./StyledProfileMenu";
// eslint-disable-next-line react/display-name
const ProfileMenu = memo(props => {
const {
displayName,
email,
avatarRole,
avatarSource
} = props;
class ProfileMenu extends React.Component {
return (
<StyledProfileMenu {...props}>
<MenuContainer {...props}>
<AvatarContainer>
<Avatar
size='medium'
role={avatarRole}
source={avatarSource}
userName={displayName}
/>
</AvatarContainer>
<MainLabelContainer>
{displayName}
</MainLabelContainer>
<LabelContainer>
{email}
</LabelContainer>
</MenuContainer>
<TopArrow />
</StyledProfileMenu>
);
});
constructor(props) {
super(props);
}
render() {
const {
avatarRole,
avatarSource,
children,
className,
displayName,
email,
clickOutsideAction,
open
} = this.props;
return (
<DropDown
className={className}
directionX='right'
open={open}
clickOutsideAction={clickOutsideAction}
>
<StyledProfileMenu>
<MenuContainer>
<AvatarContainer>
<Avatar
size='medium'
role={avatarRole}
source={avatarSource}
userName={displayName}
/>
</AvatarContainer>
<MainLabelContainer>
{displayName}
</MainLabelContainer>
<LabelContainer>
{email}
</LabelContainer>
</MenuContainer>
<TopArrow />
</StyledProfileMenu>
{children}
</DropDown>
);
}
}
ProfileMenu.displayName = 'ProfileMenu';
ProfileMenu.propTypes = {
avatarRole: PropTypes.oneOf(['owner', 'admin', 'guest', 'user']),
avatarSource: PropTypes.string,
children: PropTypes.any,
className: PropTypes.string,
displayName: PropTypes.string,
email: PropTypes.string,
id: PropTypes.string,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array])
open: PropTypes.bool,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
clickOutsideAction: PropTypes.func,
};
export default ProfileMenu

View File

@ -1,25 +1,60 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs, text, select} from '@storybook/addon-knobs/react';
import { withKnobs, text, select } from '@storybook/addon-knobs/react';
import { BooleanValue } from 'react-values'
import ProfileMenu from '.';
import Section from '../../../.storybook/decorators/section';
import withReadme from 'storybook-readme/with-readme';
import Readme from './README.md';
import { DropDownItem, Avatar } from 'asc-web-components';
const roleOptions = ['owner', 'admin','guest','user'];
const roleOptions = ['owner', 'admin', 'guest', 'user'];
const defaultAvatar = 'https://static-www.onlyoffice.com/images/team/developers_photos/personal_44_2x.jpg';
storiesOf('Components|ProfileMenu', module)
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('base', () => (
<Section>
<ProfileMenu
avatarRole={select('avatarRole', roleOptions, 'admin')}
avatarSource={text('avatarSource','') || defaultAvatar}
displayName={text('displayName','') || 'Jane Doe'}
email={text('email','') || 'janedoe@gmail.com'}
/>
</Section>
));
.add('base', () => {
const userRole = select('avatarRole', roleOptions, 'admin');
const userAvatar = text('avatarSource', '') || defaultAvatar;
const userEmail = text('email', '') || 'janedoe@gmail.com';
const userDisplayName = text('displayName', '') || 'Jane Doe';
return (
<Section >
<BooleanValue>
{({ value, toggle }) => (
<div style={{
position: 'relative',
float: 'right',
height: '56px',
paddingRight: '4px'
}}>
<Avatar
size='medium'
role={userRole}
source={userAvatar}
userName={userDisplayName}
onClick={() => toggle(!value)}
/>
<ProfileMenu
avatarRole={userRole}
avatarSource={userAvatar}
displayName={userDisplayName}
email={userEmail}
open={value}
>
<DropDownItem key='1' label='Profile' onClick={() => console.log('Profile click')} />
<DropDownItem key='2' label='Subscriptions' onClick={() => console.log('Subscriptions click')} />
<DropDownItem key='sep' isSeparator />
<DropDownItem key='3' label='About this program' onClick={() => console.log('About click')} />
<DropDownItem key='4' label='Log out' onClick={() => console.log('Log out click')} />
</ProfileMenu>
</div>
)}
</BooleanValue>
</Section>)
});

View File

@ -6,12 +6,11 @@ import Layout from "../Layout";
class PureStudioLayout extends React.Component {
shouldComponentUpdate(nextProps) {
if (this.props.availableModules && nextProps.availableModules &&
!utils.array.isArrayEqual(nextProps.availableModules, this.props.availableModules)) {
!utils.array.isArrayEqual(nextProps.modules, this.props.modules)) {
return true;
}
return this.props.hasChanges !== nextProps.hasChanges ||
this.props.currentModuleId !== nextProps.currentModuleId ||
this.props.language !== nextProps.language;
this.props.currentModuleId !== nextProps.currentModuleId
}
onProfileClick = () => {

View File

@ -82,7 +82,8 @@ function mapStateToProps(state) {
availableModules: getAvailableModules(state.auth.modules, state.auth.user),
currentUser: state.auth.user,
currentModuleId: state.auth.settings.currentProductId,
settings: state.auth.settings
settings: state.auth.settings,
modules: state.auth.modules
};
}

View File

@ -18,7 +18,6 @@ newInstance.init({
resources: resources,
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const newInstance = i18n.createInstance();
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const newInstance = i18n.createInstance();
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const newInstance = i18n.createInstance();
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const newInstance = i18n.createInstance();
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const newInstance = i18n.createInstance();
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

@ -1,6 +1,7 @@
import i18n from "i18next";
import en from "./locales/en/translation.json";
import ru from "./locales/ru/translation.json";
import { LANGUAGE } from '../../constants';
const newInstance = i18n.createInstance();
@ -15,9 +16,8 @@ const resources = {
newInstance.init({
resources: resources,
lng: 'en',
lng: localStorage.getItem(LANGUAGE) || 'en',
fallbackLng: "en",
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default

View File

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

View File

@ -19,6 +19,10 @@ const StyledComboBox = styled.div`
position: relative;
outline: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
.dropdown-container {
padding: ${props => props.advancedOptions && `6px 0px`};
}
`;
class ComboBox extends React.Component {
@ -139,6 +143,7 @@ class ComboBox extends React.Component {
/>
{displayType !== 'toggle' &&
<DropDown
className='dropdown-container'
directionX={directionX}
directionY={directionY}
manualY='102%'

View File

@ -68,6 +68,12 @@ class ContextMenu extends React.PureComponent {
this.setState({ visible: false });
}
itemClick = (action, e) => {
action && action(e);
this.setState({ visible: false });
}
render() {
const { visible } = this.state;
const { options, id, className, style } = this.props;
@ -82,7 +88,7 @@ class ContextMenu extends React.PureComponent {
>
{options.map((item) => {
if (item && item.key !== undefined) {
return <DropDownItem key={item.key} {...item} />
return <DropDownItem key={item.key} {...item} onClick={this.itemClick.bind(this, item.onClick)} />
}
})}
</DropDown>

View File

@ -27,7 +27,7 @@ const disabledAndHeaderStyle = css`
const StyledDropdownItem = styled.div`
display: block;
width: 100%;
max-width: 240px;
max-width: 500px;
border: 0px;
cursor: pointer;
margin: 0px;

View File

@ -34,7 +34,7 @@ const StyledDropdown = styled.div`
-moz-box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.13);
-webkit-box-shadow: 0px 5px 20px rgba(0, 0, 0, 0.13);
padding: ${props => !props.maxHeight && `6px 0px`};
padding: ${props => !props.maxHeight && props.children && props.children.length > 1 && `6px 0px`};
`;
// eslint-disable-next-line react/display-name, react/prop-types
@ -50,7 +50,6 @@ const Row = memo(({ data, index, style }) => {
});
class DropDown extends React.PureComponent {
constructor(props) {
super(props);
@ -83,12 +82,10 @@ class DropDown extends React.PureComponent {
else {
this.props.disableOnClickOutside();
}
}
}
handleClickOutside = e => {
//console.log(`DropDown handleClickOutside`, e);
this.toggleDropDown(e);
};
@ -101,16 +98,12 @@ class DropDown extends React.PureComponent {
const rects = this.dropDownRef.current.getBoundingClientRect();
const container = { width: window.innerWidth, height: window.innerHeight };
const left = rects.left < 0;
const right = rects.right > container.width;
let newDirection = {};
newDirection.directionX = left ? 'left' : right ? 'right' : this.props.directionX;
const left = rects.left < 0 && rects.width < container.width;
const right = rects.left < 250 && rects.left > rects.width && rects.width < container.width;
const x = left ? 'left' : right ? 'right' : this.props.directionX;
this.setState({
directionX: newDirection.directionX,
directionX: x,
width: rects.width
});
}
@ -152,19 +145,19 @@ class DropDown extends React.PureComponent {
DropDown.propTypes = {
children: PropTypes.any,
className: PropTypes.string,
clickOutsideAction: PropTypes.func,
directionX: PropTypes.oneOf(['left', 'right']),
directionY: PropTypes.oneOf(['bottom', 'top']),
disableOnClickOutside: PropTypes.func,
enableOnClickOutside: PropTypes.func,
id: PropTypes.string,
open: PropTypes.bool,
manualWidth: PropTypes.string,
manualX: PropTypes.string,
manualY: PropTypes.string,
maxHeight: PropTypes.number,
open: PropTypes.bool,
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
withBackdrop: PropTypes.bool,
clickOutsideAction: PropTypes.func,
enableOnClickOutside: PropTypes.func,
disableOnClickOutside: PropTypes.func
withBackdrop: PropTypes.bool
};
DropDown.defaultProps = {

View File

@ -1,7 +1,8 @@
import React from 'react';
import styled, {css} from 'styled-components';
import styled, { css } from 'styled-components';
import FilterButton from './filter-button';
import HideFilter from './hide-filter';
import throttle from 'lodash/throttle';
import ComboBox from '../combobox';
import CloseButton from './close-button';
import isEqual from 'lodash/isEqual';
@ -86,6 +87,9 @@ const StyledComboBox = styled(ComboBox)`
width: auto;
padding-left: 4px;
}
> div:last-child{
max-width: 220px;
}
.combo-button-label {
color: #555F65;
}
@ -136,7 +140,7 @@ class FilterItem extends React.Component {
noBorder={true}
opened={this.props.opened}
directionX='left'
toggleAction={(e,isOpen)=>{
toggleAction={(e, isOpen) => {
this.setState({
isOpen: isOpen
})
@ -145,7 +149,7 @@ class FilterItem extends React.Component {
: <StyledFilterName>{this.props.label}</StyledFilterName>
}
</StyledFilterItemContent>
<StyledCloseButtonBlock onClick={this.onClick} isDisabled={this.props.isDisabled} isClickable={true}>
<CloseButton
@ -165,8 +169,8 @@ FilterItem.propTypes = {
groupItems: PropTypes.array,
label: PropTypes.string,
groupLabel: PropTypes.string,
onClose:PropTypes.func,
onSelectFilterItem:PropTypes.func
onClose: PropTypes.func,
onSelectFilterItem: PropTypes.func
}
class FilterBlock extends React.Component {
@ -178,6 +182,8 @@ class FilterBlock extends React.Component {
openFilterItems: this.props.openFilterItems || []
};
this.throttledRender = throttle(this.onRender, 100);
}
onDeleteFilterItem = (key) => {
this.props.onDeleteFilterItem(key);
@ -252,6 +258,9 @@ class FilterBlock extends React.Component {
return result;
}
componentDidUpdate() {
this.throttledRender();
}
onRender = () => {
this.props.onRender();
}
shouldComponentUpdate(nextProps, nextState) {

View File

@ -12,7 +12,7 @@ import clone from 'lodash/clone';
const StyledFilterInput = styled.div`
width: 100%;
min-width: 255px;
&:after {
content: " ";
display: block;

View File

@ -1,6 +1,6 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import { withKnobs } from "@storybook/addon-knobs/react";
import { withKnobs, number } from "@storybook/addon-knobs/react";
import withReadme from "storybook-readme/with-readme";
import Readme from "./README.md";
import HelpButton from ".";
@ -28,6 +28,10 @@ storiesOf("Components|Buttons", module)
<IconButtons>
<HelpButton
displayType="dropdown"
offsetTop={number("offsetTop", 0)}
offsetRight={number("offsetRight", 0)}
offsetBottom={number("offsetBottom", 0)}
offsetLeft={number("offsetLeft", 0)}
tooltipContent={
<Text fontSize='13px'>
Paste you tooltip content here

View File

@ -11,14 +11,6 @@ import Heading from "../heading";
import throttle from "lodash/throttle";
import styled from "styled-components";
const HelpContainer = styled.div`
* {
white-space: unset;
overflow: unset;
text-overflow: unset;
}
`;
const Content = styled.div`
box-sizing: border-box;
position: relative;
@ -132,6 +124,8 @@ class HelpButton extends React.Component {
const {
tooltipContent,
place,
offsetTop,
offsetBottom,
offsetRight,
offsetLeft,
zIndex,
@ -145,7 +139,7 @@ class HelpButton extends React.Component {
} = this.props;
return (
<HelpContainer ref={this.ref} style={style}>
<div ref={this.ref} style={style}>
<IconButton
id={this.id}
className={`${className} help-icon`}
@ -164,6 +158,8 @@ class HelpButton extends React.Component {
reference={this.refTooltip}
effect="solid"
place={place}
offsetTop={offsetTop}
offsetBottom={offsetBottom}
offsetRight={offsetRight}
offsetLeft={offsetLeft}
afterShow={this.afterShow}
@ -180,7 +176,6 @@ class HelpButton extends React.Component {
offsetLeft={offsetLeft}
afterShow={this.afterShow}
afterHide={this.afterHide}
getContent={getContent}
>
{tooltipContent}
</Tooltip>
@ -202,7 +197,7 @@ class HelpButton extends React.Component {
</Aside>
</>
)}
</HelpContainer>
</div>
);
}
}
@ -214,10 +209,12 @@ HelpButton.propTypes = {
]),
tooltipContent: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
offsetRight: PropTypes.number,
offsetLeft: PropTypes.number,
offsetTop: PropTypes.number,
offsetBottom: PropTypes.number,
tooltipMaxWidth: PropTypes.number,
tooltipId: PropTypes.string,
place: PropTypes.string,
offsetLeft: PropTypes.number,
zIndex: PropTypes.number,
displayType: PropTypes.oneOf(["dropdown", "aside", "auto"]),
helpButtonHeaderContent: PropTypes.string,
@ -234,6 +231,8 @@ HelpButton.defaultProps = {
place: "top",
offsetRight: 120,
offsetLeft: 0,
offsetTop: 0,
offsetBottom: 0,
zIndex: 310,
displayType: "auto",
className: "icon-button",

View File

@ -64,6 +64,7 @@ const StyledDropDown = styled(DropDown)`
`;
const StyledMainButton = styled.div`
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
position: relative;
display: block;
vertical-align: middle;

View File

@ -35,6 +35,7 @@ const StyledToastContainer = styled(ToastContainer)`
top: 1em;
right: 1em;
margin-top: 40px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
@media only screen and (max-width: 480px) {
width: 100vw;

View File

@ -23,12 +23,6 @@ const TooltipStyle = styled.div`
border: none;
}
}
* {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
class Tooltip extends Component {