This commit is contained in:
Nikita Gopienko 2019-09-12 11:50:57 +03:00
commit b9edf10894
24 changed files with 819 additions and 420 deletions

2
.gitignore vendored
View File

@ -3,7 +3,6 @@
*.suo
*.user
.vs/
.vscode/
*-lock.json
**/node_modules/
**/storybook-static/
@ -12,3 +11,4 @@
*.log
/packages/asc-web-components
/products/ASC.People/Data/
Data/

View File

@ -0,0 +1,54 @@
import React from "react";
import styled from "styled-components";
import isEqual from "lodash/isEqual";
import { ComboBox, TextInput } from "asc-web-components";
const Container = styled.div`
display: flex;
margin: 0 0 16px 0;
`;
class ContactField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
render() {
console.log("ContactField render");
const {
isDisabled,
comboBoxName,
comboBoxOptions,
comboBoxSelectedOption,
comboBoxOnChange,
inputName,
inputValue,
inputOnChange
} = this.props;
return (
<Container>
<ComboBox
name={comboBoxName}
options={comboBoxOptions}
onSelect={comboBoxOnChange}
selectedOption={comboBoxSelectedOption}
isDisabled={isDisabled}
scaled={false}
className="field-select"
/>
<TextInput
name={inputName}
value={inputValue}
isDisabled={isDisabled}
onChange={inputOnChange}
/>
</Container>
);
}
}
export default ContactField;

View File

