Merge branch 'master' into feature/advanced-selector-v2

# Conflicts:
#	products/ASC.People/Client/yarn.lock
#	web/ASC.Web.Client/yarn.lock
This commit is contained in:
Alexey Safronov 2019-11-06 10:50:05 +03:00
commit 559c87a450
81 changed files with 4290 additions and 2972 deletions

View File

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

View File

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

View File

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

View File

@ -108,7 +108,7 @@ const SectionBodyContent = props => {
</EditButtonWrapper>
)}
</AvatarWrapper>
<ProfileInfo profile={profile} updateProfileCulture={updateProfileCulture} isSelf={isSelf} isAdmin={isAdmin} t={t} cultures={settings.cultures} culture={settings.culture} updateProfileCulture={updateProfileCulture} />
<ProfileInfo profile={profile} updateProfileCulture={updateProfileCulture} isSelf={isSelf} isAdmin={isAdmin} t={t} cultures={settings.cultures} culture={settings.culture} />
{isSelf && (
<ToggleWrapper isSelf={true} >
<ToggleContent label={t('Subscriptions')} isOpen={true} >

View File

@ -1,6 +1,16 @@
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { connect } from "react-redux";
import { Text, IconButton, ContextMenuButton, toastr, utils } from "asc-web-components";
import {
Text,
IconButton,
ContextMenuButton,
toastr,
utils,
TextInput,
Button,
ModalDialog,
AvatarEditor
} from "asc-web-components";
import { withRouter } from "react-router";
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
import { getUserStatus } from "../../../../../store/people/selectors";
@ -9,6 +19,13 @@ import { resendUserInvites } from "../../../../../store/services/api";
import { EmployeeStatus } from "../../../../../helpers/constants";
import { updateUserStatus } from "../../../../../store/people/actions";
import { fetchProfile } from '../../../../../store/profile/actions';
import {
sendInstructionsToChangePassword,
sendInstructionsToChangeEmail,
createThumbnailsAvatar,
loadAvatar,
deleteAvatar
} from "../../../../../store/services/api";
import styled from 'styled-components';
const wrapperStyle = {
@ -28,24 +45,180 @@ const Header = styled(Text.ContentHeader)`
const SectionHeaderContent = props => {
const { profile, history, settings, isAdmin, viewer, updateUserStatus, fetchProfile } = props;
const [newEmailState, setNewEmail] = useState(profile.email);
const [dialogState, setDialog] = useState(
{
visible: false,
header: "",
body: "",
buttons: [],
newEmail: profile.email,
});
const [avatarState, setAvatar] = useState(
{
tmpFile: "",
image: profile.avatarDefault ? "data:image/png;base64," + profile.avatarDefault : null,
defaultWidth: 0,
defaultHeight: 0
});
const [avatarEditorState, setAvatarEditorVisible] = useState(false);
const openAvatarEditor = () => {
let avatarDefault = profile.avatarDefault ? "data:image/png;base64," + profile.avatarDefault : null;
if (avatarDefault !== null) {
let img = new Image();
img.onload = function () {
setAvatar({
defaultWidth: img.width,
defaultHeight: img.height
})
};
img.src = avatarDefault;
}
setAvatarEditorVisible(true);
}
const onLoadFileAvatar = (file) => {
let data = new FormData();
data.append("file", file);
data.append("Autosave", false);
loadAvatar(profile.id, data)
.then((response) => {
var img = new Image();
img.onload = function () {
var stateCopy = Object.assign({}, avatarState);
stateCopy = {
tmpFile: response.data,
image: response.data,
defaultWidth: img.width,
defaultHeight: img.height
}
setAvatar(stateCopy);
};
img.src = response.data;
})
.catch((error) => toastr.error(error));
}
const onSaveAvatar = (isUpdate, result) => {
if (isUpdate) {
createThumbnailsAvatar(profile.id, {
x: Math.round(result.x * avatarState.defaultWidth - result.width / 2),
y: Math.round(result.y * avatarState.defaultHeight - result.height / 2),
width: result.width,
height: result.height,
tmpFile: avatarState.tmpFile
})
.then((response) => {
setAvatarEditorVisible(false);
setAvatar({ tmpFile: '' });
fetchProfile(profile.id);
toastr.success("Success");
})
.catch((error) => toastr.error(error));
} else {
deleteAvatar(profile.id)
.then((response) => {
setAvatarEditorVisible(false);
fetchProfile(profile.id);
toastr.success("Success");
})
.catch((error) => toastr.error(error));
}
}
const onCloseAvatarEditor = () => setAvatarEditorVisible(false);
const onEmailChange = event => {
const emailRegex = /.+@.+\..+/;
const newEmail = (event && event.target.value) || newEmailState;
const hasError = !emailRegex.test(newEmail);
const dialog = {
visible: true,
header: "Change email",
body: (
<Text.Body>
<span style={{ display: "block", marginBottom: "8px" }}>The activation instructions will be sent to the entered email</span>
<TextInput
id="new-email"
scale={true}
isAutoFocussed={true}
value={newEmail}
onChange={onEmailChange}
hasError={hasError}
/>
</Text.Body>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={onSendEmailChangeInstructions}
isDisabled={hasError}
/>
],
newEmail: newEmail
};
setDialog(dialog);
}
const onSendEmailChangeInstructions = () => {
sendInstructionsToChangeEmail(profile.id, newEmailState)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(onDialogClose);
}
const onPasswordChange = () => {
const dialog = {
visible: true,
header: "Change password",
body: (
<Text.Body>
Send the password change instructions to the <a href={`mailto:${profile.email}`}>{profile.email}</a> email address
</Text.Body>
),
buttons: [
<Button
key="SendBtn"
label="Send"
size="medium"
primary={true}
onClick={onSendPasswordChangeInstructions}
/>
]
};
setDialog(dialog);
}
const onSendPasswordChangeInstructions = () => {
sendInstructionsToChangePassword(profile.email)
.then((res) => {
toastr.success(res);
})
.catch((error) => toastr.error(error))
.finally(onDialogClose);
}
const onDialogClose = () => {
const dialog = { visible: false, newEmailState: profile.email };
setDialog(dialog);
}
const selectedUserIds = new Array(profile.id);
const onEditClick = () => {
history.push(`${settings.homepage}/edit/${profile.userName}`);
};
const onChangePasswordClick = () => {
toastr.success("Context action: Change password");
};
const onChangePhoneClick = () => {
toastr.success("Context action: Change phone");
};
const onChangeEmailClick = () => {
toastr.success("Context action: Change e-mail");
};
const onDisableClick = () => {
updateUserStatus(EmployeeStatus.Disabled, selectedUserIds)
.then(() => toastr.success(t("SuccessChangeUserStatus")))
@ -53,7 +226,7 @@ const SectionHeaderContent = props => {
};
const onEditPhoto = () => {
toastr.success("Context action: Edit Photo");
openAvatarEditor();
};
const onEnableClick = () => {
@ -81,7 +254,6 @@ const SectionHeaderContent = props => {
.catch(error => toastr.error(error));
};
const getUserContextOptions = (user, viewer, t) => {
let status = "";
if (isAdmin || (!isAdmin && isMe(user, viewer.userName))) {
@ -98,30 +270,33 @@ const SectionHeaderContent = props => {
onClick: onEditClick
},
{
key: "edit-photo",
label: t('EditPhoto'),
onClick: onEditPhoto
key: "change-password",
label: t('PasswordChangeButton'),
onClick: onPasswordChange
},
{
key: "change-email",
label: t('EmailChangeButton'),
onClick: onChangeEmailClick
onClick: onEmailChange
},
{
key: "change-phone",
label: t('PhoneChange'),
onClick: onChangePhoneClick
key: "edit-photo",
label: t('EditPhoto'),
onClick: onEditPhoto
},
{
key: "change-password",
label: t('PasswordChangeButton'),
onClick: onChangePasswordClick
},
{
key: "disable",
label: t('DisableUserButton'),
onClick: onDisableClick
}
isMe(user, viewer.userName)
? viewer.isOwner
? {}
: {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: onDeleteProfileClick
}
: {
key: "disable",
label: t("DisableUserButton"),
onClick: onDisableClick
}
];
case "disabled":
return [
@ -158,25 +333,38 @@ const SectionHeaderContent = props => {
label: t('EditButton'),
onClick: onEditClick
},
{
key: "edit-photo",
label: t('EditPhoto'),
onClick: onEditPhoto
},
{
key: "invite-again",
label: t('InviteAgainLbl'),
onClick: onInviteAgainClick
},
{
key: "disable",
label: t('DisableUserButton'),
onClick: onDisableClick
key: "edit-photo",
label: t('EditPhoto'),
onClick: onEditPhoto
},
!isMe(user, viewer.userName) &&
(user.status === EmployeeStatus.Active
? {
key: "disable",
label: t("DisableUserButton"),
onClick: onDisableClick
}
: {
key: "enable",
label: t("EnableUserButton"),
onClick: onEnableClick
}),
isMe(user, viewer.userName) && {
key: "delete-profile",
label: t("DeleteSelfProfile"),
onClick: onDeleteProfileClick
}
];
default:
return [];
}
};
const { t } = useTranslation();
@ -200,7 +388,7 @@ const SectionHeaderContent = props => {
{profile.displayName}
{profile.isLDAP && ` (${t('LDAPLbl')})`}
</Header>
{(isAdmin || isMe(viewer, profile.userName)) && (
{((isAdmin && !profile.isOwner) || isMe(viewer, profile.userName)) && (
<ContextMenuButton
directionX="right"
title={t('Actions')}
@ -211,6 +399,25 @@ const SectionHeaderContent = props => {
isDisabled={false}
/>
)}
<ModalDialog
visible={dialogState.visible}
headerContent={dialogState.header}
bodyContent={dialogState.body}
footerContent={dialogState.buttons}
onClose={onDialogClose}
/>
<AvatarEditor
image={avatarState.image}
visible={avatarEditorState}
onClose={onCloseAvatarEditor}
onSave={onSaveAvatar}
onLoadFile={onLoadFileAvatar}
headerLabel={t("editAvatar")}
chooseFileLabel={t("chooseFileLabel")}
unknownTypeError={t("unknownTypeError")}
maxSizeFileError={t("maxSizeFileError")}
unknownError={t("unknownError")}
/>
</div>
);
};

View File

@ -36,5 +36,11 @@
"Culture_en": "English (United Kingdom)",
"Culture_en-US": "English (United States)",
"Culture_ru-RU": "Russian (Russia)",
"LearnMore": "Learn more..."
"LearnMore": "Learn more...",
"chooseFileLabel": "Drop file here, or click to select file",
"unknownTypeError": "Unknown image file type",
"maxSizeFileError": "Maximum file size exceeded",
"unknownError": "Error",
"editAvatar": "Edit photo"
}

View File

@ -36,5 +36,11 @@
"Culture_en": "Английский (Великобритания)",
"Culture_en-US": "Английский (США)",
"Culture_ru-RU": "Русский (Россия)",
"LearnMore": "Подробнее..."
"LearnMore": "Подробнее...",
"chooseFileLabel": "Перетащите файл сюда или нажмите, чтобы выбрать файл",
"unknownTypeError": "Неизвестный тип файла изображения",
"maxSizeFileError": "Превышен максимальный размер файла",
"unknownError": "Ошибка",
"editAvatar": "Изменить фотографию"
}

View File

@ -42,7 +42,7 @@ export function getPortalPasswordSettings() {
export function getUser(userId) {
return request({
method: "get",
url: "/people/@self.json"
url: `/people/${userId || "@self"}.json`
});
}

View File

@ -1802,10 +1802,12 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../../packages/asc-web-components":
version "1.0.147"
version "1.0.152"
dependencies:
email-addresses "^3.0.3"
moment "^2.24.0"
prop-types "^15.7.2"
punycode "^2.1.1"
rc-tree "^2.1.2"
react-autosize-textarea "^7.0.0"
react-avatar-editor "^11.0.7"
@ -3745,6 +3747,11 @@ elliptic@^6.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
email-addresses@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb"
integrity sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==
emoji-regex@^7.0.1, emoji-regex@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"

View File

@ -126,6 +126,7 @@ namespace ASC.Api.Settings
settings.Timezone = timeZone.Id;
settings.UtcOffset = timeZone.GetUtcOffset(DateTime.UtcNow);
settings.UtcHoursOffset = settings.UtcOffset.TotalHours;
settings.OwnerId = Tenant.OwnerId;
}
return settings;
@ -145,7 +146,7 @@ namespace ASC.Api.Settings
}
[Read("timezones")]
public IEnumerable<TimeZoneInfo> GetTimeZones()
public List<object> GetTimeZones()
{
var timeZones = TimeZoneInfo.GetSystemTimeZones().ToList();
@ -154,7 +155,72 @@ namespace ASC.Api.Settings
timeZones.Add(TimeZoneInfo.Utc);
}
return timeZones;
List<object> listOfTimezones = new List<object>();
foreach (var tz in timeZones.OrderBy(z => z.BaseUtcOffset))
{
var displayName = tz.DisplayName;
if (tz.StandardName.StartsWith("GMT") && !tz.StandardName.StartsWith("GMT "))
{
displayName = string.Format("(UTC{0}{1}) ", tz.BaseUtcOffset < TimeSpan.Zero ? "-" : "+", tz.BaseUtcOffset.ToString(@"hh\:mm")) + tz.Id;
}
listOfTimezones.Add(new TimezonesModel { Id = tz.Id, DisplayName = displayName });
}
return listOfTimezones;
}
[AllowAnonymous]
[Read("greetingsettings")]
public string GetGreetingSettings()
{
return Tenant.Name;
}
[Create("greetingsettings")]
public object SaveGreetingSettings(GreetingSettingsModel model)
{
try
{
SecurityContext.DemandPermissions(Tenant, SecutiryConstants.EditPortalSettings);
Tenant.Name = model.Title;
CoreContext.TenantManager.SaveTenant(Tenant);
MessageService.Send(MessageAction.GreetingSettingsUpdated);
return new { Status = 1, Message = Resource.SuccessfullySaveGreetingSettingsMessage };
}
catch (Exception e)
{
return new { Status = 0, Message = e.Message.HtmlEncode() };
}
}
[Create("greetingsettings/restore")]
public object RestoreGreetingSettings()
{
try
{
SecurityContext.DemandPermissions(Tenant, SecutiryConstants.EditPortalSettings);
TenantInfoSettings.Load().RestoreDefaultTenantName();
//_tenantInfoSettings.Save();
return new
{
Status = 1,
Message = Resource.SuccessfullySaveGreetingSettingsMessage,
CompanyName = CoreContext.TenantManager.GetCurrentTenant().Name
};
}
catch (Exception e)
{
return new { Status = 0, Message = e.Message.HtmlEncode() };
}
}
[Read("recalculatequota")]

View File

@ -0,0 +1,7 @@
namespace ASC.Web.Api.Models
{
public class GreetingSettingsModel
{
public string Title { get; set; }
}
}

View File

@ -53,6 +53,10 @@ namespace ASC.Api.Settings
[DataMember(EmitDefaultValue = false)]
public double UtcHoursOffset { get; set; }
[DataMember(EmitDefaultValue = false)]
public Guid OwnerId { get; set; }
public static SettingsWrapper GetSample()
{
return new SettingsWrapper
@ -61,8 +65,9 @@ namespace ASC.Api.Settings
Timezone = TimeZoneInfo.Utc.ToString(),
TrustedDomains = new List<string> { "mydomain.com" },
UtcHoursOffset = -8.5,
UtcOffset = TimeSpan.FromHours(-8.5)
};
UtcOffset = TimeSpan.FromHours(-8.5),
OwnerId = Guid.NewGuid()
};
}
}
}

View File

@ -0,0 +1,8 @@
namespace ASC.Web.Api.Models
{
public class TimezonesModel
{
public string Id { get; set; }
public string DisplayName { get; set; }
}
}

View File

@ -72,7 +72,7 @@ const getAvailableModules = (modules, currentUser) => {
id: "nav-separator-2"
},
{
id: 'testId',
id: 'settings',
title: 'Settings',
iconName: "SettingsIcon",
notifications: 0,
@ -102,7 +102,7 @@ function mapStateToProps(state) {
hasChanges: state.auth.isAuthenticated && state.auth.isLoaded,
availableModules: availableModules,
currentUser: state.auth.user,
currentModuleId: state.auth.settings.currentModuleId,
currentModuleId: state.auth.settings.currentProductId,
language: state.auth.user.cultureName || state.auth.settings.culture,
};
};

View File

@ -25,9 +25,9 @@ import { login } from "../../../store/auth/actions";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import i18n from "./i18n";
import { welcomePageTitle } from "./../../../helpers/customNames";
import { sendInstructionsToChangePassword } from "../../../store/services/api";
import SubModalDialog from "./sub-components/modal-dialog";
import { getGreetingSettings } from '../../../store/services/api';
const FormContainer = styled(Container)`
margin-top: 70px;
@ -106,6 +106,7 @@ const Form = props => {
const [email, setEmail] = useState("");
const [isDisabled, setIsDisabled] = useState(false);
const [isChecked, setIsisChecked] = useState(false);
const [greetingTitle, setGreetingTitle] = useState('');
const onClick = () => {
setOpenDialog(true);
@ -182,6 +183,13 @@ const Form = props => {
};
}, [onKeyPress, params, language]);
useEffect(() => {
getGreetingSettings()
.then((res) => {
setGreetingTitle(res)
});
}, []);
const onChangePassword = event => {
setPassword(event.target.value);
!passwordValid && setPasswordValid(true);
@ -212,7 +220,7 @@ const Form = props => {
top
/>
<CardTitle className="card-title">
{t("CustomWelcomePageTitle", { welcomePageTitle })}
{greetingTitle}
</CardTitle>
</Card>
</Col>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,66 @@
import React from "react";
import { withRouter } from "react-router";
import { Text, utils } from 'asc-web-components';
import styled from 'styled-components';
import { withTranslation } from 'react-i18next';
import { getKeyByLink, settingsTree, getTKeyByKey } from '../../../utils';
const Header = styled(Text.ContentHeader)`
margin-right: 16px;
max-width: calc(100vw - 430px);
@media ${utils.device.tablet} {
max-width: calc(100vw - 96px);
}
`;
class SectionHeaderContent extends React.Component {
constructor(props) {
super(props);
const { match, location } = props;
const fullSettingsUrl = match.url;
const locationPathname = location.pathname;
const fullSettingsUrlLength = fullSettingsUrl.length;
const resultPath = locationPathname.slice(fullSettingsUrlLength + 1);
const arrayOfParams = resultPath.split('/');
const key = getKeyByLink(arrayOfParams, settingsTree);
const header = getTKeyByKey(key, settingsTree);
this.state = {
header
};
}
componentDidUpdate() {
const { match, location } = this.props;
const fullSettingsUrl = match.url;
const locationPathname = location.pathname;
const fullSettingsUrlLength = fullSettingsUrl.length;
const resultPath = locationPathname.slice(fullSettingsUrlLength + 1);
const arrayOfParams = resultPath.split('/');
const key = getKeyByLink(arrayOfParams, settingsTree);
const header = getTKeyByKey(key, settingsTree);
header !== this.state.header && this.setState({ header });
}
render() {
const { t } = this.props;
const { header } = this.state;
return (
<Header truncate={true}>
{t(header)}
</Header>
);
}
};
export default withRouter(withTranslation()(SectionHeaderContent));

View File

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

View File

@ -0,0 +1,44 @@
import React, { Suspense, useEffect } from "react";
import { connect } from 'react-redux';
import { Loader, PageLayout } from "asc-web-components";
import i18n from "../i18n";
import { I18nextProvider } from "react-i18next";
import {
ArticleHeaderContent,
ArticleBodyContent
} from "./Article";
import { SectionHeaderContent } from './Section';
import { setCurrentProductId } from '../../../../store/auth/actions';
const Layout = ({ currentProductId, setCurrentProductId, language, children }) => {
useEffect(() => {
currentProductId !== 'settings' && setCurrentProductId('settings');
i18n.changeLanguage(language);
}, [language]);
return (
<I18nextProvider i18n={i18n}>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<PageLayout
withBodyScroll={true}
articleHeaderContent={<ArticleHeaderContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={<SectionHeaderContent />}
sectionBodyContent={children}
/>
</Suspense>
</I18nextProvider >
);
};
function mapStateToProps(state) {
return {
language: state.auth.user.cultureName
};
}
export default connect(mapStateToProps, { setCurrentProductId })(Layout);

View File

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

View File

@ -1,31 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import { Text, utils } from 'asc-web-components';
import styled from 'styled-components';
const Header = styled(Text.ContentHeader)`
margin-left: 16px;
margin-right: 16px;
max-width: calc(100vw - 430px);
@media ${utils.device.tablet} {
max-width: calc(100vw - 96px);
}
`;
const SectionHeaderContent = props => {
return (
<Header truncate={true}>
{props.header}
</Header>
);
};
function mapStateToProps(state) {
return {
header: state.auth.settings.settingsTree.selectedSubtitle
};
}
export default connect(mapStateToProps)(withRouter(SectionHeaderContent));

View File

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

View File

@ -0,0 +1,284 @@
import React from "react";
import { connect } from "react-redux";
import { withTranslation } from 'react-i18next';
import { FieldContainer, Text, ComboBox, Loader, Button, toastr, Link, TextInput } from "asc-web-components";
import { getCultures, setLanguageAndTime, getPortalTimezones } from '../../../../../store/auth/actions';
import { getGreetingTitle, setGreetingTitle, restoreGreetingTitle } from '../../../../../store/settings/actions';
import styled from 'styled-components';
import { Trans } from 'react-i18next';
const mapCulturesToArray = (cultures, t) => {
return cultures.map((culture) => {
return { key: culture, label: t(`Culture_${culture}`) };
});
};
const mapTimezonesToArray = (timezones) => {
return timezones.map((timezone) => {
return { key: timezone.id, label: timezone.displayName };
});
};
const findSelectedItemByKey = (items, selectedItemKey) => {
return items.find(item => item.key === selectedItemKey);
}
const StyledComponent = styled.div`
.margin-top {
margin-top: 20px;
}
.margin-left {
margin-left: 20px;
}
.settings-block {
margin-bottom: 70px;
}
.input-width {
width: 500px;
}
.dropdown-item-width {
& > div:first-child {
div:first-child{
max-width: 100%;
}
}
}
`;
class Customization extends React.Component {
constructor(props) {
super(props);
const { portalLanguage, portalTimeZoneId, rawCultures, rawTimezones, t, greetingSettings } = props;
const languages = mapCulturesToArray(rawCultures, t);
const timezones = mapTimezonesToArray(rawTimezones);
this.state = {
isLoadedData: false,
isLoading: false,
timezones,
timezone: findSelectedItemByKey(timezones, portalTimeZoneId),
languages,
language: findSelectedItemByKey(languages, portalLanguage),
greetingTitle: greetingSettings,
isLoadingGreeting: false,
}
}
componentDidMount() {
const { getCultures, portalLanguage, portalTimeZoneId, t, getPortalTimezones, getGreetingTitle } = this.props;
const { timezones, languages, greetingTitle } = this.state;
if (!greetingTitle.length) {
getGreetingTitle()
.then(() => this.setState({ greetingTitle: this.props.greetingSettings }));
}
if (!timezones.length && !languages.length) {
let languages;
getCultures()
.then(() => {
languages = mapCulturesToArray(this.props.rawCultures, t);
})
.then(() => getPortalTimezones())
.then(() => {
const timezones = mapTimezonesToArray(this.props.rawTimezones);
const timezone = findSelectedItemByKey(timezones, portalTimeZoneId);
const language = findSelectedItemByKey(languages, portalLanguage);
this.setState({ languages, language, timezones, timezone, isLoadedData: true });
});
}
else {
this.setState({ isLoadedData: true });
}
}
onLanguageSelect = (language) => {
this.setState({ language })
};
onTimezoneSelect = (timezone) => {
this.setState({ timezone })
};
onSaveLngTZSettings = () => {
const { setLanguageAndTime, t } = this.props;
this.setState({ isLoading: true }, function () {
setLanguageAndTime(this.state.language.key, this.state.timezone.key)
.then(() => {
this.setState({ isLoading: false })
toastr.success(t('SuccessfullySaveSettingsMessage'));
});
})
}
onChangeGreetingTitle = (e) => {
this.setState({ greetingTitle: e.target.value })
};
onSaveGreetingSettings = () => {
const { setGreetingTitle, t } = this.props;
this.setState({ isLoadingGreeting: true }, function () {
setGreetingTitle(this.state.greetingTitle)
.then(() => {
this.setState({ isLoadingGreeting: false })
toastr.success(t('SuccessfullySaveGreetingSettingsMessage'));
});
})
}
onRestoreGreetingSettings = () => {
const { restoreGreetingTitle, t } = this.props;
this.setState({ isLoadingGreeting: true }, function () {
restoreGreetingTitle()
.then(() => {
this.setState({
isLoadingGreeting: false,
greetingTitle: this.props.greetingSettings
})
toastr.success(t('SuccessfullySaveGreetingSettingsMessage'));
});
})
}
render() {
const { t, i18n } = this.props;
const { isLoadedData, languages, language, isLoading, timezones, timezone, greetingTitle, isLoadingGreeting } = this.state;
const supportEmail = "documentation@onlyoffice.com";
const tooltipLanguage =
<Text.Body fontSize={13}>
<Trans i18nKey="NotFoundLanguage" i18n={i18n}>
"In case you cannot find your language in the list of the
available ones, feel free to write to us at
<Link href="mailto:documentation@onlyoffice.com" isHovered={true}>
{{ supportEmail }}
</Link> to take part in the translation and get up to 1 year free of
charge."
</Trans>
{" "}
<Link isHovered={true} href="https://helpcenter.onlyoffice.com/ru/guides/become-translator.aspx">{t("LearnMore")}</Link>
</Text.Body>
console.log("CustomizationSettings render");
return (
!isLoadedData ?
<Loader className="pageLoader" type="rombs" size={40} />
: <>
<StyledComponent>
<div className='settings-block'>
<Text.Body fontSize={16}>{t('StudioTimeLanguageSettings')}</Text.Body>
<FieldContainer
id='fieldContainerLanguage'
className='margin-top'
labelText={`${t("Language")}:`}
tooltipContent={tooltipLanguage}
isVertical={true}>
<ComboBox
id='comboBoxLanguage'
options={languages}
selectedOption={language}
onSelect={this.onLanguageSelect}
isDisabled={isLoading}
noBorder={false}
scaled={false}
scaledOptions={true}
dropDownMaxHeight={300}
size='huge'
/>
</FieldContainer>
<FieldContainer
id='fieldContainerTimezone'
labelText={`${t("TimeZone")}:`}
isVertical={true}>
<ComboBox
id='comboBoxTimezone'
options={timezones}
selectedOption={timezone}
onSelect={this.onTimezoneSelect}
isDisabled={isLoading}
noBorder={false}
scaled={false}
scaledOptions={true}
dropDownMaxHeight={300}
size='huge'
className='dropdown-item-width'
/>
</FieldContainer>
<Button
id='btnSaveLngTZ'
className='margin-top'
primary={true}
size='medium'
label={t('SaveButton')}
isLoading={isLoading}
onClick={this.onSaveLngTZSettings}
/>
</div>
<div className='settings-block'>
<Text.Body fontSize={16}>{t('GreetingSettingsTitle')}</Text.Body>
<FieldContainer
id='fieldContainerWelcomePage'
className='margin-top'
labelText={`${t("GreetingTitle")}:`}
isVertical={true}>
<TextInput
className='input-width'
scale={true}
value={greetingTitle}
onChange={this.onChangeGreetingTitle}
isDisabled={isLoadingGreeting}
/>
</FieldContainer>
<Button
id='btnSaveGreetingSetting'
className='margin-top'
primary={true}
size='medium'
label={t('SaveButton')}
isLoading={isLoadingGreeting}
onClick={this.onSaveGreetingSettings}
/>
<Button
id='btnRestoreToDefault'
className='margin-top margin-left'
size='medium'
label={t('RestoreDefaultButton')}
isDisabled={isLoadingGreeting}
onClick={this.onRestoreGreetingSettings}
/>
</div>
</StyledComponent>
</>
);
}
};
function mapStateToProps(state) {
return {
portalLanguage: state.auth.settings.culture,
portalTimeZoneId: state.auth.settings.timezone,
language: state.auth.user.cultureName || state.auth.settings.culture,
rawTimezones: state.auth.settings.timezones,
rawCultures: state.auth.settings.cultures,
greetingSettings: state.settings.greetingSettings,
};
}
export default connect(mapStateToProps, {
getCultures, setLanguageAndTime, getPortalTimezones,
getGreetingTitle, setGreetingTitle, restoreGreetingTitle
})(withTranslation()(Customization));

View File

@ -0,0 +1,23 @@
import React, { lazy } from "react";
import { Route, Switch } from "react-router-dom";
import { withRouter } from "react-router";
const CustomizationSettings = lazy(() => import("./customization"));
const Common = ({ match }) => {
const basePath = '/settings/common';
return (
<Switch>
<Route
exact
path={[`${basePath}/customization`, '/common', match.path]}
component={CustomizationSettings}
/>
</Switch>
);
};
export default withRouter(Common);

View File

@ -0,0 +1,469 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import i18n from "../../i18n";
import { I18nextProvider, withTranslation } from "react-i18next";
import styled from "styled-components";
import {
changeAdmins,
getListAdmins,
getListUsers,
getUserById
} from "../../../../../store/settings/actions";
import {
getUsers,
getAdmins,
getSelectorOptions
} from "../../../../../store/settings/selectors";
import {
Text,
Avatar,
ToggleContent,
Row,
RowContent,
RowContainer,
Link,
RadioButtonGroup,
Paging,
SelectorAddButton,
IconButton,
AdvancedSelector,
toastr,
RequestLoader
} from "asc-web-components";
const MainContainer = styled.div`
padding: 16px 16px 16px 24px;
width: 100%;
.page_loader {
position: fixed;
left: 48%;
}
`;
const ProjectsContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
.display-block {
display: block;
}
div label:not(:first-child) {
margin: 0;
}
`;
const RadioButtonContainer = styled.div`
margin-right: 150px;
margin-bottom: 16px;
width: 310px;
`;
const HeaderContainer = styled.div`
margin-bottom: 16px;
`;
const BodyContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 24px;
`;
const AvatarContainer = styled.div`
display: flex;
width: 330px;
height: 120px;
margin-right: 130px;
margin-bottom: 24px;
padding: 8px;
border: 1px solid lightgrey;
.avatar_wrapper {
width: 100px;
height: 100px;
}
.avatar_body {
margin-left: 24px;
max-width: 190px;
word-wrap: break-word;
overflow: hidden;
}
`;
const ToggleContentContainer = styled.div`
.toggle_content {
margin-bottom: 24px;
}
.wrapper {
margin-top: 16px;
}
.advanced-selector {
position: relative;
}
.selector-button {
max-width: 34px;
}
`;
const ProjectsBody = styled.div`
width: 280px;
`;
class PureAccessRights extends Component {
constructor(props) {
super(props);
this.state = {
showSelector: false,
options: [],
isLoading: false,
selectedOptions: []
};
}
componentDidMount() {
const {
getListAdmins,
getListUsers,
getUserById,
ownerId,
productId
} = this.props;
getUserById(ownerId).catch(error => {
toastr.error(error);
//console.log("accessRights getUserById", error);
});
getListUsers().catch(error => {
toastr.error(error);
//console.log("accessRights getListAdmins", error);
});
getListAdmins(productId).catch(error => {
toastr.error(error);
//console.log("accessRights getListAdmins", error);
});
}
onChangeAdmin = (userId, isAdmin) => {
this.onLoading(true);
const { changeAdmins, productId } = this.props;
changeAdmins(userId, productId, isAdmin)
.catch(error => {
toastr.error("accessRights onChangeAdmin", error);
//console.log("accessRights onChangeAdmin", error)
})
.finally(() => this.onLoading(false));
};
onShowGroupSelector = () =>
this.setState({
showSelector: !this.state.showSelector,
options: this.props.options,
selectedOptions: []
});
onSelect = selected => {
selected.map(user => this.onChangeAdmin(user.key, true));
this.onShowGroupSelector();
this.setState({ selectedOptions: selected });
};
onSearchUsers = template => {
this.onLoading(true);
this.setState({
options: this.filterUserSelectorOptions(this.props.options, template)
});
this.onLoading(false);
};
filterUserSelectorOptions = (options, template) =>
options.filter(option => option.label.indexOf(template) > -1);
onLoading = status => {
console.log("onLoading status", status);
this.setState({ isLoading: status });
};
render() {
const { t, owner, admins } = this.props;
const { showSelector, options, selectedOptions, isLoading } = this.state;
const OwnerOpportunities = t("AccessRightsOwnerOpportunities").split("|");
const countItems = [
{ key: 25, label: t("CountPerPage", { count: 25 }) },
{ key: 50, label: t("CountPerPage", { count: 50 }) },
{ key: 100, label: t("CountPerPage", { count: 100 }) }
];
console.log("isLoading", isLoading);
return (
<MainContainer>
<RequestLoader
visible={isLoading}
zIndex={256}
loaderSize={16}
loaderColor={"#999"}
label={`${t("LoadingProcessing")} ${t("LoadingDescription")}`}
fontSize={12}
fontColor={"#999"}
className="page_loader"
/>
<HeaderContainer>
<Text.Body fontSize={18}>{t("PortalOwner")}</Text.Body>
</HeaderContainer>
<BodyContainer>
<AvatarContainer>
<Avatar
className="avatar_wrapper"
size="big"
role="owner"
userName={owner.userName}
source={owner.avatar}
/>
<div className="avatar_body">
<Text.Body className="avatar_text" fontSize={16} isBold={true}>
{owner.displayName}
</Text.Body>
{owner.groups &&
owner.groups.map(group => (
<Link fontSize={12} key={group.id} href={owner.profileUrl}>
{group.name}
</Link>
))}
</div>
</AvatarContainer>
<ProjectsBody>
<Text.Body className="portal_owner" fontSize={12}>
{t("AccessRightsOwnerCan")}:
</Text.Body>
<Text.Body fontSize={12}>
{OwnerOpportunities.map((item, key) => (
<li key={key}>{item};</li>
))}
</Text.Body>
</ProjectsBody>
</BodyContainer>
<ToggleContentContainer>
<ToggleContent
className="toggle_content"
label={t("AdminSettings")}
isOpen={true}
>
<SelectorAddButton
className="selector-button"
isDisabled={isLoading}
//title={showGroupSelectorButtonTitle}
onClick={this.onShowGroupSelector}
/>
<div className="advanced-selector">
<AdvancedSelector
displayType="dropdown"
isOpen={showSelector}
placeholder="placeholder"
options={options}
onSearchChanged={this.onSearchUsers}
//groups={groups}
isMultiSelect={true}
buttonLabel="Add members"
onSelect={this.onSelect}
onCancel={this.onShowGroupSelector}
onAddNewClick={() => console.log("onAddNewClick")}
selectAllLabel="selectorSelectAllText"
selectedOptions={selectedOptions}
/>
</div>
<div className="wrapper">
<RowContainer manualHeight={`${admins.length * 50}px`}>
{admins.map(user => {
const element = (
<Avatar
size="small"
role="admin"
userName={user.displayName}
source={user.avatarSmall}
/>
);
const nameColor =
user.status === "pending" ? "#A3A9AE" : "#333333";
return (
<Row
key={user.id}
status={user.status}
data={user}
element={element}
>
<RowContent disableSideInfo={true}>
<Link
containerWidth="120px"
type="page"
title={user.displayName}
isBold={true}
fontSize={15}
color={nameColor}
href={user.profileUrl}
>
{user.displayName}
</Link>
<div style={{ maxWidth: 120 }} />
<div style={{ marginLeft: "60px" }}>
<IconButton
size="16"
isDisabled={isLoading}
onClick={this.onChangeAdmin.bind(
this,
user.id,
false
)}
iconName={"CatalogTrashIcon"}
isFill={true}
isClickable={false}
/>
</div>
</RowContent>
</Row>
);
})}
</RowContainer>
</div>
{admins.length > 25 ? (
<div className="wrapper">
<Paging
previousLabel={t("PreviousPage")}
nextLabel={t("NextPage")}
openDirection="top"
displayItems={false}
countItems={countItems}
selectedPageItem={{ label: "1 of 1" }}
selectedCountItem={{ label: "25 per page" }}
previousAction={() => console.log("previousAction")}
nextAction={() => console.log("nextAction")}
onSelectPage={a => console.log(a)}
onSelectCount={a => console.log(a)}
//pageItems={pageItems}
//disablePrevious={!filter.hasPrev()}
//disableNext={!filter.hasNext()}
/>
</div>
) : null}
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("People")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", { product: t("People") })}:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", {
category: t("People")
})}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("ViewProfilesAndGroups")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
</ToggleContentContainer>
</MainContainer>
);
}
}
const AccessRightsContainer = withTranslation()(PureAccessRights);
const AccessRights = props => {
const { language } = props;
i18n.changeLanguage(language);
return (
<I18nextProvider i18n={i18n}>
<AccessRightsContainer {...props} />
</I18nextProvider>
);
};
function mapStateToProps(state) {
const { ownerId } = state.auth.settings;
const { admins, users, owner } = state.settings;
const arrayUsers = getUsers(users, ownerId);
const filterArrayUsers = getAdmins(arrayUsers);
const options = getSelectorOptions(filterArrayUsers);
return {
users: filterArrayUsers,
admins: getUsers(admins, ownerId),
productId: state.auth.modules[0].id,
owner,
ownerId,
options
};
}
AccessRights.defaultProps = {
users: [],
admins: [],
productId: "",
ownerId: "",
owner: {},
options: []
};
AccessRights.propTypes = {
users: PropTypes.arrayOf(PropTypes.object),
admins: PropTypes.arrayOf(PropTypes.object),
productId: PropTypes.string,
ownerId: PropTypes.string,
owner: PropTypes.object,
options: PropTypes.arrayOf(PropTypes.object)
};
export default connect(
mapStateToProps,
{ getUserById, changeAdmins, getListAdmins, getListUsers }
)(withRouter(AccessRights));

View File

@ -0,0 +1,23 @@
import React, { lazy } from "react";
import { Route, Switch } from "react-router-dom";
import { withRouter } from "react-router";
const AccessRightsSettings = lazy(() => import("./accessRights"));
const Security = () => {
const basePath = '/settings/security';
return (
<Switch>
<Route
exact
path={[`${basePath}/accessrights`, basePath]}
component={AccessRightsSettings}
/>
</Switch>
);
};
export default withRouter(Security);

View File

@ -1,31 +1,38 @@
import React, { Suspense } from "react";
import { Loader, PageLayout } from "asc-web-components";
import i18n from "./i18n";
import { I18nextProvider } from "react-i18next";
import {
ArticleHeaderContent,
ArticleBodyContent
} from "./Article";
import { SectionHeaderContent, SectionBodyContent } from './Section';
import React, { lazy } from "react";
import { Route, Switch, Redirect } from "react-router-dom";
import { withRouter } from "react-router";
import Layout from './Layout';
const CommonSettings = lazy(() => import("./categories/common"));
const SecuritySettings = lazy(() => import("./categories/security"));
const Settings = () => {
console.log("Settings render");
const basePath = '/settings';
return (
<I18nextProvider i18n={i18n}>
<Suspense
fallback={<Loader className="pageLoader" type="rombs" size={40} />}
>
<PageLayout
withBodyScroll={false}
articleHeaderContent={<ArticleHeaderContent />}
articleBodyContent={<ArticleBodyContent />}
sectionHeaderContent={<SectionHeaderContent />}
sectionBodyContent={<SectionBodyContent />}
<Layout key='1'>
<Switch>
<Route
path={`${basePath}/security`}
component={SecuritySettings}
/>
</Suspense>
</I18nextProvider >
<Route
path={[`${basePath}/common`, basePath]}
component={CommonSettings}
/>
<Redirect
to={{
pathname: "/error/404",
}}
/>
</Switch>
</Layout>
);
};
export default Settings;
export default withRouter(Settings);

View File

@ -16,6 +16,38 @@
"AccessRightsAllUsers": "All {{users}}",
"AccessRightsUsersFromList": "{{users}} from the list",
"Employees": "users",
"StudioTimeLanguageSettings": "Language and Time Zone Settings",
"Language": "Language",
"TimeZone": "Time Zone",
"SaveButton": "Save",
"SuccessfullySaveSettingsMessage": "Settings have been successfully updated",
"NotFoundLanguage": "In case you cannot find your language in the list of the available ones, feel free to write to us at <1>{{supportEmail}}</1> to take part in the translation and get up to 1 year free of charge.",
"Settings": "Settings",
"PreviousPage": "Previous",
"NextPage": "Next",
"ManagementCategoryCommon": "Common",
"Customization": "Customization",
"ProductsAndInstruments": "Modules & tools",
"WhiteLabel": "White label",
"ManagementCategorySecurity": "Security",
"PortalSecurity": "Portal Access",
"AccessRights": "Access Rights",
"LoginHistoryNav": "Login History",
"AuditTrailNav": "Audit Trail",
"DataManagement": "Data Management",
"Migration": "Migration",
"Backup": "Backup",
"DeactivationDeletionPortal": "Portal Deactivation/Deletion",
"ManagementCategoryIntegration": "Integration",
"ThirdPartyAuthorization": "Third Party Authorization",
"SmtpSettings": "SMTP Settings",
"ManagementCategoryStatistic": "Statistics",
"LoadingProcessing": "Loading...",
"LoadingDescription": "Please wait...",
"GreetingSettingsTitle": "Welcome Page Settings",
"GreetingTitle": "Title",
"RestoreDefaultButton": "Restore to Default",
"SuccessfullySaveGreetingSettingsMessage": "Welcome Page settings have been successfully saved",
@ -31,5 +63,10 @@
"Sample": "Sample",
"ManageOwnMailAccounts": "Manage own mail accounts, receive and send letters",
"ManageOwnMailSettings": "Manage own Mail settings",
"ManageTheTagsAndAddressBook": "Manage the tags and address book"
"ManageTheTagsAndAddressBook": "Manage the tags and address book",
"Culture_en": "English (United Kingdom)",
"Culture_en-US": "English (United States)",
"Culture_ru-RU": "Russian (Russia)",
"LearnMore": "Learn more...",
"CountPerPage": "{{count}} per page"
}

View File

@ -15,7 +15,38 @@
"AccessRightsAllUsers": "Всех участников со статусом {{users}}",
"AccessRightsUsersFromList": "Участников со статусом Пользователи из списка",
"Employees": "Пользователи",
"StudioTimeLanguageSettings": "Настройки языка и часового пояса",
"Language": "Язык",
"TimeZone": "Часовой пояс",
"SaveButton": "Сохранить",
"SuccessfullySaveSettingsMessage": "Настройки успешно обновлены",
"NotFoundLanguage": "Если Вы не можете найти свой язык в списке доступных, Вы всегда можете написать нам по адресу <1>{{supportEmail}}</1>, чтобы принять участие в переводе и получить до 1 года бесплатного использования.",
"Settings": "Настройки",
"PreviousPage": "Предыдущая",
"NextPage": "Следующая",
"ManagementCategoryCommon": "Общие",
"Customization": "Кастомизация",
"ProductsAndInstruments": "Модули и инструменты",
"WhiteLabel": "Ребрендинг",
"ManagementCategorySecurity": "Безопасность",
"PortalSecurity": "Доступ к порталу",
"AccessRights": "Права доступа",
"LoginHistoryNav": "История входов в систему",
"AuditTrailNav": "Журнал аудита",
"DataManagement": "Управление данными",
"Migration": "Миграция",
"Backup": "Резервное копирование",
"DeactivationDeletionPortal": "Деактивация/Удаление портала",
"ManagementCategoryIntegration": "Интеграция",
"ThirdPartyAuthorization": "Сторонние сервисы",
"SmtpSettings": "Настройки SMTP",
"ManagementCategoryStatistic": "Статистика",
"LoadingProcessing": "Загрузка...",
"LoadingDescription": "Пожалуйста подождите...",
"GreetingSettingsTitle": "Настройки страницы приветствия",
"GreetingTitle": "Заголовок",
"RestoreDefaultButton": "Настройки по умолчанию",
"SuccessfullySaveGreetingSettingsMessage": "Настройки страницы приветствия успешно сохранены",
@ -31,5 +62,10 @@
"Sample": "Sample",
"ManageOwnMailAccounts": "Управлять своими учетными записями почты, получать и отправлять письма",
"ManageOwnMailSettings": "Управлять своими настройками Почты",
"ManageTheTagsAndAddressBook": "Управлять тегами и адресной книгой"
"ManageTheTagsAndAddressBook": "Управлять тегами и адресной книгой",
"Culture_en": "Английский (Великобритания)",
"Culture_en-US": "Английский (США)",
"Culture_ru-RU": "Русский (Россия)",
"LearnMore": "Подробнее...",
"CountPerPage": "{{count}} на странице"
}

View File

@ -1,137 +0,0 @@
import React from "react";
import { connect } from "react-redux";
import { withTranslation } from 'react-i18next';
import { FieldContainer, Text, ComboBox, Loader, Button, toastr } from "asc-web-components";
import { getCultures, setLanguageAndTime } from '../../../../../store/auth/actions';
import { getPortalTimezones } from '../../../../../store/services/api';
import styled from 'styled-components'
const StyledComponent = styled.div`
.margin-top {
margin-top: 20px;
}
`;
class Customization extends React.Component {
constructor() {
super();
this.state = {
isLoadedData: false,
isLoading: false,
timezones: [],
timezone: {},
languages: [],
language: {},
}
}
componentDidMount() {
const { getCultures, portalLanguage, portalTimeZoneId } = this.props;
let languages, timezones;
getCultures()
.then((cultures) => {
languages = cultures.map((culture) => {
return { key: culture, label: culture };
});
return getPortalTimezones();
})
.then((timezonesArr) => {
timezones = timezonesArr.map((timezone) => {
return { key: timezone.Id, label: timezone.DisplayName };
});
const language = languages.find(item => item.key === portalLanguage);
const timezone = timezones.find(item => item.key === portalTimeZoneId);
this.setState({ timezones, timezone, languages, language, isLoadedData: true });
}
);
}
onLanguageSelect = (language) => {
this.setState({ language })
};
onTimezoneSelect = (timezone) => {
this.setState({ timezone })
};
onSaveLngTZSettings = () => {
const { setLanguageAndTime } = this.props;
this.setState({ isLoading: true }, function () {
setLanguageAndTime(this.state.language.key, this.state.timezone.key)
.then(() => {
this.setState({ isLoading: false })
toastr.success('Settings have been successfully updated');
});
})
}
render() {
const { isLoadedData } = this.state;
const tooltipContent = 'Tooltip content 123456';
console.log("CustomizationSettings render");
return (
!isLoadedData ?
<Loader className="pageLoader" type="rombs" size={40} />
: <>
<StyledComponent>
<Text.Body fontSize={16}>Language and Time Zone Settings</Text.Body>
<FieldContainer
className='margin-top'
labelText="Language:"
tooltipContent={tooltipContent}
isVertical={true}>
<ComboBox
options={this.state.languages}
selectedOption={this.state.language}
onSelect={this.onLanguageSelect}
isDisabled={this.state.isLoading}
noBorder={false}
scaled={false}
scaledOptions={true}
size='huge'
/>
</FieldContainer>
<FieldContainer
labelText="Time Zone:"
tooltipContent={tooltipContent}
isVertical={true}>
<ComboBox
options={this.state.timezones}
selectedOption={this.state.timezone}
onSelect={this.onTimezoneSelect}
isDisabled={this.state.isLoading}
noBorder={false}
scaled={false}
scaledOptions={true}
dropDownMaxHeight={300}
size='huge'
/>
</FieldContainer>
<Button
className='margin-top'
primary={true}
size='big'
label='Save'
isLoading={this.state.isLoading}
onClick={this.onSaveLngTZSettings}
/>
</StyledComponent>
</>
);
}
};
function mapStateToProps(state) {
return {
portalLanguage: state.auth.settings.culture,
portalTimeZoneId: state.auth.settings.timezone
};
}
export default connect(mapStateToProps, { getCultures, setLanguageAndTime })(withTranslation()(Customization));

View File

@ -1,16 +0,0 @@
import React from "react";
class NotImplementedSettings extends React.Component {
render() {
//console.log("NotImplementedSettings render");
return (
<div>
Settings section in progress
</div>
);
}
};
export default NotImplementedSettings;

View File

@ -1,557 +0,0 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { withRouter } from "react-router";
import i18n from "../../i18n";
import { I18nextProvider, withTranslation } from "react-i18next";
import styled from "styled-components";
import { getAdminUsers } from "../../../../../store/people/actions";
import {
Text,
Avatar,
ToggleContent,
Row,
RowContent,
RowContainer,
RadioButtonGroup,
Link,
Checkbox
//toastr,
} from "asc-web-components";
const MainContainer = styled.div`
padding: 16px 16px 16px 24px;
width: 100%;
`;
const HeaderContainer = styled.div`
margin-bottom: 16px;
`;
const BodyContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
`;
const AvatarContainer = styled.div`
display: flex;
width: 330px;
height: 100px;
margin-right: 130px;
margin-bottom: 24px;
padding: 8px;
border: 1px solid lightgrey;
`;
const ToggleContentContainer = styled.div`
.toggle_content {
margin-bottom: 24px;
}
`;
const ProjectsContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: row;
flex-wrap: wrap;
.display-block {
display: block;
}
div label:not(:first-child) {
margin: 0;
}
`;
const RadioButtonContainer = styled.div`
margin-right: 150px;
margin-bottom: 16px;
width: 310px;
`;
const ProjectsBody = styled.div`
width: 280px;
`;
/*const AdministratorsHead = styled.div`
display: flex;
margin-right: 70px;
.category {
margin-left: 5px;
flex-basis: 12.5%; (1/elementsCount*100%)
text-align: center;
}
`;*/
class PureAccessRights extends Component {
constructor(props) {
super(props);
this.state = {
isChecked: this.props.isChecked,
portalOwner: null,
};
}
componentDidMount() {
const { getAdminUsers } = this.props;
getAdminUsers()
.then(res => {
console.log("getAdminUsers response", res);
})
.catch(error => {
console.log(error);
});
}
//componentDidUpdate(prevProps, prevState) {}
//componentWillUnmount() {}
onChange = e => {
//console.log(e.target.value);
//e.target.value = !e.target.value;
};
render() {
const { t } = this.props;
const { users } = this.props;
const { portalOwner } = this.state;
const OwnerOpportunities = t("AccessRightsOwnerOpportunities").split("|");
return (
<MainContainer>
<HeaderContainer>
<Text.Body fontSize={18}>{t("PortalOwner")}</Text.Body>
</HeaderContainer>
<BodyContainer>
<AvatarContainer>
<Avatar size="big" role="owner" />
{portalOwner ? (
<div style={{ marginLeft: "24px" }}>
<Text.Body className="avatar_text" fontSize={16} isBold={true}>
{portalOwner.displayName}
</Text.Body>
<Text.Body className="avatar_text" fontSize={12}>
{portalOwner.department}
</Text.Body>
</div>
) : null}
</AvatarContainer>
<ProjectsBody>
<Text.Body className="portal_owner" fontSize={12}>
{t("AccessRightsOwnerCan")}:
</Text.Body>
<Text.Body fontSize={12}>
{OwnerOpportunities.map((item, key) => (
<li key={key}>{item};</li>
))}
</Text.Body>
</ProjectsBody>
</BodyContainer>
<ToggleContentContainer>
<ToggleContent
className="toggle_content"
label={t("AdminSettings")}
isOpen={true}
>
<RowContainer manualHeight="200px">
{users.map(user => {
const element = (
<Avatar
size="small"
role="admin"
userName={user.userName}
source={user.avatar}
/>
);
const nameColor =
user.status === "pending" ? "#A3A9AE" : "#333333";
return (
<Row
key={user.id}
status={user.status}
checked={false}
data={user}
element={element}
//contextOptions={user.contextOptions}
>
<RowContent disableSideInfo={true}>
<Link
containerWidth="120px"
type="page"
title={user.userName}
isBold={true}
fontSize={15}
color={nameColor}
href={user.profileUrl}
>
{user.userName}
</Link>
<div
/*containerWidth='120px'*/ style={{ maxWidth: 120 }}
></div>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`fullAccess_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`people_${user.id}`}
/>
</RowContent>
</Row>
);
})}
</RowContainer>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("ProjectsProduct")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", {
product: t("ProjectsProduct")
})}
:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", {
category: t("ProjectsProduct")
})}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("ProjectsUserCapabilityView")}</li>
<li>{t("ProjectsUserCapabilityCreate")}</li>
<li>{t("ProjectsUserCapabilityTrack")}</li>
<li>{t("ProjectsUserCapabilityForm")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("CrmProduct")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", {
product: t("CrmProduct")
})}
:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", {
category: t("CrmProduct")
})}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("CRMUserCapability")}</li>
<li>{t("CRMUserCapabilityEdit")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("CommunityProduct")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", {
product: t("CommunityProduct")
})}
:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", {
category: t("CommunityProduct")
})}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("CommunityUserCapability")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("People")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", { product: t("People") })}:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", { category: t("People") })}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("ViewProfilesAndGroups")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("Sample")}
isOpen={true}
>
<Text.Body fontSize={12}>
{t("AccessRightsDisabledProduct", { module: "Sample" })}
</Text.Body>
</ToggleContent>
<ToggleContent
className="toggle_content"
label={t("Mail")}
isOpen={true}
>
<ProjectsContainer>
<RadioButtonContainer>
<Text.Body>
{t("AccessRightsAccessToProduct", { product: t("Mail") })}:
</Text.Body>
<RadioButtonGroup
name="selectGroup"
selected="allUsers"
options={[
{
value: "allUsers",
label: t("AccessRightsAllUsers", {
users: t("Employees")
})
},
{
value: "usersFromTheList",
label: t("AccessRightsUsersFromList", {
users: t("Employees")
})
}
]}
className="display-block"
/>
</RadioButtonContainer>
<ProjectsBody>
<Text.Body className="projects_margin" fontSize={12}>
{t("AccessRightsProductUsersCan", { category: t("Mail") })}
</Text.Body>
<Text.Body fontSize={12}>
<li>{t("ManageOwnMailAccounts")}</li>
<li>{t("ManageOwnMailSettings")}</li>
<li>{t("ManageTheTagsAndAddressBook")}</li>
</Text.Body>
</ProjectsBody>
</ProjectsContainer>
</ToggleContent>
</ToggleContentContainer>
</MainContainer>
);
}
}
PureAccessRights.propTypes = {};
PureAccessRights.defaultProps = {
isChecked: false
};
const AccessRightsContainer = withTranslation()(PureAccessRights);
const AccessRights = props => {
const { language } = props;
i18n.changeLanguage(language);
return (
<I18nextProvider i18n={i18n}>
<AccessRightsContainer {...props} />
</I18nextProvider>
);
};
function mapStateToProps(state) {
return {
users: state.people.users
};
}
AccessRights.defaultProps = {
users: []
};
AccessRights.propTypes = {
users: PropTypes.arrayOf(PropTypes.object)
};
export default connect(
mapStateToProps,
{ getAdminUsers }
)(withRouter((AccessRights)));
/*
<AdministratorsHead>
<div style={{width: 250}}></div>
<Text.Body className="category">
{t("AccessRightsFullAccess")}
</Text.Body>
<Text.Body className="category">{t("DocumentsProduct")}</Text.Body>
<Text.Body className="category">{t("ProjectsProduct")}</Text.Body>
<Text.Body className="category">{t("CrmProduct")}</Text.Body>
<Text.Body className="category">{t("CommunityProduct")}</Text.Body>
<Text.Body className="category">{t("People")}</Text.Body>
<Text.Body className="category">{t("Sample")}</Text.Body>
<Text.Body className="category">{t("Mail")}</Text.Body>
</AdministratorsHead>
*/
/*
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`fullAccess_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`documents_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`projects_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`crm_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`community_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`people_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`sample_${user.id}`}
/>
<Checkbox
isChecked={false}
onChange={this.onChange}
id={`mail_${user.id}`}
/>
*/

View File

@ -0,0 +1,11 @@
export const checkForRoot = (key, treeData, depth = 0) => {
let newKey = key.slice(0, 1 + 2 * depth);
const item = treeData.find(item => item.key === newKey);
if (key === newKey) {
return item.children ? true : false;
}
else {
const depthLevel = depth + 1;
return checkForRoot(key, item.children, depthLevel);
}
}

View File

@ -0,0 +1,16 @@
export const getKeyByLink = (linkArr, data, index = 0) => {
const length = linkArr.length;
const currentElement = linkArr[index];
const item = data.find(item => item.link === currentElement);
if (index === length - 1 && item) {
return item.key;
}
else if (!item || !item.children) {
return '0';
}
else {
const newIndex = index + 1;
return getKeyByLink(linkArr, item.children, newIndex);
}
}

View File

@ -0,0 +1,11 @@
export const getSelectedLinkByKey = (key, treeData, depth = 0) => {
let newKey = key.slice(0, 1 + 2 * depth);
const item = treeData.find(item => item.key === newKey);
if (key === newKey) {
return '/' + item.link;
}
else {
const depthLevel = depth + 1;
return '/' + item.link + getSelectedLinkByKey(key, item.children, depthLevel);
}
}

View File

@ -0,0 +1,11 @@
export const getTKeyByKey = (key, treeData, depth = 0) => {
let newKey = key.slice(0, 1 + 2 * depth);
const item = treeData.find(item => item.key === newKey);
if (key === newKey) {
return item.tKey;
}
else {
const depthLevel = depth + 1;
return getTKeyByKey(key, item.children, depthLevel);
}
}

View File

@ -0,0 +1,5 @@
export { getKeyByLink } from './getKeyByLink';
export { settingsTree } from './settingsTree';
export { getTKeyByKey } from './getTKeyByKey';
export { getSelectedLinkByKey } from './getSelectedLinkByKey';
export { selectKeyOfTreeElement } from './selectKeyOfTreeElement';

View File

@ -0,0 +1,9 @@
export const selectFirstChildItem = (rootKey, data) => {
const item = data.find(item => item.key[0] === rootKey);
if (item.children) {
return selectFirstChildItem(rootKey, item.children);
}
else {
return [data[0].key];
}
}

View File

@ -0,0 +1,16 @@
import { checkForRoot } from './checkForRoot';
import { selectFirstChildItem } from './selectFirstChildItem';
export const selectKeyOfTreeElement = (value, settingsTree) => {
const selectedKey = value[0];
const isRootElementSelected = checkForRoot(selectedKey, settingsTree);
if (isRootElementSelected) {
const firstChildren = selectFirstChildItem(selectedKey[0], settingsTree);
return firstChildren;
}
else {
return value;
}
}

View File

@ -0,0 +1,152 @@
/**
* Array for generation current settings tree.
*/
export const settingsTree = [
{
key: '0',
icon: 'SettingsIcon',
link: 'common',
tKey: 'ManagementCategoryCommon',
children: [
{
key: '0-0',
icon: '',
link: 'customization',
tKey: 'Customization',
}
]
},
{
key: '1',
icon: 'SettingsIcon',
link: 'security',
tKey: 'ManagementCategorySecurity',
children: [
{
key: '1-0',
icon: '',
link: 'accessrights',
tKey: 'AccessRights',
},
]
},
];
/**
* Array for generation full settings tree, old structure.
param title is unused
param link also used as translation key
*/
export const settingsTreeFull = [
{
title: 'Common',
key: '0',
icon: 'SettingsIcon',
link: 'common',
children: [
{
title: 'Customization',
key: '0-0',
icon: '',
link: 'customization',
},
{
title: 'Modules & tools',
key: '0-1',
icon: '',
link: 'modules-and-tools',
},
{
title: 'White label',
key: '0-2',
icon: '',
link: 'white-label',
},
]
},
{
title: 'Security',
key: '1',
icon: 'SettingsIcon',
link: 'security',
children: [
{
title: 'Portal Access',
key: '1-0',
icon: '',
link: 'portal-access',
},
{
title: 'Access Rights',
key: '1-1',
icon: '',
link: 'access-rights',
},
{
title: 'Login History',
key: '1-2',
icon: '',
link: 'login-history',
},
{
title: 'Audit Trail',
key: '1-3',
icon: '',
link: 'audit-trail',
},
]
},
{
title: 'Data Management',
key: '2',
icon: 'SettingsIcon',
link: 'data-management',
children: [
{
title: 'Migration',
key: '2-0',
icon: '',
link: 'migration',
},
{
title: 'Backup',
key: '2-1',
icon: '',
link: 'backup',
},
{
title: 'Portal Deactivation/Deletion',
key: '2-2',
icon: '',
link: 'portal-deactivation-deletion',
},
]
},
{
title: 'Integration',
key: '3',
icon: 'SettingsIcon',
link: 'integration',
children: [
{
title: 'Third-Party Services',
key: '3-0',
icon: '',
link: 'third-party-services',
},
{
title: 'SMTP Settings',
key: '3-1',
icon: '',
link: 'smtp-settings',
}
]
},
{
title: 'Statistics',
key: '4',
icon: 'SettingsIcon',
link: 'statistics',
},
];

View File

@ -26,7 +26,8 @@
"ForgotPassword",
"PasswordRecoveryTitle",
"MessageSendPasswordRecoveryInstructionsOnEmail",
"RememberHelper"
"RememberHelper",
"LoadingDescription"
]
},
"Confirm": {
@ -68,6 +69,30 @@
"AccessRightsDisabledProduct",
"AccessRightsAllUsers",
"AccessRightsUsersFromList",
"StudioTimeLanguageSettings",
"Language",
"TimeZone",
"SaveButton",
"NotFoundLanguage",
"ManagementCategoryCommon",
"Customization",
"ProductsAndInstruments",
"WhiteLabel",
"ManagementCategorySecurity",
"PortalSecurity",
"AccessRights",
"ManagementCategoryIntegration",
"ManagementCategoryStatistic",
"DataManagement",
"Migration",
"Backup",
"DeactivationDeletionPortal",
"ThirdPartyAuthorization",
"SmtpSettings",
"GreetingSettingsTitle",
"GreetingTitle",
"RestoreDefaultButton",
"SuccessfullySaveGreetingSettingsMessage",
"Employees"
],
"FeedResource": [
@ -75,6 +100,17 @@
"CrmProduct",
"CommunityProduct",
"DocumentsProduct"
],
"PeopleResource": [
"Settings"
],
"AuditResource": [
"LoginHistoryNav",
"AuditTrailNav"
],
"UserControlsCommonResource": [
"NextPage",
"PreviousPage"
]
}
},

View File

@ -10,9 +10,10 @@ export const LOGOUT = 'LOGOUT';
export const SET_PASSWORD_SETTINGS = 'SET_PASSWORD_SETTINGS';
export const SET_IS_CONFIRM_LOADED = 'SET_IS_CONFIRM_LOADED';
export const SET_NEW_EMAIL = 'SET_NEW_EMAIL';
export const SET_NEW_SETTING_NODE = 'SET_NEW_SETTING_NODE';
export const GET_PORTAL_CULTURES = 'GET_PORTAL_CULTURES';
export const SET_PORTAL_LANGUAGE_AND_TIME = 'SET_PORTAL_LANGUAGE_AND_TIME';
export const GET_TIMEZONES = 'GET_TIMEZONES';
export const SET_CURRENT_PRODUCT_ID = 'SET_CURRENT_PRODUCT_ID';
export function setCurrentUser(user) {
return {
@ -69,13 +70,6 @@ export function setNewEmail(email) {
};
};
export function setNewSelectedNode(selectedNodeData) {
return {
type: SET_NEW_SETTING_NODE,
selectedNodeData
};
};
export function getPortalCultures(cultures) {
return {
type: GET_PORTAL_CULTURES,
@ -90,6 +84,20 @@ export function setPortalLanguageAndTime(newSettings) {
};
};
export function getTimezones(timezones) {
return {
type: GET_TIMEZONES,
timezones
};
};
export function setCurrentProductId(currentProductId) {
return {
type: SET_CURRENT_PRODUCT_ID,
currentProductId
};
};
export function getUser(dispatch) {
return api.getUser()
.then(user => dispatch(setCurrentUser(user)));
@ -196,7 +204,6 @@ export function getCultures() {
return api.getPortalCultures()
.then(cultures => {
dispatch(getPortalCultures(cultures));
return cultures;
}
);
};
@ -208,3 +215,12 @@ export function setLanguageAndTime(lng, timeZoneID) {
.then(() => dispatch(setPortalLanguageAndTime({ lng, timeZoneID })));
};
}
export function getPortalTimezones() {
return dispatch => {
return api.getPortalTimezones()
.then((timezones) => {
dispatch(getTimezones(timezones))
});
};
};

View File

@ -1,122 +1,10 @@
import {
SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS, SET_IS_CONFIRM_LOADED, SET_NEW_EMAIL, SET_NEW_SETTING_NODE,
GET_PORTAL_CULTURES, SET_PORTAL_LANGUAGE_AND_TIME
SET_CURRENT_USER, SET_MODULES, SET_SETTINGS, SET_IS_LOADED, LOGOUT, SET_PASSWORD_SETTINGS, SET_IS_CONFIRM_LOADED, SET_NEW_EMAIL,
GET_PORTAL_CULTURES, SET_PORTAL_LANGUAGE_AND_TIME, GET_TIMEZONES, SET_CURRENT_PRODUCT_ID
} from './actions';
import isEmpty from 'lodash/isEmpty';
import config from "../../../package.json";
const settingsTree = [
{
title: 'Common',
key: '0',
icon: 'SettingsIcon',
link: 'common',
children: [
{
title: 'Customization',
key: '0-0',
icon: '',
link: 'customization',
},
{
title: 'Modules & tools',
key: '0-1',
icon: '',
link: 'modules-and-tools',
},
{
title: 'White label',
key: '0-2',
icon: '',
link: 'white-label',
},
]
},
{
title: 'Security',
key: '1',
icon: 'SettingsIcon',
link: 'security',
children: [
{
title: 'Portal Access',
key: '1-0',
icon: '',
link: 'portal-access',
},
{
title: 'Access Rights',
key: '1-1',
icon: '',
link: 'access-rights',
},
{
title: 'Login History',
key: '1-2',
icon: '',
link: 'login-history',
},
{
title: 'Audit Trail',
key: '1-3',
icon: '',
link: 'audit-trail',
},
]
},
{
title: 'Data Management',
key: '2',
icon: 'SettingsIcon',
link: 'data-management',
children: [
{
title: 'Migration',
key: '2-0',
icon: '',
link: 'migration',
},
{
title: 'Backup',
key: '2-1',
icon: '',
link: 'backup',
},
{
title: 'Portal Deactivation/Deletion',
key: '2-2',
icon: '',
link: 'portal-deactivation-deletion',
},
]
},
{
title: 'Integration',
key: '3',
icon: 'SettingsIcon',
link: 'integration',
children: [
{
title: 'Third-Party Services',
key: '3-0',
icon: '',
link: 'third-party-services',
},
{
title: 'SMTP Settings',
key: '3-1',
icon: '',
link: 'smtp-settings',
}
]
},
{
title: 'Statistics',
key: '4',
icon: 'SettingsIcon',
link: 'statistics',
},
];
const initialState = {
isAuthenticated: false,
@ -127,9 +15,11 @@ const initialState = {
settings: {
currentProductId: "home",
culture: "en-US",
cultures: [],
trustedDomains: [],
trustedDomainsType: 1,
timezone: "UTC",
timezones: [],
utcOffset: "00:00:00",
utcHoursOffset: 0,
homepage: config.homepage,
@ -142,11 +32,7 @@ const initialState = {
timePattern: "h:mm tt"
},
settingsTree: {
list: settingsTree,
selectedKey: ['0-0'],
selectedTitle: 'Common',
selectedSubtitle: 'Customization',
selectedLink: '/common/customization',
selectedKey: ['0-0']
}
}/*,
password: null*/
@ -187,14 +73,18 @@ const authReducer = (state = initialState, action) => {
return Object.assign({}, state, {
user: { ...state.user, email: action.email }
});
case SET_NEW_SETTING_NODE:
return Object.assign({}, state, {
settings: { ...state.settings, settingsTree: { ...state.settings.settingsTree, ...action.selectedNodeData } }
});
case SET_PORTAL_LANGUAGE_AND_TIME:
return Object.assign({}, state, {
settings: { ...state.settings, culture: action.newSettings.lng, timezone: action.newSettings.timeZoneID }
});
case GET_TIMEZONES:
return Object.assign({}, state, {
settings: { ...state.settings, timezones: action.timezones }
});
case SET_CURRENT_PRODUCT_ID:
return Object.assign({}, state, {
settings: { ...state.settings, currentProductId: action.currentProductId }
});
case LOGOUT:
return initialState;
default:

View File

@ -1,17 +0,0 @@
import * as api from "../services/api";
export const SET_USERS = "SET_USERS";
export function setUsers(users) {
return {
type: SET_USERS,
users
};
}
export function getAdminUsers() {
return dispatch => {
return api.getUserList()
.then(users => dispatch(setUsers(users)));
};
}

View File

@ -1,27 +0,0 @@
import { SET_USERS } from "./actions";
const initialState = {
users: [],
//groups: [],
//selection: [],
//selected: "none",
//selectedGroup: null,
//filter: Filter.getDefault(),
//selector: {
// users: []
//}
};
const peopleReducer = (state = initialState, action) => {
switch (action.type) {
case SET_USERS:
return Object.assign({}, state, {
users: action.users
});
default:
return state;
}
};
export default peopleReducer;

View File

@ -1,10 +1,10 @@
import { combineReducers } from 'redux';
import authReducer from './auth/reducer';
import peopleReducer from './people/reducer';
import peopleReducer from './settings/reducer';
const rootReducer = combineReducers({
auth: authReducer,
people: peopleReducer
settings: peopleReducer
});
export default rootReducer;

View File

@ -78,7 +78,6 @@ export function changePassword(userId, password, key) {
}
export function changeEmail(userId, email, key) {
const data = { email };
return request({
@ -89,7 +88,6 @@ export function changeEmail(userId, email, key) {
});
}
export function updateActivationStatus(activationStatus, userId, key) {
return request({
method: "put",
url: `/people/activationstatus/${activationStatus}.json`,
@ -154,6 +152,54 @@ export function getPortalTimezones() {
export function getUserList() {
return request({
method: "get",
url: "/people/filter?isAdministrator=true"
url: `/people`
});
}
export function getProductAdminsList(productId) {
return request({
method: "get",
url: `/settings/security/administrator/${productId}`
});
}
export function changeProductAdmin(userId, productId, administrator) {
return request({
method: "put",
url: "/settings/security/administrator",
data: {
productId,
userId,
administrator
}
});
}
export function getUserById(userId) {
return request({
method: "get",
url: `/people/${userId}`,
});
}
export function getGreetingSettings() {
return request({
method: "get",
url: `/settings/greetingsettings.json`,
});
}
export function setGreetingSettings(title) {
return request({
method: "post",
url: `/settings/greetingsettings.json`,
data: { title }
});
}
export function restoreGreetingSettings() {
return request({
method: "post",
url: `/settings/greetingsettings/restore.json`
});
}

View File

@ -0,0 +1,88 @@
import * as api from "../services/api";
export const SET_USERS = "SET_USERS";
export const SET_ADMINS = "SET_ADMINS";
export const SET_OWNER = "SET_OWNER";
export const SET_GREETING_SETTINGS = "SET_GREETING_SETTINGS";
export function setUsers(users) {
return {
type: SET_USERS,
users
};
}
export function setAdmins(admins) {
return {
type: SET_ADMINS,
admins
};
}
export function setOwner(owner) {
return {
type: SET_OWNER,
owner
};
}
export function setGreetingSettings(title) {
return {
type: SET_GREETING_SETTINGS,
title
};
}
export function getListUsers() {
return dispatch => {
return api.getUserList().then(users => dispatch(setUsers(users)));
};
}
export function getListAdmins(productId) {
return dispatch => {
return api
.getProductAdminsList(productId)
.then(admins => dispatch(setAdmins(admins)));
};
}
export function changeAdmins(userId, productId, isAdmin) {
return dispatch => {
return api.changeProductAdmin(userId, productId, isAdmin).then(() => {
dispatch(getListUsers());
dispatch(getListAdmins(productId));
});
};
}
export function getUserById(userId) {
return dispatch => {
return api.getUserById(userId).then(owner => dispatch(setOwner(owner)));
};
}
export function getGreetingTitle() {
return dispatch => {
return api.getGreetingSettings()
.then(greetingTitle => dispatch(setGreetingSettings(greetingTitle)));
};
}
export function setGreetingTitle(greetingTitle) {
return dispatch => {
return api.setGreetingSettings(greetingTitle)
.then((res) => {
dispatch(setGreetingSettings(greetingTitle))
});
};
}
export function restoreGreetingTitle() {
return dispatch => {
return api.restoreGreetingSettings()
.then((res) => {
dispatch(setGreetingSettings(res.companyName))
});
};
}

View File

@ -0,0 +1,33 @@
import { SET_USERS, SET_ADMINS, SET_OWNER, SET_GREETING_SETTINGS } from "./actions";
const initialState = {
users: [],
admins: [],
owner: {},
greetingSettings: ''
};
const peopleReducer = (state = initialState, action) => {
switch (action.type) {
case SET_USERS:
return Object.assign({}, state, {
users: action.users
});
case SET_ADMINS:
return Object.assign({}, state, {
admins: action.admins
});
case SET_OWNER:
return Object.assign({}, state, {
owner: action.owner
});
case SET_GREETING_SETTINGS:
return Object.assign({}, state, {
greetingSettings: action.title
});
default:
return state;
}
};
export default peopleReducer;

View File

@ -0,0 +1,31 @@
import { filter } from "lodash";
export function getUsers(users, ownerId) {
return filter(users, function(f) {
return f.id !== ownerId;
});
}
export function getAdmins(users) {
const newArray = [];
users.map(user => {
if (user.listAdminModules !== undefined) {
if (!user.listAdminModules.includes("people")) {
newArray.push(user);
}
} else {
newArray.push(user);
}
});
return newArray.filter(user => !user.isVisitor);
}
export function getSelectorOptions(users) {
return users.map(user => {
return {
key: user.id,
label: user.displayName,
selected: false
};
});
}

View File

@ -1802,10 +1802,12 @@ asap@~2.0.6:
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
"asc-web-components@file:../../packages/asc-web-components":
version "1.0.147"
version "1.0.152"
dependencies:
email-addresses "^3.0.3"
moment "^2.24.0"
prop-types "^15.7.2"
punycode "^2.1.1"
rc-tree "^2.1.2"
react-autosize-textarea "^7.0.0"
react-avatar-editor "^11.0.7"

View File

@ -26,7 +26,7 @@ module.exports = ({ config }) => {
// add story source
{
test: /\.stories\.js$/,
loaders: [require.resolve('@storybook/addon-storysource/loader')],
loaders: [require.resolve('@storybook/source-loader')],
enforce: 'pre',
},
// Process JS with Babel.

View File

@ -1,6 +1,6 @@
{
"name": "asc-web-components",
"version": "1.0.147",
"version": "1.0.156",
"description": "Ascensio System SIA component library",
"license": "AGPL-3.0",
"main": "dist/asc-web-components.js",
@ -46,16 +46,16 @@
"@babel/preset-react": "^7.0.0",
"@emotion/babel-preset-css-prop": "^10.0.17",
"@storybook/addon-a11y": "^5.1.11",
"@storybook/addon-actions": "^5.1.11",
"@storybook/addon-actions": "^5.2.5",
"@storybook/addon-console": "^1.2.1",
"@storybook/addon-knobs": "^5.1.11",
"@storybook/addon-links": "^5.1.11",
"@storybook/addon-options": "^5.1.11",
"@storybook/addon-storysource": "^5.1.11",
"@storybook/addon-viewport": "^5.1.11",
"@storybook/addons": "^5.1.11",
"@storybook/react": "^5.1.11",
"@svgr/rollup": "^4.3.2",
"@storybook/addon-knobs": "^5.2.5",
"@storybook/addon-links": "^5.2.5",
"@storybook/addon-options": "^5.2.5",
"@storybook/addon-storysource": "^5.2.5",
"@storybook/addon-viewport": "^5.2.5",
"@storybook/addons": "^5.2.5",
"@storybook/react": "^5.2.5",
"@svgr/rollup": "^4.3.3",
"@svgr/webpack": "^4.3.2",
"@testing-library/react": "^8.0.8",
"@types/jest": "^24.0.17",

View File

@ -49,8 +49,7 @@ const getButtons = (primary) => {
};
storiesOf('Components|Buttons', module)
// To set a default viewport for all the stories for this component
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => (
<Section>

View File

@ -1,16 +1,151 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import ContextMenuButton from '.';
const baseData = () => [
{
key: 'key',
label: 'label',
onClick: () => jest.fn()
}
];
const baseProps = {
title: 'Actions',
iconName: 'VerticalDotsIcon',
size: 16,
color: '#A3A9AE',
getData: baseData,
isDisabled: false
}
describe('<ContextMenuButton />', () => {
it('renders without error', () => {
const wrapper = mount(
<ContextMenuButton
title="Actions"
getData={() => [{key: 'key', label: 'label', onClick: () => alert('label')}]}
/>
);
const wrapper = mount(<ContextMenuButton {...baseProps} />);
expect(wrapper).toExist();
});
it('render with full custom props', () => {
const wrapper = mount(
<ContextMenuButton
color='red'
hoverColor='red'
clickColor='red'
size={20}
iconName='CatalogFolderIcon'
iconHoverName='CatalogFolderIcon'
iconClickName='CatalogFolderIcon'
isFill={true}
isDisabled={true}
onClick={() => jest.fn()}
onMouseEnter={() => jest.fn()}
onMouseLeave={() => jest.fn()}
onMouseOver={() => jest.fn()}
onMouseOut={() => jest.fn()}
getData={() => [
{
key: 'key',
icon: 'CatalogFolderIcon',
onClick: () => jest.fn()
},
{
label: 'CatalogFolderIcon',
onClick: () => jest.fn()
},
{}
]}
directionX='right'
opened={true}
/>);
expect(wrapper).toExist();
});
it('disabled', () => {
const wrapper = mount(<ContextMenuButton {...baseProps} isDisabled={true} />);
expect(wrapper.prop('isDisabled')).toEqual(true);
});
it('not re-render', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate(wrapper.props, wrapper.state);
expect(shouldUpdate).toBe(false);
});
it('re-render', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} />).instance();
const shouldUpdate = wrapper.shouldComponentUpdate({ opened: true }, wrapper.state);
expect(shouldUpdate).toBe(true);
});
it('causes function onDropDownItemClick()', () => {
const onClick = jest.fn();
const wrapper = shallow(<ContextMenuButton {...baseProps} opened={true} onClick={onClick} />);
const instance = wrapper.instance();
instance.onDropDownItemClick({
key: 'key',
label: 'label',
onClick: onClick
});
expect(wrapper.state('isOpen')).toBe(false);
expect(onClick).toHaveBeenCalled();
});
it('causes function onIconButtonClick()', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} isDisabled={false} opened={true} />);
const instance = wrapper.instance();
instance.onIconButtonClick();
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function onIconButtonClick() with isDisabled prop', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} isDisabled={true} opened={true} />);
const instance = wrapper.instance();
instance.onIconButtonClick();
expect(wrapper.state('isOpen')).toBe(true);
});
it('componentDidUpdate() state lifecycle test', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} />);
const instance = wrapper.instance();
wrapper.setState({ isOpen: false });
instance.componentDidUpdate(wrapper.props(), wrapper.state());
expect(wrapper.state()).toBe(wrapper.state());
});
it('componentDidUpdate() props lifecycle test', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} />);
const instance = wrapper.instance();
instance.componentDidUpdate({ opened: true }, wrapper.state());
expect(wrapper.props()).toBe(wrapper.props());
});
it('componentWillUnmount() props lifecycle test', () => {
const wrapper = shallow(<ContextMenuButton {...baseProps} />);
const instance = wrapper.instance();
instance.componentWillUnmount();
expect(wrapper).toExist(false);
});
});

View File

@ -6,7 +6,7 @@ import DropDown from '../drop-down'
import IconButton from '../icon-button'
import { handleAnyClick } from '../../utils/event';
const StyledOuther = styled.div`
const StyledOuter = styled.div`
display: inline-block;
position: relative;
cursor: pointer;
@ -29,7 +29,7 @@ class ContextMenuButton extends React.Component {
this.onIconButtonClick = this.onIconButtonClick.bind(this);
this.onDropDownItemClick = this.onDropDownItemClick.bind(this);
if(props.opened)
if (props.opened)
handleAnyClick(true, this.handleClick);
}
@ -42,23 +42,21 @@ class ContextMenuButton extends React.Component {
}
componentDidUpdate(prevProps, prevState) {
// Store prevId in state so we can compare when props change.
// Clear out previously-loaded data (so we don't render stale stuff).
if (this.props.opened !== prevProps.opened) {
this.toggle(this.props.opened);
}
if(this.state.isOpen !== prevState.isOpen) {
if (this.state.isOpen !== prevState.isOpen) {
handleAnyClick(this.state.isOpen, this.handleClick);
}
}
onIconButtonClick = () => {
if(!this.props.isDisabled) {
this.setState({
data: this.props.getData(),
isOpen: !this.state.isOpen
});
if (!this.props.isDisabled) {
this.setState({
data: this.props.getData(),
isOpen: !this.state.isOpen
});
}
else {
this.stopAction
@ -79,35 +77,51 @@ class ContextMenuButton extends React.Component {
render() {
//console.log("ContextMenuButton render");
const {
color,
hoverColor,
clickColor,
size,
iconName,
iconHoverName,
iconClickName,
isDisabled,
onMouseEnter,
onMouseLeave,
onMouseOver,
onMouseOut,
directionX
} = this.props;
const { isOpen } = this.state;
return (
<StyledOuther ref={this.ref}>
<StyledOuter ref={this.ref}>
<IconButton
color={this.props.color}
hoverColor={this.props.hoverColor}
clickColor={this.props.clickColor}
size={this.props.size}
iconName={this.props.iconName}
iconHoverName={this.props.iconHoverName}
iconClickName={this.props.iconClickName}
color={color}
hoverColor={hoverColor}
clickColor={clickColor}
size={size}
iconName={iconName}
iconHoverName={iconHoverName}
iconClickName={iconClickName}
isFill={false}
isDisabled={this.props.isDisabled}
isDisabled={isDisabled}
onClick={this.onIconButtonClick}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
onMouseOver={this.props.onMouseOver}
onMouseOut={this.props.onMouseOut}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseOver={onMouseOver}
onMouseOut={onMouseOut}
/>
<DropDown directionX={this.props.directionX || 'left'} isOpen={this.state.isOpen}>
<DropDown directionX={directionX} isOpen={isOpen}>
{
this.state.data.map(item =>
<DropDownItem
{...item}
onClick={this.onDropDownItemClick.bind(this, item)}
this.state.data.map((item, index) =>
(item.label || item.icon) && <DropDownItem {...item} key={item.key || index} onClick={this.onDropDownItemClick.bind(this, item)}
/>
)
}
</DropDown>
</StyledOuther>
</StyledOuter>
);
}
}
@ -120,7 +134,20 @@ ContextMenuButton.propTypes = {
iconName: PropTypes.string,
size: PropTypes.number,
color: PropTypes.string,
isDisabled: PropTypes.bool
isDisabled: PropTypes.bool,
hoverColor: PropTypes.string,
clickColor: PropTypes.string,
iconHoverName: PropTypes.string,
iconClickName: PropTypes.string,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
onMouseOver: PropTypes.func,
onMouseOut: PropTypes.func,
directionX: PropTypes.string
};
ContextMenuButton.defaultProps = {
@ -129,7 +156,8 @@ ContextMenuButton.defaultProps = {
title: '',
iconName: 'VerticalDotsIcon',
size: 16,
isDisabled: false
isDisabled: false,
directionX: 'left'
};
export default ContextMenuButton

View File

@ -1,18 +1,76 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import DropDownItem from '.';
const baseProps = {
isSeparator: false,
isHeader: false,
tabIndex: -1,
label: 'test',
disabled: false,
icon: 'NavLogoIcon',
noHover: false,
onClick: jest.fn()
}
describe('<DropDownItem />', () => {
it('renders without error', () => {
const wrapper = mount(
<DropDownItem
isSeparator={false}
isHeader={false}
label='Button 1'
icon='NavLogoIcon'
onClick={() => console.log('Button 1 clicked')} />
<DropDownItem {...baseProps} />
);
expect(wrapper).toExist();
});
it('check disabled props', () => {
const wrapper = mount(
<DropDownItem {...baseProps} disabled={true} />
);
expect(wrapper.prop('disabled')).toEqual(true);
});
it('check isSeparator props', () => {
const wrapper = mount(
<DropDownItem {...baseProps} isSeparator={true} />
);
expect(wrapper.prop('isSeparator')).toEqual(true);
});
it('check isHeader props', () => {
const wrapper = mount(
<DropDownItem {...baseProps} isHeader={true} />
);
expect(wrapper.prop('isHeader')).toEqual(true);
});
it('check noHover props', () => {
const wrapper = mount(
<DropDownItem {...baseProps} noHover={true} />
);
expect(wrapper.prop('noHover')).toEqual(true);
});
it('causes function onClick()', () => {
const onClick = jest.fn();
const wrapper = shallow(<DropDownItem id='test' {...baseProps} onClick={onClick} />);
wrapper.find("#test").simulate("click")
expect(onClick).toHaveBeenCalled();
});
it('render without child', () => {
const wrapper = shallow(
<DropDownItem >
test
</DropDownItem>
);
expect(wrapper.props.children).toEqual(undefined);
});
});

View File

@ -35,12 +35,24 @@ describe('<DropDown />', () => {
expect(wrapper.prop('directionX')).toEqual('right');
});
it('directionX right manualX', () => {
const wrapper = mount(<DropDown {...baseProps} directionX='right' manualX='100px' />);
expect(wrapper.prop('directionX')).toEqual('right');
});
it('directionY top', () => {
const wrapper = mount(<DropDown {...baseProps} directionY='top' />);
expect(wrapper.prop('directionY')).toEqual('top');
});
it('directionY top manualY', () => {
const wrapper = mount(<DropDown {...baseProps} directionY='top' manualY='100%' />);
expect(wrapper.prop('directionY')).toEqual('top');
});
it('withArrow', () => {
const wrapper = mount(<DropDown {...baseProps} withArrow />);
@ -74,15 +86,37 @@ describe('<DropDown />', () => {
expect(wrapper.children()).toHaveLength(1);
});
/*
it('with maxHeight and children', () => {
const wrapper = mount((
<DropDown {...baseProps} maxHeight={200}>
<div>1</div>
</DropDown>
));
expect(wrapper.children()).toHaveLength(1);
it('with maxHeight and children', () => {
const child = (<div>1</div>);
const wrapper = shallow((
<DropDown
maxHeight={0}>
{child}
</DropDown>
)).instance();
expect(wrapper.props.children).toEqual(child);
});
*/
it('componentDidUpdate() state lifecycle test', () => {
const wrapper = shallow(<DropDown {...baseProps} />);
const instance = wrapper.instance();
wrapper.setState({ isOpen: true });
instance.componentDidUpdate(wrapper.props(), wrapper.state());
expect(wrapper.state()).toBe(wrapper.state());
});
it('componentDidUpdate() props lifecycle test', () => {
const wrapper = shallow(<DropDown {...baseProps} />);
const instance = wrapper.instance();
instance.componentDidUpdate({ opened: true }, wrapper.state());
expect(wrapper.props()).toBe(wrapper.props());
});
});

View File

@ -94,7 +94,9 @@ class DropDown extends React.PureComponent {
render() {
const {maxHeight, withArrow, directionX, children} = this.props;
const dropDownMaxHeightProp = maxHeight ? { height: maxHeight + 'px' } : {};
const fullHeight = children && children.length * 36;
const calculatedHeight = ((fullHeight > 0) && (fullHeight < maxHeight)) ? fullHeight : maxHeight;
const dropDownMaxHeightProp = maxHeight ? { height: calculatedHeight + 'px' } : {};
//console.log("DropDown render");
return (
<StyledDropdown
@ -105,7 +107,7 @@ class DropDown extends React.PureComponent {
{withArrow && <Arrow directionX={directionX} />}
{maxHeight
? <FixedSizeList
height={maxHeight}
height={calculatedHeight}
width={this.state.width}
itemSize={36}
itemCount={children.length}

View File

@ -77,6 +77,7 @@ class EmailInput extends React.Component {
onChangeAction = (e) => {
this.props.onChange && this.props.onChange(e);
this.props.customValidateFunc ? this.props.customValidateFunc(e) : this.checkEmail(e.target.value);
}
@ -96,7 +97,7 @@ class EmailInput extends React.Component {
isValidEmail={isValid || isValidEmail}
value={inputValue}
onChange={this.onChangeAction}
type='email'
type='text'
onValidateInput={onValidateInput}
{...rest}
/>

View File

@ -84,8 +84,6 @@ const LoginForm = props => {
};
storiesOf('EXAMPLES|Forms', module)
// To set a default viewport for all the stories for this component
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.add('login', () => {
const onSubmit = (e, credentials) => {
console.log("onSubmit", e, credentials);

View File

@ -82,8 +82,6 @@ function getRandomInt(min, max) {
storiesOf("EXAMPLES|AdvancedSelector", module)
.addDecorator(withKnobs)
// To set a default viewport for all the stories for this component
.addParameters({ viewport: { defaultViewport: "responsive" } })
.add("people group selector", () => {
return (
<Section>

View File

@ -1,15 +1,165 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import GroupButton from '.';
describe('<GroupButton />', () => {
it('renders without error', () => {
const wrapper = mount(
<GroupButton label='Group button' disabled={false} isDropdown={false} opened={false} ></GroupButton>
);
const baseProps = {
label: 'test',
disabled: false,
opened: false,
isDropdown: false,
}
describe('<GroupButton />', () => {
it('renders without error', () => {
const wrapper = mount(<GroupButton {...baseProps} />);
expect(wrapper).toExist();
});
it('renders with child', () => {
const wrapper = mount(
<GroupButton {...baseProps} isDropdown={true} >
<div key='demo' label='demo'>1</div>
</GroupButton>);
expect(wrapper).toExist();
});
it('applies disabled prop', () => {
const wrapper = mount(<GroupButton {...baseProps} disabled={true} />);
expect(wrapper.prop('disabled')).toEqual(true);
});
it('applies opened prop', () => {
const wrapper = mount(<GroupButton {...baseProps} opened={true} />);
expect(wrapper.prop('opened')).toEqual(true);
expect(wrapper.state('isOpen')).toEqual(true);
});
it('applies isDropdown prop', () => {
const wrapper = mount(<GroupButton {...baseProps} isDropdown={true} />);
expect(wrapper.prop('isDropdown')).toEqual(true);
});
it('applies activated prop', () => {
const wrapper = mount(<GroupButton {...baseProps} activated={true} />);
expect(wrapper.prop('activated')).toEqual(true);
});
it('applies hovered prop', () => {
const wrapper = mount(<GroupButton {...baseProps} hovered={true} />);
expect(wrapper.prop('hovered')).toEqual(true);
});
it('applies isSeparator prop', () => {
const wrapper = mount(<GroupButton {...baseProps} isSeparator={true} />);
expect(wrapper.prop('isSeparator')).toEqual(true);
});
it('applies isSelect prop', () => {
const wrapper = mount(<GroupButton {...baseProps} isSelect={true} />);
expect(wrapper.prop('isSelect')).toEqual(true);
});
it('applies checked prop', () => {
const wrapper = mount(<GroupButton {...baseProps} checked={true} />);
expect(wrapper.prop('checked')).toEqual(true);
});
it('applies isIndeterminate prop', () => {
const wrapper = mount(<GroupButton {...baseProps} isIndeterminate={true} />);
expect(wrapper.prop('isIndeterminate')).toEqual(true);
});
it('applies dropDownMaxHeight prop', () => {
const wrapper = mount(<GroupButton {...baseProps} dropDownMaxHeight={100} />);
expect(wrapper.prop('dropDownMaxHeight')).toEqual(100);
});
it('componentDidUpdate() state lifecycle test', () => {
const wrapper = shallow(<GroupButton {...baseProps} />);
const instance = wrapper.instance();
wrapper.setState({ isOpen: true });
instance.componentDidUpdate(wrapper.props(), wrapper.state());
expect(wrapper.state()).toBe(wrapper.state());
});
it('componentDidUpdate() props lifecycle test', () => {
const wrapper = shallow(<GroupButton {...baseProps} />);
const instance = wrapper.instance();
instance.componentDidUpdate({ opened: true }, wrapper.state());
expect(wrapper.props()).toBe(wrapper.props());
});
it('componentWillUnmount() props lifecycle test', () => {
const wrapper = shallow(<GroupButton {...baseProps} />);
const instance = wrapper.instance();
instance.componentWillUnmount();
expect(wrapper).toExist(false);
});
it('causes function dropDownItemClick()', () => {
const onClick = jest.fn();
const onSelect = jest.fn()
const child = (<div onClick={onClick}>1</div>);
const wrapper = shallow(
<GroupButton {...baseProps} opened={true} onSelect={onSelect} >
{child}
</GroupButton>);
const instance = wrapper.instance();
instance.dropDownItemClick(child);
expect(wrapper.state('isOpen')).toBe(false);
expect(onClick).toBeCalled();
});
it('causes function dropDownToggleClick()', () => {
const wrapper = shallow(
<GroupButton {...baseProps} opened={true} />);
const instance = wrapper.instance();
instance.dropDownToggleClick();
expect(wrapper.state('isOpen')).toBe(false);
});
it('causes function checkboxChange()', () => {
const wrapper = shallow(
<GroupButton {...baseProps} selected='demo' />);
const instance = wrapper.instance();
instance.checkboxChange();
expect(wrapper.state('selected')).toBe('demo');
});
it('causes function handleClick()', () => {
const wrapper = mount(<GroupButton {...baseProps} />);
const instance = wrapper.instance();
instance.handleClick(new Event('click'));
expect(wrapper.state('isOpen')).toBe(false);
});
expect(wrapper).toExist();
});
});

View File

@ -1,40 +1,145 @@
import React from 'react';
import { mount } from 'enzyme';
import { mount, shallow } from 'enzyme';
import GroupButtonsMenu from '.';
import DropDownItem from '../drop-down-item';
const defaultMenuItems = [
{
label: 'Select',
isDropdown: true,
isSeparator: true,
isSelect: true,
fontWeight: 'bold',
children: [
<DropDownItem key='aaa' label='aaa' />,
<DropDownItem key='bbb' label='bbb' />,
<DropDownItem key='ccc' label='ccc' />,
],
onSelect: () => { }
},
{
label: 'Menu item 1',
disabled: false,
onClick: () => { }
},
{
label: 'Menu item 2',
disabled: true,
onClick: () => { }
}
];
const baseProps = {
checked: false,
menuItems: defaultMenuItems,
visible: true,
moreLabel: 'More',
closeTitle: 'Close'
}
describe('<GroupButtonsMenu />', () => {
it('renders without error', () => {
const menuItems = [
{
label: 'Select',
isDropdown: true,
isSeparator: true,
isSelect: true,
fontWeight: 'bold',
children: [
<DropDownItem key='aaa' label='aaa' />,
<DropDownItem key='bbb' label='bbb' />,
<DropDownItem key='ccc' label='ccc' />,
],
onSelect: () => {}
},
{
label: 'Menu item 1',
disabled: false,
onClick: () => {}
},
{
label: 'Menu item 2',
disabled: true,
onClick: () => {}
}
];
it('renders without error', () => {
const wrapper = mount(<GroupButtonsMenu {...baseProps} />);
const wrapper = mount(
<GroupButtonsMenu checked={false} menuItems={menuItems} visible={true} moreLabel='More' closeTitle='Close' />
);
expect(wrapper).toExist();
});
expect(wrapper).toExist();
it('applies checked prop', () => {
const wrapper = mount(<GroupButtonsMenu {...baseProps} checked={true} />);
expect(wrapper.prop('checked')).toEqual(true);
});
it('applies visible prop', () => {
const wrapper = mount(<GroupButtonsMenu {...baseProps} visible={true} />);
expect(wrapper.prop('visible')).toEqual(true);
});
it('causes function closeMenu()', () => {
const onClose = jest.fn();
const wrapper = mount(<GroupButtonsMenu {...baseProps} onClose={onClose} />);
const instance = wrapper.instance();
instance.closeMenu(new Event('click'));
expect(wrapper.state('visible')).toBe(false);
expect(onClose).toBeCalled();
});
it('causes function groupButtonClick()', () => {
const onClick = jest.fn();
const item = {
label: 'Menu item 1',
disabled: false,
onClick: onClick
};
const wrapper = mount(<GroupButtonsMenu {...baseProps} />);
const instance = wrapper.instance();
instance.groupButtonClick(item);
expect(wrapper.state('visible')).toBe(false);
expect(onClick).toBeCalled();
});
it('causes function countMenuItems()', () => {
const wrapper = mount(<GroupButtonsMenu {...baseProps} />);
const instance = wrapper.instance();
instance.countMenuItems([], 2, 1);
instance.countMenuItems([1, 2], 200, 2);
instance.countMenuItems([1,2,3,4], 1, 2);
expect(wrapper.state('visible')).toBe(true);
});
it('causes function groupButtonClick() if disabled', () => {
const onClick = jest.fn();
const item = {
label: 'Menu item 1',
disabled: true,
onClick: onClick
};
const wrapper = mount(<GroupButtonsMenu {...baseProps} />);
const instance = wrapper.instance();
instance.groupButtonClick(item);
expect(wrapper.state('visible')).toBe(true);
expect(onClick).toBeCalledTimes(0);
});
it('componentDidUpdate() props lifecycle test', () => {
const wrapper = shallow(<GroupButtonsMenu {...baseProps} visible={false} />);
const instance = wrapper.instance();
instance.componentDidUpdate({ visible: true, menuItems: defaultMenuItems }, wrapper.state());
expect(wrapper.props()).toBe(wrapper.props());
});
it('componentWillUnmount() props lifecycle test', () => {
const wrapper = shallow(<GroupButtonsMenu {...baseProps} />);
const instance = wrapper.instance();
instance.componentWillUnmount();
expect(wrapper).toExist(false);
});
it('filled state moreItems', () => {
const wrapper = shallow(<GroupButtonsMenu {...baseProps} />);
wrapper.setState({
moreItems: [{
label: 'Menu item 1',
disabled: false,
onClick: jest.fn()
}]
});
expect(wrapper).toExist(false);
});
});

View File

@ -60,9 +60,9 @@ class GroupButtonsMenu extends React.PureComponent {
this.state = {
priorityItems: props.menuItems,
moreItems: [],
visible: true
visible: props.visible
}
this.throttledResize = throttle(this.updateMenu, 300);
}
@ -78,30 +78,38 @@ class GroupButtonsMenu extends React.PureComponent {
};
componentDidMount() {
const groupMenuItems = document.getElementById("groupMenu")
? document.getElementById("groupMenu").children
: [];
const groupMenuElement = document.getElementById("groupMenu");
const groupMenuItems = groupMenuElement ? groupMenuElement.children : [0];
const groupMenuItemsArray = [...groupMenuItems];
this.widthsArray = groupMenuItemsArray.map(item => item.getBoundingClientRect().width);
this.widthsArray = groupMenuItemsArray.map(item => item.offsetWidth);
window.addEventListener('resize', this.throttledResize);
this.updateMenu();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps, prevState) {
if (this.props.visible !== prevProps.visible) {
this.setState({ visible: this.props.visible });
}
if(!isArrayEqual(this.props.menuItems, prevProps.menuItems)){
if (!isArrayEqual(this.props.menuItems, prevProps.menuItems)) {
this.setState({ priorityItems: this.props.menuItems, });
}
if (this.state.priorityItems.length !== prevState.priorityItems.length || this.state.moreItems.length !== prevState.moreItems.length) {
this.updateMenu();
}
}
countMenuItems = (array, outerWidth, initialWidth) => {
let total = (initialWidth + 80);
for (let i = 0, len = array.length; i < len; i++) {
countMenuItems = (array, outerWidth, moreWidth) => {
const itemsArray = array || []
const moreButton = moreWidth || 0;
let total = (moreButton + 80);
for (let i = 0, len = itemsArray.length; i < len; i++) {
if (total + array[i] > outerWidth) {
return i < 1 ? 1 : i;
} else {
@ -114,12 +122,12 @@ class GroupButtonsMenu extends React.PureComponent {
const moreMenuElement = document.getElementById("moreMenu");
const groupMenuOuterElement = document.getElementById("groupMenuOuter");
const moreMenuWidth = moreMenuElement ? moreMenuElement.getBoundingClientRect().width : 0;
const groupMenuOuterWidth = groupMenuOuterElement ? groupMenuOuterElement.getBoundingClientRect().width : 0;
const moreMenuWidth = moreMenuElement && moreMenuElement.getBoundingClientRect().width;
const groupMenuOuterWidth = groupMenuOuterElement && groupMenuOuterElement.getBoundingClientRect().width;
const visibleItemsCount = this.countMenuItems(this.widthsArray, groupMenuOuterWidth, moreMenuWidth);
const navItemsCopy = this.props.menuItems;
const priorityItems = navItemsCopy.slice(0, visibleItemsCount);
const moreItems = priorityItems.length !== navItemsCopy.length ? navItemsCopy.slice(visibleItemsCount, navItemsCopy.length) : [];
@ -137,12 +145,12 @@ class GroupButtonsMenu extends React.PureComponent {
//console.log("GroupButtonsMenu render");
const { selected, moreLabel, closeTitle } = this.props;
const { priorityItems, moreItems, visible } = this.state;
return (
<StyledGroupButtonsMenu id="groupMenuOuter" visible={visible} >
<GroupMenuWrapper id="groupMenu">
{priorityItems.map((item, i) =>
<GroupButton
<GroupButton
key={`navItem-${i}`}
label={item.label}
isDropdown={item.isDropdown}
@ -160,8 +168,8 @@ class GroupButtonsMenu extends React.PureComponent {
)}
</GroupMenuWrapper>
{moreItems.length > 0 &&
<GroupButton
id="moreMenu"
<GroupButton
id="moreMenu"
isDropdown={true}
label={moreLabel}
>
@ -197,7 +205,7 @@ GroupButtonsMenu.propTypes = {
GroupButtonsMenu.defaultProps = {
checked: false,
selected: 'Select',
visible: PropTypes.bool,
visible: true,
moreLabel: 'More',
closeTitle: 'Close'
}

View File

@ -25,7 +25,6 @@ const data = [
const dropdownType = ['alwaysDashed', 'appearDashedAfterHover'];
storiesOf('Components|LinkWithDropdown', module)
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => (
<>

View File

@ -17,7 +17,6 @@ const headerStyle = {
storiesOf('Components|Link', module)
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => (
<>

View File

@ -89,7 +89,8 @@ Link.propTypes = {
target: PropTypes.oneOf(["_blank", "_self", "_parent", "_top"]),
title: PropTypes.string,
type: PropTypes.oneOf(["action", "page"]),
children: PropTypes.string
children: PropTypes.string,
className: PropTypes.string
};
Link.defaultProps = {
@ -103,6 +104,7 @@ Link.defaultProps = {
rel: "noopener noreferrer",
tabIndex: -1,
type: "page",
className: "",
};
export default Link;

View File

@ -7,8 +7,6 @@ import ModuleTile from '../module-tile';
const rowStyle = { marginTop: 8 };
storiesOf('Components|ModuleTile', module)
// To set a default viewport for all the stories for this component
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false }})
.add('all', () => (
<Container>

View File

@ -10,8 +10,6 @@ import Readme from './README.md';
const rowStyle = { marginTop: 8 };
storiesOf('Components|ModuleTile', module)
// To set a default viewport for all the stories for this component
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addDecorator(withKnobs)
.addDecorator(withReadme(Readme))
.add('base', () => (

View File

@ -46,9 +46,13 @@ const MainContainerWrapper = styled.div`
`;
const MainContainer = styled.div`
${truncateCss};
height: 20px;
margin-right: 8px;
@media ${tablet} {
${truncateCss};
}
`;
const MainIcons = styled.div`
@ -57,9 +61,12 @@ const MainIcons = styled.div`
`;
const SideContainerWrapper = styled.div`
${truncateCss};
${commonCss};
@media ${tablet} {
${truncateCss};
}
align-self: center;
width: ${props => props.containerWidth ? props.containerWidth : '100px'};
color: ${props => props.color && props.color};

View File

@ -4,6 +4,7 @@ import PropTypes from 'prop-types'
import Checkbox from '../checkbox'
import ContextMenuButton from '../context-menu-button'
import { tablet } from '../../utils/device';
const StyledRow = styled.div`
cursor: default;
@ -27,9 +28,11 @@ const StyledContent = styled.div`
min-width: 160px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media ${tablet} {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const StyledCheckbox = styled.div`

View File

@ -26,7 +26,6 @@ const StyledContainerInline = styled.div`
`;
storiesOf('Components|SelectedItem', module)
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => {

View File

@ -36,6 +36,7 @@ const SelectorAddButton = (props) => {
iconName='PlusIcon'
isFill={true}
isDisabled={isDisabled}
isClickable={!isDisabled}
/>
</StyledButton>
);

View File

@ -48,6 +48,5 @@ class TostWrapper extends React.Component {
}
storiesOf('Components|Toast', module)
.addParameters({ viewport: { defaultViewport: 'responsive' } })
.addParameters({ options: { showAddonPanel: false } })
.add('all', () => (<TostWrapper />));

View File

@ -59,4 +59,4 @@ export { default as DatePicker } from './components/date-picker'
export { default as PasswordInput } from './components/password-input'
export { default as Tooltip } from './components/tooltip'
export { default as HelpButton } from './components/help-button'
export { default as EmailInput } from './components/email-input'

View File

@ -1,7 +1,7 @@
import emailAddresses from "email-addresses";
import punycode from "punycode";
import { parseErrorTypes } from "./../constants";
import { EmailSettings } from './index';
import { EmailSettings } from './emailSettings';
const getParts = string => {
let mass = [];

File diff suppressed because it is too large Load Diff