@ -1,6 +1,8 @@
import React from 'react'
import styled from 'styled-components';
import { ComboBox, TextInput } from 'asc-web-components'
import React from "react";
import styled from "styled-components";
import isEqual from "lodash/isEqual";
import ContactField from "./ContactField";
import { ComboBox } from "asc-web-components";
const Container = styled.div`
width: 100%;
@ -12,82 +14,96 @@ const Container = styled.div`
}
`;
const Item = styled.div`
display: flex;
margin: 0 0 16px 0;
`;
const getOptions = (patterns, keyPrefix) => {
return patterns.map((item, index) => {
return {
key: keyPrefix + index,
label: item.type, //from resource
icon: item.icon,
value: item.type
};
});
}
return patterns.map((item, index) => {
return {
key: keyPrefix + index,
label: item.type, //from resource
icon: item.icon,
value: item.type
};
});
};
const renderItems = (contacts, pattern, onTypeChange, onTextChange, isDisabled) => {
const items = contacts.map((contact, index) => {
const renderItems = (
contacts,
pattern,
onTypeChange,
onTextChange,
isDisabled
) => {
const items = contacts.map((contact, index) => {
const prefix = contact.id + "_";
const itemOptions = getOptions(pattern, prefix);
const itemSelectedOption = itemOptions.filter(
option => option.value === contact.type
)[0];
const prefix = contact.id + "_";
return (
<ContactField
key={prefix + "item_" + index}
isDisabled={isDisabled}
comboBoxName={prefix + "type"}
comboBoxOptions={itemOptions}
comboBoxSelectedOption={itemSelectedOption}
comboBoxOnChange={onTypeChange}
inputName={prefix + "value"}
inputValue={contact.value}
inputOnChange={onTextChange}
/>
);
});
const itemOptions = getOptions(pattern, prefix);
return items;
};
const itemSelectedOption = itemOptions.filter(option => option.value === contact.type)[0];
return (
<Item key={prefix + "item_" + index}>
<ComboBox
name={prefix + "type"}
options={itemOptions}
onSelect={onTypeChange}
selectedOption={itemSelectedOption}
isDisabled={isDisabled}
scaled={false}
className="field-select"
/>
<TextInput
name={prefix + "value"}
value={contact.value}
isDisabled={isDisabled}
onChange={onTextChange}
/>
</Item>
);
});
return items;
};
class ContactsField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
const ContactsField = React.memo((props) => {
render() {
console.log("ContactsField render");
const { pattern, contacts, addItemText, onItemAdd, onItemTypeChange, onItemTextChange, isDisabled } = props;
const {
pattern,
contacts,
addItemText,
onItemAdd,
onItemTypeChange,
onItemTextChange,
isDisabled
} = this.props;
const existItems = renderItems(contacts, pattern, onItemTypeChange, onItemTextChange, isDisabled);
const existItems = renderItems(
contacts,
pattern,
onItemTypeChange,
onItemTextChange,
isDisabled
);
const prefix = "null_";
const options = getOptions(pattern, prefix);
return (
<Container>
{existItems}
<ComboBox
options={options}
onSelect={onItemAdd}
selectedOption={{
key: prefix,
label: addItemText,
value: ""
}}
isDisabled={isDisabled}
scaled={false}
className="field-select"
/>
</Container>
<Container>
{existItems}
<ComboBox
options={options}
onSelect={onItemAdd}
selectedOption={{
key: prefix,
label: addItemText,
value: ""
}}
isDisabled={isDisabled}
scaled={false}
className="field-select"
/>
</Container>
);
});
}
}
export default ContactsField
export default ContactsField;

View File

@ -1,35 +1,44 @@
import React from 'react'
import { FieldContainer, DateInput } from 'asc-web-components'
import React from "react";
import isEqual from "lodash/isEqual";
import { FieldContainer, DatePicker } from "asc-web-components";
const DateField = React.memo((props) => {
const {
isRequired,
hasError,
labelText,
class DateField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
inputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputTabIndex
} = props;
render() {
console.log("DateField render");
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<DateInput
name={inputName}
selected={inputValue}
disabled={inputIsDisabled}
onChange={inputOnChange}
const {
isRequired,
hasError,
labelText,
inputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputTabIndex
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
tabIndex={inputTabIndex}
/>
</FieldContainer>
);
});
labelText={labelText}
>
<DatePicker
name={inputName}
selectedDate={inputValue}
disabled={inputIsDisabled}
onChange={inputOnChange}
hasError={hasError}
tabIndex={inputTabIndex}
/>
</FieldContainer>
);
}
}
export default DateField
export default DateField;

View File

@ -1,42 +1,57 @@
import React from 'react'
import { FieldContainer, SelectorAddButton, SelectedItem } from 'asc-web-components'
import React from "react";
import isEqual from "lodash/isEqual";
import {
FieldContainer,
SelectorAddButton,
SelectedItem
} from "asc-web-components";
const DepartmentField = React.memo((props) => {
const {
isRequired,
isDisabled,
hasError,
labelText,
addButtonTitle,
departments,
onAddDepartment,
onRemoveDepartment
} = props;
class DepartmentField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
className="departments-field"
>
<SelectorAddButton
isDisabled={isDisabled}
title={addButtonTitle}
onClick={onAddDepartment}
className="department-add-btn"
/>
{departments && departments.map((department) => (
<SelectedItem
key={`department_${department.id}`}
text={department.name}
onClose={() => { onRemoveDepartment(department.id) }}
isInline={true}
className="department-item"
render() {
console.log("DepartmentField render");
const {
isRequired,
isDisabled,
hasError,
labelText,
addButtonTitle,
departments,
onAddDepartment,
onRemoveDepartment
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
className="departments-field"
>
<SelectorAddButton
isDisabled={isDisabled}
title={addButtonTitle}
onClick={onAddDepartment}
className="department-add-btn"
/>
))}
</FieldContainer>
);
});
{departments.map(department => (
<SelectedItem
key={`department_${department.id}`}
text={department.name}
onClose={() => {
onRemoveDepartment(department.id);
}}
isInline={true}
className="department-item"
/>
))}
</FieldContainer>
);
}
}
export default DepartmentField
export default DepartmentField;

View File

@ -1,6 +1,6 @@
import React from 'react'
import styled from 'styled-components';
import { Text } from 'asc-web-components'
import React from "react";
import styled from "styled-components";
import { Text } from "asc-web-components";
const Container = styled.div`
margin: 0 0 40px 0;
@ -11,7 +11,7 @@ const Header = styled(Text.ContentHeader)`
line-height: unset;
`;
const InfoFieldContainer = React.memo((props) => {
const InfoFieldContainer = React.memo(props => {
const { headerText, children } = props;
return (
@ -22,4 +22,4 @@ const InfoFieldContainer = React.memo((props) => {
);
});
export default InfoFieldContainer
export default InfoFieldContainer;

View File

@ -1,66 +1,80 @@
import React from 'react'
import { FieldContainer, RadioButtonGroup, PasswordInput } from 'asc-web-components'
import React from "react";
import isEqual from "lodash/isEqual";
import {
FieldContainer,
RadioButtonGroup,
PasswordInput
} from "asc-web-components";
const PasswordField = React.memo((props) => {
const {
isRequired,
hasError,
labelText,
passwordSettings,
class PasswordField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
radioName,
radioValue,
radioOptions,
radioIsDisabled,
radioOnChange,
render() {
console.log("PasswordField render");
inputName,
emailInputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputTabIndex,
const {
isRequired,
hasError,
labelText,
passwordSettings,
copyLinkText,
} = props;
radioName,
radioValue,
radioOptions,
radioIsDisabled,
radioOnChange,
const tooltipPasswordLength = 'from ' + passwordSettings.minLength + ' to 30 characters';
inputName,
emailInputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputTabIndex,
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<RadioButtonGroup
name={radioName}
selected={radioValue}
options={radioOptions}
isDisabled={radioIsDisabled}
onClick={radioOnChange}
className="radio-group"
/>
<PasswordInput
inputName={inputName}
emailInputName={emailInputName}
inputValue={inputValue}
inputWidth="320px"
inputTabIndex={inputTabIndex}
onChange={inputOnChange}
clipActionResource={copyLinkText}
clipEmailResource='E-mail: '
clipPasswordResource='Password: '
tooltipPasswordTitle='Password must contain:'
tooltipPasswordLength={tooltipPasswordLength}
tooltipPasswordDigits='digits'
tooltipPasswordCapital='capital letters'
tooltipPasswordSpecial='special characters (!@#$%^&*)'
generatorSpecial='!@#$%^&*'
passwordSettings={passwordSettings}
isDisabled={inputIsDisabled}
/>
</FieldContainer>
);
});
copyLinkText
} = this.props;
export default PasswordField;
const tooltipPasswordLength =
"from " + passwordSettings.minLength + " to 30 characters";
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<RadioButtonGroup
name={radioName}
selected={radioValue}
options={radioOptions}
isDisabled={radioIsDisabled}
onClick={radioOnChange}
className="radio-group"
/>
<PasswordInput
inputName={inputName}
emailInputName={emailInputName}
inputValue={inputValue}
inputWidth="320px"
inputTabIndex={inputTabIndex}
onChange={inputOnChange}
clipActionResource={copyLinkText}
clipEmailResource="E-mail: "
clipPasswordResource="Password: "
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength={tooltipPasswordLength}
tooltipPasswordDigits="digits"
tooltipPasswordCapital="capital letters"
tooltipPasswordSpecial="special characters (!@#$%^&*)"
generatorSpecial="!@#$%^&*"
passwordSettings={passwordSettings}
isDisabled={inputIsDisabled}
/>
</FieldContainer>
);
}
}
export default PasswordField;

View File

@ -1,35 +1,44 @@
import React from 'react'
import { FieldContainer, RadioButtonGroup } from 'asc-web-components'
import React from "react";
import isEqual from "lodash/isEqual";
import { FieldContainer, RadioButtonGroup } from "asc-web-components";
const RadioField = React.memo((props) => {
const {
isRequired,
hasError,
labelText,
class RadioField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
radioName,
radioValue,
radioOptions,
radioIsDisabled,
radioOnChange
} = props;
render() {
console.log("RadioField render");
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<RadioButtonGroup
name={radioName}
selected={radioValue}
options={radioOptions}
isDisabled={radioIsDisabled}
onClick={radioOnChange}
className="radio-group"
/>
</FieldContainer>
);
});
const {
isRequired,
hasError,
labelText,
export default RadioField
radioName,
radioValue,
radioOptions,
radioIsDisabled,
radioOnChange
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<RadioButtonGroup
name={radioName}
selected={radioValue}
options={radioOptions}
isDisabled={radioIsDisabled}
onClick={radioOnChange}
className="radio-group"
/>
</FieldContainer>
);
}
}
export default RadioField;

View File

@ -1,6 +1,7 @@
import React from 'react'
import styled from 'styled-components';
import { FieldContainer, TextInput, Button } from 'asc-web-components'
import React from "react";
import styled from "styled-components";
import isEqual from "lodash/isEqual";
import { FieldContainer, TextInput, Button } from "asc-web-components";
const InputContainer = styled.div`
width: 100%;
@ -9,47 +10,55 @@ const InputContainer = styled.div`
align-items: center;
`;
const TextChangeField = React.memo((props) => {
const {
isRequired,
hasError,
labelText,
class TextChangeField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
inputName,
inputValue,
inputTabIndex,
render() {
console.log("TextChangeField render");
buttonText,
buttonIsDisabled,
buttonOnClick,
buttonTabIndex
} = props;
const {
isRequired,
hasError,
labelText,
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<InputContainer>
<TextInput
name={inputName}
value={inputValue}
isDisabled={true}
hasError={hasError}
tabIndex={inputTabIndex}
/>
<Button
label={buttonText}
onClick={buttonOnClick}
isDisabled={buttonIsDisabled}
size="medium"
style={{ marginLeft: "8px" }}
tabIndex={buttonTabIndex}
/>
</InputContainer>
</FieldContainer>
);
});
inputName,
inputValue,
inputTabIndex,
export default TextChangeField;
buttonText,
buttonIsDisabled,
buttonOnClick,
buttonTabIndex
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<InputContainer>
<TextInput
name={inputName}
value={inputValue}
isDisabled={true}
hasError={hasError}
tabIndex={inputTabIndex}
/>
<Button
label={buttonText}
onClick={buttonOnClick}
isDisabled={buttonIsDisabled}
size="medium"
style={{ marginLeft: "8px" }}
tabIndex={buttonTabIndex}
/>
</InputContainer>
</FieldContainer>
);
}
}
export default TextChangeField;

View File

@ -1,38 +1,47 @@
import React from 'react'
import { FieldContainer, TextInput } from 'asc-web-components'
import React from "react";
import isEqual from "lodash/isEqual";
import { FieldContainer, TextInput } from "asc-web-components";
const TextField = React.memo((props) => {
const {
isRequired,
hasError,
labelText,
class TextField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
inputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputAutoFocussed,
inputTabIndex
} = props;
render() {
console.log("TextField render");
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
labelText={labelText}
>
<TextInput
name={inputName}
value={inputValue}
isDisabled={inputIsDisabled}
onChange={inputOnChange}
const {
isRequired,
hasError,
labelText,
inputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputAutoFocussed,
inputTabIndex
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError}
className="field-input"
isAutoFocussed={inputAutoFocussed}
tabIndex={inputTabIndex}
/>
</FieldContainer>
);
});
labelText={labelText}
>
<TextInput
name={inputName}
value={inputValue}
isDisabled={inputIsDisabled}
onChange={inputOnChange}
hasError={hasError}
className="field-input"
isAutoFocussed={inputAutoFocussed}
tabIndex={inputTabIndex}
/>
</FieldContainer>
);
}
}
export default TextField;
export default TextField;

View File

@ -1,7 +1,7 @@
import React from 'react'
import { withRouter } from 'react-router'
import { connect } from 'react-redux'
import { Avatar, Button, Textarea, Text, toastr } from 'asc-web-components'
import { Avatar, Button, Textarea, toastr } from 'asc-web-components'
import { withTranslation } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts } from "../../../../../store/people/selectors";
import { createProfile } from '../../../../../store/profile/actions';

View File

@ -1,4 +1,4 @@
import { find, filter } from "lodash";
import { find, filter, cloneDeep } from "lodash";
import { EmployeeActivationStatus, EmployeeStatus } from "../../helpers/constants";
export function getSelectedUser(selection, userId) {
@ -141,5 +141,5 @@ export function toEmployeeWrapper(profile) {
contacts: []
};
return { ...emptyData, ...profile };
return cloneDeep({ ...emptyData, ...profile });
}

View File

@ -318,6 +318,10 @@ namespace ASC.Employee.Core.Controllers
[Authorize(AuthenticationSchemes = "confirm")]
public EmployeeWraperFull AddMember(MemberModel memberModel)
{
if (HttpContext.User.IsInRole(ASC.Common.Security.Authorizing.Role.System))
{
SecurityContext.AuthenticateMe(ASC.Core.Configuration.Constants.CoreSystem);
}
SecurityContext.DemandPermissions(Tenant, Constants.Action_AddRemoveUser);
if (string.IsNullOrEmpty(memberModel.Password))

View File

@ -60,8 +60,6 @@ namespace ASC.People
var builder = services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
config.Filters.Add(new AuthorizeFilter(policy));
config.Filters.Add(new TypeFilterAttribute(typeof(TenantStatusFilter)));
config.Filters.Add(new TypeFilterAttribute(typeof(PaymentFilter)));
config.Filters.Add(new TypeFilterAttribute(typeof(IpSecurityFilter)));

View File

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

View File

@ -0,0 +1,24 @@
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import { AUTH_KEY } from './constants';
import Cookies from 'universal-cookie';
export const PublicRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props =>
(new Cookies()).get(AUTH_KEY) ? (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
) : (
<Component {...props} />
)
}
/>
)
};

View File

@ -0,0 +1,37 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Jest All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Current File",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"${fileBasenameNoExtension}",
"--config",
"jest.config.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest-cli/bin/jest",
}
}
]
}

View File

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

View File

@ -6,7 +6,7 @@ import withReadme from "storybook-readme/with-readme";
import Readme from "./README.md";
import AdvancedSelector from "./";
import Section from "../../../.storybook/decorators/section";
import { boolean } from "@storybook/addon-knobs/dist/deprecated";
import { boolean, select } from "@storybook/addon-knobs/dist/deprecated";
import { ArrayValue, BooleanValue } from "react-values";
import Button from "../button";
@ -77,6 +77,7 @@ storiesOf("Components|AdvancedSelector", module)
>
{({ value, set }) => (
<AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
placeholder={text("placeholder", "Search users")}
onSearchChanged={value => {
action("onSearchChanged")(value);
@ -179,6 +180,7 @@ storiesOf("Components|AdvancedSelector", module)
>
{({ value, set }) => (
<AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
isDropDown={true}
isOpen={isOpen}
placeholder={text("placeholder", "Search users")}

View File

@ -12,6 +12,8 @@ import { isArrayEqual } from "../../utils/array";
import findIndex from "lodash/findIndex";
import filter from "lodash/filter";
import DropDown from "../drop-down";
import { handleAnyClick } from "../../utils/event";
import isEmpty from 'lodash/isEmpty';
/* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */
@ -19,7 +21,7 @@ const Container = ({
value,
placeholder,
isMultiSelect,
mode,
size,
width,
maxHeight,
isDisabled,
@ -32,16 +34,27 @@ const Container = ({
groups,
selectedGroups,
onChangeGroup,
isOpen,
isDropDown,
containerWidth,
containerHeight,
...props
}) => <div {...props} />;
/* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */
const StyledContainer = styled(Container)`
${props => (props.width ? `width: ${props.width}px;` : "")}
display: flex;
flex-direction: column;
${props => (props.containerWidth ? `width: ${props.containerWidth}px;` : "")}
${props =>
props.containerHeight
? `height: ${props.containerHeight}px;`
: ""}
.data_container {
margin: 16px;
margin: 16px 16px 0 16px;
.options_searcher {
margin-bottom: 12px;
@ -53,7 +66,7 @@ const StyledContainer = styled(Container)`
.option_select_all_checkbox {
margin-bottom: 12px;
margin-left: 8px;
/*margin-left: 8px;*/
}
.options_list {
@ -62,33 +75,36 @@ const StyledContainer = styled(Container)`
cursor: pointer;
.option_checkbox {
margin-left: 8px;
/*margin-left: 8px;*/
}
.option_link {
padding-left: 8px;
}
&:hover {
/*&:hover {
background-color: #eceef1;
}
}*/
}
}
}
.button_container {
border-top: 1px solid #eceef1;
.add_members_btn {
margin: 16px;
width: 293px;
border-top: 1px solid #eceef1;
display: flex;
.add_members_btn {
margin: 16px;
}
}
}
`;
class AdvancedSelector extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
const groups = this.convertGroups(this.props.groups);
const currentGroup = this.getCurrentGroup(groups);
@ -98,25 +114,53 @@ class AdvancedSelector extends React.Component {
groups: groups,
currentGroup: currentGroup
};
if (props.isOpen) handleAnyClick(true, this.handleClick);
}
handleClick = e => {
if (this.props.isOpen && !this.ref.current.contains(e.target)) {
this.props.onSelect && this.props.onSelect(this.state.selectedOptions);
}
};
componentWillUnmount() {
handleAnyClick(false, this.handleClick);
}
componentDidUpdate(prevProps) {
let newState = {};
if (!isArrayEqual(this.props.selectedOptions, prevProps.selectedOptions)) {
this.setState({ selectedOptions: this.props.selectedOptions });
newState = { selectedOptions: this.props.selectedOptions };
}
if (this.props.isMultiSelect !== prevProps.isMultiSelect) {
this.setState({ selectedOptions: [] });
newState = Object.assign({}, newState, {
selectedOptions: []
});
}
if (this.props.selectedAll !== prevProps.selectedAll) {
this.setState({ selectedAll: this.props.selectedAll });
newState = Object.assign({}, newState, {
selectedAll: this.props.selectedAll
});
}
if (!isArrayEqual(this.props.groups, prevProps.groups)) {
const groups = this.convertGroups(this.props.groups);
const currentGroup = this.getCurrentGroup(groups);
this.setState({ groups, currentGroup });
newState = Object.assign({}, newState, {
groups, currentGroup
});
}
if(!isEmpty(newState)) {
this.setState({ ...this.state, ...newState });
}
if (this.props.isOpen !== prevProps.isOpen) {
handleAnyClick(this.props.isOpen, this.handleClick);
}
}
@ -215,19 +259,29 @@ class AdvancedSelector extends React.Component {
const {
value,
placeholder,
maxHeight,
isDisabled,
onSearchChanged,
options,
isMultiSelect,
buttonLabel,
selectAllLabel
selectAllLabel,
size
} = this.props;
const { selectedOptions, selectedAll, currentGroup, groups } = this.state;
const containerHeight = size === "compact" ? (!groups || !groups.length ? 336 : 326) : 545;
const containerWidth = size === "compact" ? (!groups || !groups.length ? 325 : 326) : 690;
const listHeight = size === "compact" ? (!groups || !groups.length ? 176 : 120) : 345;
const itemHeight = 32;
return (
<StyledContainer {...this.props}>
<div className="data_container">
<StyledContainer
containerHeight={containerHeight}
containerWidth={containerWidth}
{...this.props}
>
<div className="data_container" ref={this.ref}>
<SearchInput
className="options_searcher"
isDisabled={isDisabled}
@ -264,10 +318,10 @@ class AdvancedSelector extends React.Component {
)}
<FixedSizeList
className="options_list"
height={maxHeight}
itemSize={32}
itemCount={options.length}
itemData={options}
height={listHeight}
itemSize={itemHeight}
itemCount={this.props.options.length}
itemData={this.props.options}
outerElementType={CustomScrollbarsVirtualList}
>
{this.renderRow.bind(this)}
@ -310,8 +364,7 @@ AdvancedSelector.propTypes = {
value: PropTypes.string,
placeholder: PropTypes.string,
isMultiSelect: PropTypes.bool,
mode: PropTypes.oneOf(["base", "compact"]),
width: PropTypes.number,
size: PropTypes.oneOf(["compact", "full"]),
maxHeight: PropTypes.number,
isDisabled: PropTypes.bool,
onSearchChanged: PropTypes.func,
@ -330,9 +383,7 @@ AdvancedSelector.propTypes = {
AdvancedSelector.defaultProps = {
isMultiSelect: false,
width: 325,
maxHeight: 545,
mode: "base",
size: "compact",
buttonLabel: "Add members",
selectAllLabel: "Select all"
};

View File

@ -0,0 +1,98 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import { withKnobs, text } from "@storybook/addon-knobs/react";
import AdvancedSelector from "../advanced-selector";
import Section from "../../../.storybook/decorators/section";
import { boolean } from "@storybook/addon-knobs/dist/deprecated";
import { ArrayValue, BooleanValue } from "react-values";
import Button from "../button";
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", () => {
const options = [
{
key: "group-all",
label: "All groups",
total: 0
},
{
key: "group-dev",
label: "Development",
total: 0
},
{
key: "group-management",
label: "Management",
total: 0
},
{
key: "group-marketing",
label: "Marketing",
total: 0
},
{
key: "group-mobile",
label: "Mobile",
total: 0
},
{
key: "group-support",
label: "Support",
total: 0
},
{
key: "group-web",
label: "Web",
total: 0
}
];
return (
<Section>
<BooleanValue
defaultValue={true}
onChange={() => action("isOpen changed")}
>
{({ value: isOpen, toggle }) => (
<div style={{ position: "relative" }}>
<Button label="Toggle dropdown" onClick={toggle} />
<ArrayValue
defaultValue={options}
onChange={() => action("options onChange")}
>
{({ value, set }) => (
<AdvancedSelector
isDropDown={true}
isOpen={isOpen}
maxHeight={336}
width={379}
placeholder={text("placeholder", "Search")}
onSearchChanged={value => {
action("onSearchChanged")(value);
set(
options.filter(option => {
return option.label.indexOf(value) > -1;
})
);
}}
options={value}
isMultiSelect={boolean("isMultiSelect", true)}
buttonLabel={text("buttonLabel", "Add departments")}
selectAllLabel={text("selectAllLabel", "Select all")}
onSelect={selectedOptions => {
action("onSelect")(selectedOptions);
toggle();
}}
/>
)}
</ArrayValue>
</div>
)}
</BooleanValue>
</Section>
);
});

View File

@ -2,16 +2,17 @@ import React from "react";
import PropTypes from "prop-types";
import styled from 'styled-components';
import { Icons } from '../icons';
import isEqual from 'lodash/isEqual';
const StyledOuter = styled.div`
width: ${props => props.size ? Math.abs(parseInt(props.size)) + "px" : "20px"};
cursor: ${props => props.isDisabled || !props.isClickable ? 'default' : 'pointer'};
line-height: 0;
`;
class IconButton extends React.Component{
class IconButton extends React.Component {
constructor(props) {
super(props);
this.state = {
currentIconName: this.props.iconName,
currentIconColor: this.props.color
@ -24,9 +25,9 @@ class IconButton extends React.Component{
this.isNeedUpdate = false;
}
onMouseEnter(e){
if(!this.props.isDisabled){
onMouseEnter(e) {
if (!this.props.isDisabled) {
this.setState({
currentIconName: this.props.iconHoverName ? this.props.iconHoverName : this.props.iconName,
currentIconColor: this.props.hoverColor ? this.props.hoverColor : this.props.color
@ -34,8 +35,8 @@ class IconButton extends React.Component{
this.props.onMouseEnter && this.props.onMouseEnter(e);
}
}
onMouseLeave(e){
if(!this.props.isDisabled){
onMouseLeave(e) {
if (!this.props.isDisabled) {
this.setState({
currentIconName: this.props.iconName,
currentIconColor: this.props.color
@ -43,22 +44,22 @@ class IconButton extends React.Component{
this.props.onMouseDown && this.props.onMouseDown(e);
}
}
onMouseDown(e){
if(!this.props.isDisabled){
onMouseDown(e) {
if (!this.props.isDisabled) {
this.setState({
currentIconName: this.props.iconClickName ? this.props.iconClickName : this.props.iconName,
currentIconColor: this.props.clickColor ? this.props.clickColor : this.props.color
currentIconColor: this.props.clickColor ? this.props.clickColor : this.props.color
});
this.props.onMouseDown && this.props.onMouseDown(e);
}
}
onMouseUp(e){
if(!this.props.isDisabled){
onMouseUp(e) {
if (!this.props.isDisabled) {
switch (e.nativeEvent.which) {
case 1: //Left click
this.setState({
currentIconName: this.props.iconHoverName ? this.props.iconHoverName : this.props.iconName,
currentIconColor: this.props.iconHoverName ? this.props.iconHoverName : this.props.color
currentIconColor: this.props.iconHoverName ? this.props.iconHoverName : this.props.color
});
this.props.onClick && this.props.onClick(e);
this.props.onMouseUp && this.props.onMouseUp(e);
@ -66,43 +67,32 @@ class IconButton extends React.Component{
case 3://Right click
this.props.onMouseUp && this.props.onMouseUp(e);
break;
default:
break;
}
}
}
}
shouldComponentUpdate(nextProps, nextState){
if(!this.isNeedUpdate){
for (let propsKey in this.props) {
if(typeof this.props[propsKey] != "function" && typeof this.props[propsKey] != "object" && this.props[propsKey] != nextProps[propsKey]){
this.isNeedUpdate = true;
if(propsKey == "iconName"){
this.setState({
currentIconName: nextProps[propsKey]
});
break;
}
}
shouldComponentUpdate(nextProps, nextState) {
if (!isEqual(this.props, nextProps)) {
let newState = {
currentIconName: this.state.currentIconName,
currentIconColor: this.state.currentIconColor
}
for (let stateKey in this.state) {
if(typeof this.state[stateKey] != "function" && typeof this.state[stateKey] != "object" && this.state[stateKey] != nextState[stateKey]){
this.isNeedUpdate = true;
break;
}
}
if(!this.isNeedUpdate) return false;
else return true;
if (this.props.iconName !== nextProps.iconName) newState.currentIconName = nextProps.iconName;
if (this.props.color !== nextProps.color) newState.currentIconColor = nextProps.color;
this.setState(newState);
return true;
}
this.isNeedUpdate = false;
return true;
return !isEqual(this.state, nextState);
}
render(){
render() {
//console.log("IconButton render");
return (
<StyledOuter
size={this.props.size}
isDisabled={this.props.isDisabled}
<StyledOuter
size={this.props.size}
isDisabled={this.props.isDisabled}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
@ -111,7 +101,7 @@ class IconButton extends React.Component{
isClickable={typeof this.props.onClick === 'function'}
>
{React.createElement(Icons[this.state.currentIconName], {size: "scale", color: this.state.currentIconColor, isfill: this.props.isFill})}
{React.createElement(Icons[this.state.currentIconName], { size: "scale", color: this.state.currentIconColor, isfill: this.props.isFill })}
</StyledOuter>
);
}
@ -127,7 +117,7 @@ IconButton.propTypes = {
iconName: PropTypes.string.isRequired,
iconHoverName: PropTypes.string,
iconClickName: PropTypes.string,
onClick:PropTypes.func
onClick: PropTypes.func
};
IconButton.defaultProps = {

View File

@ -273,6 +273,7 @@ class PasswordInput extends React.PureComponent {
} = this.state;
const iconsColor = isDisabled ? '#D0D5DA' : '#A3A9AE';
const iconName = type === 'password' ? 'EyeIcon' : 'EyeOffIcon';
const tooltipContent = (
<StyledTooltipContainer forwardedAs='div' title={tooltipPasswordTitle}>
@ -305,7 +306,7 @@ class PasswordInput extends React.PureComponent {
name={inputName}
hasError={hasError}
isDisabled={isDisabled}
iconName='EyeIcon'
iconName={iconName}
value={inputValue}
onIconClick={this.changeInputType}
onChange={this.onChangeAction}

View File

@ -2,40 +2,98 @@ import React from 'react';
import { mount } from 'enzyme';
import PasswordInput from '.';
const basePasswordSettings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
};
const baseProps = {
inputName: 'demoPasswordInput',
emailInputName: 'demoEmailInput',
inputValue: '',
clipActionResource: 'Copy e-mail and password',
clipEmailResource: 'E-mail: ',
clipPasswordResource: 'Password: ',
tooltipPasswordTitle: 'Password must contain:',
tooltipPasswordLength: 'from 6 to 30 characters',
tooltipPasswordDigits: 'digits',
tooltipPasswordCapital: 'capital letters',
tooltipPasswordSpecial: 'special characters (!@#$%^&*)',
generatorSpecial: '!@#$%^&*',
passwordSettings: basePasswordSettings,
isDisabled: false,
placeholder: 'password',
onChange: () => jest.fn(),
onValidateInput: () => jest.fn(),
onCopyToClipboard: () => jest.fn()
}
describe('<PasswordInput />', () => {
it('renders without error', () => {
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false
};
const wrapper = mount(
<PasswordInput
inputName="demoPasswordInput"
emailInputName="demoEmailInput"
inputValue={""}
onChange={e => {
console.log(e.target.value);
}}
clipActionResource="Copy e-mail and password"
clipEmailResource="E-mail: "
clipPasswordResource="Password: "
tooltipPasswordTitle="Password must contain:"
tooltipPasswordLength="from 6 to 30 characters"
tooltipPasswordDigits="digits"
tooltipPasswordCapital="capital letters"
tooltipPasswordSpecial="special characters (!@#$%^&*)"
generatorSpecial="!@#$%^&*"
passwordSettings={settings}
isDisabled={false}
placeholder="password"
onValidateInput={a => console.log(a)}
onCopyToClipboard={b => console.log("Data " + b + " copied to clipboard")}
/>
);
const wrapper = mount(<PasswordInput {...baseProps} />);
expect(wrapper).toExist();
});
it('render password input', () => {
const wrapper = mount(<PasswordInput {...baseProps} />);
expect(wrapper.find('input').prop('type')).toEqual('password');
});
it('have an HTML name', () => {
const wrapper = mount(<PasswordInput {...baseProps} />);
expect(wrapper.find('input').prop('name')).toEqual('demoPasswordInput');
});
it('forward passed value', () => {
const wrapper = mount(<PasswordInput {...baseProps} inputValue='demo' />);
expect(wrapper.props().inputValue).toEqual('demo');
});
it('call onChange when changing value', () => {
const onChange = jest.fn(event => {
expect(event.target.id).toEqual('demoPasswordInput');
expect(event.target.name).toEqual('demoPasswordInput');
expect(event.target.value).toEqual('demo');
});
const wrapper = mount(<PasswordInput {...baseProps} id="demoPasswordInput" name="demoPasswordInput" onChange={onChange} />);
const event = { target: { value: "demo" } };
wrapper.simulate('change', event);
});
it('call onFocus when input is focused', () => {
const onFocus = jest.fn(event => {
expect(event.target.id).toEqual('demoPasswordInput');
expect(event.target.name).toEqual('demoPasswordInput');
});
const wrapper = mount(<PasswordInput {...baseProps} id="demoPasswordInput" name="demoPasswordInput" onFocus={onFocus} />);
wrapper.simulate('focus');
});
it('call onBlur when input loses focus', () => {
const onBlur = jest.fn(event => {
expect(event.target.id).toEqual('demoPasswordInput');
expect(event.target.name).toEqual('demoPasswordInput');
});
const wrapper = mount(<PasswordInput {...baseProps} id="demoPasswordInput" name="demoPasswordInput" onBlur={onBlur} />);
wrapper.simulate('blur');
});
it('disabled when isDisabled is passed', () => {
const wrapper = mount(<PasswordInput {...baseProps} isDisabled={true} />);
expect(wrapper.prop('isDisabled')).toEqual(true);
});
});