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 *.suo
*.user *.user
.vs/ .vs/
.vscode/
*-lock.json *-lock.json
**/node_modules/ **/node_modules/
**/storybook-static/ **/storybook-static/
@ -12,3 +11,4 @@
*.log *.log
/packages/asc-web-components /packages/asc-web-components
/products/ASC.People/Data/ /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 React from "react";
import styled from 'styled-components'; import styled from "styled-components";
import { ComboBox, TextInput } from 'asc-web-components' import isEqual from "lodash/isEqual";
import ContactField from "./ContactField";
import { ComboBox } from "asc-web-components";
const Container = styled.div` const Container = styled.div`
width: 100%; 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) => { const getOptions = (patterns, keyPrefix) => {
return patterns.map((item, index) => { return patterns.map((item, index) => {
return { return {
key: keyPrefix + index, key: keyPrefix + index,
label: item.type, //from resource label: item.type, //from resource
icon: item.icon, icon: item.icon,
value: item.type value: item.type
}; };
}); });
} };
const renderItems = (contacts, pattern, onTypeChange, onTextChange, isDisabled) => { const renderItems = (
const items = contacts.map((contact, index) => { 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]; class ContactsField extends React.Component {
shouldComponentUpdate(nextProps) {
return !isEqual(this.props, nextProps);
}
return ( render() {
<Item key={prefix + "item_" + index}> console.log("ContactsField render");
<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; const {
}; pattern,
contacts,
addItemText,
onItemAdd,
onItemTypeChange,
onItemTextChange,
isDisabled
} = this.props;
const ContactsField = React.memo((props) => { const existItems = renderItems(
contacts,
const { pattern, contacts, addItemText, onItemAdd, onItemTypeChange, onItemTextChange, isDisabled } = props; pattern,
onItemTypeChange,
const existItems = renderItems(contacts, pattern, onItemTypeChange, onItemTextChange, isDisabled); onItemTextChange,
isDisabled
);
const prefix = "null_"; const prefix = "null_";
const options = getOptions(pattern, prefix); const options = getOptions(pattern, prefix);
return ( return (
<Container> <Container>
{existItems} {existItems}
<ComboBox <ComboBox
options={options} options={options}
onSelect={onItemAdd} onSelect={onItemAdd}
selectedOption={{ selectedOption={{
key: prefix, key: prefix,
label: addItemText, label: addItemText,
value: "" value: ""
}} }}
isDisabled={isDisabled} isDisabled={isDisabled}
scaled={false} scaled={false}
className="field-select" className="field-select"
/> />
</Container> </Container>
); );
}); }
}
export default ContactsField export default ContactsField;

View File

@ -1,35 +1,44 @@
import React from 'react' import React from "react";
import { FieldContainer, DateInput } from 'asc-web-components' import isEqual from "lodash/isEqual";
import { FieldContainer, DatePicker } from "asc-web-components";
const DateField = React.memo((props) => { class DateField extends React.Component {
const { shouldComponentUpdate(nextProps) {
isRequired, return !isEqual(this.props, nextProps);
hasError, }
labelText,
inputName, render() {
inputValue, console.log("DateField render");
inputIsDisabled,
inputOnChange,
inputTabIndex
} = props;
return ( const {
<FieldContainer isRequired,
isRequired={isRequired} hasError,
hasError={hasError} labelText,
labelText={labelText}
> inputName,
<DateInput inputValue,
name={inputName} inputIsDisabled,
selected={inputValue} inputOnChange,
disabled={inputIsDisabled} inputTabIndex
onChange={inputOnChange} } = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError} hasError={hasError}
tabIndex={inputTabIndex} labelText={labelText}
/> >
</FieldContainer> <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 React from "react";
import { FieldContainer, SelectorAddButton, SelectedItem } from 'asc-web-components' import isEqual from "lodash/isEqual";
import {
FieldContainer,
SelectorAddButton,
SelectedItem
} from "asc-web-components";
const DepartmentField = React.memo((props) => { class DepartmentField extends React.Component {
const { shouldComponentUpdate(nextProps) {
isRequired, return !isEqual(this.props, nextProps);
isDisabled, }
hasError,
labelText,
addButtonTitle,
departments,
onAddDepartment,
onRemoveDepartment
} = props;
return ( render() {
<FieldContainer console.log("DepartmentField render");
isRequired={isRequired}
hasError={hasError} const {
labelText={labelText} isRequired,
className="departments-field" isDisabled,
> hasError,
<SelectorAddButton labelText,
isDisabled={isDisabled} addButtonTitle,
title={addButtonTitle} departments,
onClick={onAddDepartment} onAddDepartment,
className="department-add-btn" onRemoveDepartment
/> } = this.props;
{departments && departments.map((department) => (
<SelectedItem return (
key={`department_${department.id}`} <FieldContainer
text={department.name} isRequired={isRequired}
onClose={() => { onRemoveDepartment(department.id) }} hasError={hasError}
isInline={true} labelText={labelText}
className="department-item" className="departments-field"
>
<SelectorAddButton
isDisabled={isDisabled}
title={addButtonTitle}
onClick={onAddDepartment}
className="department-add-btn"
/> />
))} {departments.map(department => (
</FieldContainer> <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 React from "react";
import styled from 'styled-components'; import styled from "styled-components";
import { Text } from 'asc-web-components' import { Text } from "asc-web-components";
const Container = styled.div` const Container = styled.div`
margin: 0 0 40px 0; margin: 0 0 40px 0;
@ -11,7 +11,7 @@ const Header = styled(Text.ContentHeader)`
line-height: unset; line-height: unset;
`; `;
const InfoFieldContainer = React.memo((props) => { const InfoFieldContainer = React.memo(props => {
const { headerText, children } = props; const { headerText, children } = props;
return ( 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 React from "react";
import { FieldContainer, RadioButtonGroup, PasswordInput } from 'asc-web-components' import isEqual from "lodash/isEqual";
import {
FieldContainer,
RadioButtonGroup,
PasswordInput
} from "asc-web-components";
const PasswordField = React.memo((props) => { class PasswordField extends React.Component {
const { shouldComponentUpdate(nextProps) {
isRequired, return !isEqual(this.props, nextProps);
hasError, }
labelText,
passwordSettings,
radioName, render() {
radioValue, console.log("PasswordField render");
radioOptions,
radioIsDisabled,
radioOnChange,
inputName, const {
emailInputName, isRequired,
inputValue, hasError,
inputIsDisabled, labelText,
inputOnChange, passwordSettings,
inputTabIndex,
copyLinkText, radioName,
} = props; radioValue,
radioOptions,
radioIsDisabled,
radioOnChange,
const tooltipPasswordLength = 'from ' + passwordSettings.minLength + ' to 30 characters'; inputName,
emailInputName,
inputValue,
inputIsDisabled,
inputOnChange,
inputTabIndex,
return ( copyLinkText
<FieldContainer } = this.props;
isRequired={isRequired}
hasError={hasError} const tooltipPasswordLength =
labelText={labelText} "from " + passwordSettings.minLength + " to 30 characters";
>
<RadioButtonGroup return (
name={radioName} <FieldContainer
selected={radioValue} isRequired={isRequired}
options={radioOptions} hasError={hasError}
isDisabled={radioIsDisabled} labelText={labelText}
onClick={radioOnChange} >
className="radio-group" <RadioButtonGroup
/> name={radioName}
<PasswordInput selected={radioValue}
inputName={inputName} options={radioOptions}
emailInputName={emailInputName} isDisabled={radioIsDisabled}
inputValue={inputValue} onClick={radioOnChange}
inputWidth="320px" className="radio-group"
inputTabIndex={inputTabIndex} />
onChange={inputOnChange} <PasswordInput
clipActionResource={copyLinkText} inputName={inputName}
clipEmailResource='E-mail: ' emailInputName={emailInputName}
clipPasswordResource='Password: ' inputValue={inputValue}
tooltipPasswordTitle='Password must contain:' inputWidth="320px"
tooltipPasswordLength={tooltipPasswordLength} inputTabIndex={inputTabIndex}
tooltipPasswordDigits='digits' onChange={inputOnChange}
tooltipPasswordCapital='capital letters' clipActionResource={copyLinkText}
tooltipPasswordSpecial='special characters (!@#$%^&*)' clipEmailResource="E-mail: "
generatorSpecial='!@#$%^&*' clipPasswordResource="Password: "
passwordSettings={passwordSettings} tooltipPasswordTitle="Password must contain:"
isDisabled={inputIsDisabled} tooltipPasswordLength={tooltipPasswordLength}
/> tooltipPasswordDigits="digits"
</FieldContainer> tooltipPasswordCapital="capital letters"
); tooltipPasswordSpecial="special characters (!@#$%^&*)"
}); generatorSpecial="!@#$%^&*"
passwordSettings={passwordSettings}
isDisabled={inputIsDisabled}
/>
</FieldContainer>
);
}
}
export default PasswordField; export default PasswordField;

View File

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

View File

@ -1,38 +1,47 @@
import React from 'react' import React from "react";
import { FieldContainer, TextInput } from 'asc-web-components' import isEqual from "lodash/isEqual";
import { FieldContainer, TextInput } from "asc-web-components";
const TextField = React.memo((props) => { class TextField extends React.Component {
const { shouldComponentUpdate(nextProps) {
isRequired, return !isEqual(this.props, nextProps);
hasError, }
labelText,
inputName, render() {
inputValue, console.log("TextField render");
inputIsDisabled,
inputOnChange,
inputAutoFocussed,
inputTabIndex
} = props;
return ( const {
<FieldContainer isRequired,
isRequired={isRequired} hasError,
hasError={hasError} labelText,
labelText={labelText}
> inputName,
<TextInput inputValue,
name={inputName} inputIsDisabled,
value={inputValue} inputOnChange,
isDisabled={inputIsDisabled} inputAutoFocussed,
onChange={inputOnChange} inputTabIndex
} = this.props;
return (
<FieldContainer
isRequired={isRequired}
hasError={hasError} hasError={hasError}
className="field-input" labelText={labelText}
isAutoFocussed={inputAutoFocussed} >
tabIndex={inputTabIndex} <TextInput
/> name={inputName}
</FieldContainer> 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 React from 'react'
import { withRouter } from 'react-router' import { withRouter } from 'react-router'
import { connect } from 'react-redux' 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 { withTranslation } from 'react-i18next';
import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts } from "../../../../../store/people/selectors"; import { toEmployeeWrapper, getUserRole, getUserContactsPattern, getUserContacts } from "../../../../../store/people/selectors";
import { createProfile } from '../../../../../store/profile/actions'; 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"; import { EmployeeActivationStatus, EmployeeStatus } from "../../helpers/constants";
export function getSelectedUser(selection, userId) { export function getSelectedUser(selection, userId) {
@ -141,5 +141,5 @@ export function toEmployeeWrapper(profile) {
contacts: [] contacts: []
}; };
return { ...emptyData, ...profile }; return cloneDeep({ ...emptyData, ...profile });
} }

View File

@ -318,6 +318,10 @@ namespace ASC.Employee.Core.Controllers
[Authorize(AuthenticationSchemes = "confirm")] [Authorize(AuthenticationSchemes = "confirm")]
public EmployeeWraperFull AddMember(MemberModel memberModel) 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); SecurityContext.DemandPermissions(Tenant, Constants.Action_AddRemoveUser);
if (string.IsNullOrEmpty(memberModel.Password)) if (string.IsNullOrEmpty(memberModel.Password))

View File

@ -60,8 +60,6 @@ namespace ASC.People
var builder = services.AddMvc(config => 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(TenantStatusFilter)));
config.Filters.Add(new TypeFilterAttribute(typeof(PaymentFilter))); config.Filters.Add(new TypeFilterAttribute(typeof(PaymentFilter)));
config.Filters.Add(new TypeFilterAttribute(typeof(IpSecurityFilter))); 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 StudioLayout from "./components/Layout/index";
import Login from "./components/pages/Login"; import Login from "./components/pages/Login";
import { PrivateRoute } from "./helpers/privateRoute"; import { PrivateRoute } from "./helpers/privateRoute";
import { PublicRoute } from "./helpers/publicRoute";
import { Error404 } from "./components/pages/Error"; import { Error404 } from "./components/pages/Error";
const Home = lazy(() => import("./components/pages/Home")); const Home = lazy(() => import("./components/pages/Home"));
@ -18,8 +19,8 @@ const App = () => {
fallback={<Loader className="pageLoader" type="rombs" size={40} />} fallback={<Loader className="pageLoader" type="rombs" size={40} />}
> >
<Switch> <Switch>
<Route exact path="/login" component={Login} /> <PublicRoute exact path="/login" component={Login} />
<Route exact path="/confirm" component={Confirm} /> <PublicRoute path="/confirm" component={Confirm} />
<PrivateRoute exact path="/" component={Home} /> <PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/about" component={About} /> <PrivateRoute exact path="/about" component={About} />
<PrivateRoute component={Error404} /> <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", "name": "asc-web-components",
"version": "1.0.63", "version": "1.0.66",
"description": "Ascensio System SIA component library", "description": "Ascensio System SIA component library",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "dist/asc-web-components.js", "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 Readme from "./README.md";
import AdvancedSelector from "./"; import AdvancedSelector from "./";
import Section from "../../../.storybook/decorators/section"; 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 { ArrayValue, BooleanValue } from "react-values";
import Button from "../button"; import Button from "../button";
@ -77,6 +77,7 @@ storiesOf("Components|AdvancedSelector", module)
> >
{({ value, set }) => ( {({ value, set }) => (
<AdvancedSelector <AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
placeholder={text("placeholder", "Search users")} placeholder={text("placeholder", "Search users")}
onSearchChanged={value => { onSearchChanged={value => {
action("onSearchChanged")(value); action("onSearchChanged")(value);
@ -179,6 +180,7 @@ storiesOf("Components|AdvancedSelector", module)
> >
{({ value, set }) => ( {({ value, set }) => (
<AdvancedSelector <AdvancedSelector
size={select("size", ["compact", "full"], "compact")}
isDropDown={true} isDropDown={true}
isOpen={isOpen} isOpen={isOpen}
placeholder={text("placeholder", "Search users")} placeholder={text("placeholder", "Search users")}

View File

@ -12,6 +12,8 @@ import { isArrayEqual } from "../../utils/array";
import findIndex from "lodash/findIndex"; import findIndex from "lodash/findIndex";
import filter from "lodash/filter"; import filter from "lodash/filter";
import DropDown from "../drop-down"; import DropDown from "../drop-down";
import { handleAnyClick } from "../../utils/event";
import isEmpty from 'lodash/isEmpty';
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
@ -19,7 +21,7 @@ const Container = ({
value, value,
placeholder, placeholder,
isMultiSelect, isMultiSelect,
mode, size,
width, width,
maxHeight, maxHeight,
isDisabled, isDisabled,
@ -32,16 +34,27 @@ const Container = ({
groups, groups,
selectedGroups, selectedGroups,
onChangeGroup, onChangeGroup,
isOpen,
isDropDown,
containerWidth,
containerHeight,
...props ...props
}) => <div {...props} />; }) => <div {...props} />;
/* eslint-enable react/prop-types */ /* eslint-enable react/prop-types */
/* eslint-enable no-unused-vars */ /* eslint-enable no-unused-vars */
const StyledContainer = styled(Container)` 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 { .data_container {
margin: 16px; margin: 16px 16px 0 16px;
.options_searcher { .options_searcher {
margin-bottom: 12px; margin-bottom: 12px;
@ -53,7 +66,7 @@ const StyledContainer = styled(Container)`
.option_select_all_checkbox { .option_select_all_checkbox {
margin-bottom: 12px; margin-bottom: 12px;
margin-left: 8px; /*margin-left: 8px;*/
} }
.options_list { .options_list {
@ -62,33 +75,36 @@ const StyledContainer = styled(Container)`
cursor: pointer; cursor: pointer;
.option_checkbox { .option_checkbox {
margin-left: 8px; /*margin-left: 8px;*/
} }
.option_link { .option_link {
padding-left: 8px; padding-left: 8px;
} }
&:hover { /*&:hover {
background-color: #eceef1; background-color: #eceef1;
} }*/
} }
} }
} }
.button_container { .button_container {
border-top: 1px solid #eceef1; border-top: 1px solid #eceef1;
.add_members_btn { display: flex;
margin: 16px;
width: 293px; .add_members_btn {
margin: 16px;
}
} }
}
`; `;
class AdvancedSelector extends React.Component { class AdvancedSelector extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.ref = React.createRef();
const groups = this.convertGroups(this.props.groups); const groups = this.convertGroups(this.props.groups);
const currentGroup = this.getCurrentGroup(groups); const currentGroup = this.getCurrentGroup(groups);
@ -98,25 +114,53 @@ class AdvancedSelector extends React.Component {
groups: groups, groups: groups,
currentGroup: currentGroup 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) { componentDidUpdate(prevProps) {
let newState = {};
if (!isArrayEqual(this.props.selectedOptions, prevProps.selectedOptions)) { if (!isArrayEqual(this.props.selectedOptions, prevProps.selectedOptions)) {
this.setState({ selectedOptions: this.props.selectedOptions }); newState = { selectedOptions: this.props.selectedOptions };
} }
if (this.props.isMultiSelect !== prevProps.isMultiSelect) { if (this.props.isMultiSelect !== prevProps.isMultiSelect) {
this.setState({ selectedOptions: [] }); newState = Object.assign({}, newState, {
selectedOptions: []
});
} }
if (this.props.selectedAll !== prevProps.selectedAll) { 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)) { if (!isArrayEqual(this.props.groups, prevProps.groups)) {
const groups = this.convertGroups(this.props.groups); const groups = this.convertGroups(this.props.groups);
const currentGroup = this.getCurrentGroup(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 { const {
value, value,
placeholder, placeholder,
maxHeight,
isDisabled, isDisabled,
onSearchChanged, onSearchChanged,
options, options,
isMultiSelect, isMultiSelect,
buttonLabel, buttonLabel,
selectAllLabel selectAllLabel,
size
} = this.props; } = this.props;
const { selectedOptions, selectedAll, currentGroup, groups } = this.state; const { selectedOptions, selectedAll, currentGroup, groups } = this.state;
const containerHeight = size === "compact" ? (!groups || !groups.length ? 336 : 326) : 545;
const containerWidth = size === "compact" ? (!groups || !groups.length ? 325 : 326) : 690;
const listHeight = size === "compact" ? (!groups || !groups.length ? 176 : 120) : 345;
const itemHeight = 32;
return ( return (
<StyledContainer {...this.props}> <StyledContainer
<div className="data_container"> containerHeight={containerHeight}
containerWidth={containerWidth}
{...this.props}
>
<div className="data_container" ref={this.ref}>
<SearchInput <SearchInput
className="options_searcher" className="options_searcher"
isDisabled={isDisabled} isDisabled={isDisabled}
@ -264,10 +318,10 @@ class AdvancedSelector extends React.Component {
)} )}
<FixedSizeList <FixedSizeList
className="options_list" className="options_list"
height={maxHeight} height={listHeight}
itemSize={32} itemSize={itemHeight}
itemCount={options.length} itemCount={this.props.options.length}
itemData={options} itemData={this.props.options}
outerElementType={CustomScrollbarsVirtualList} outerElementType={CustomScrollbarsVirtualList}
> >
{this.renderRow.bind(this)} {this.renderRow.bind(this)}
@ -310,8 +364,7 @@ AdvancedSelector.propTypes = {
value: PropTypes.string, value: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
isMultiSelect: PropTypes.bool, isMultiSelect: PropTypes.bool,
mode: PropTypes.oneOf(["base", "compact"]), size: PropTypes.oneOf(["compact", "full"]),
width: PropTypes.number,
maxHeight: PropTypes.number, maxHeight: PropTypes.number,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
onSearchChanged: PropTypes.func, onSearchChanged: PropTypes.func,
@ -330,9 +383,7 @@ AdvancedSelector.propTypes = {
AdvancedSelector.defaultProps = { AdvancedSelector.defaultProps = {
isMultiSelect: false, isMultiSelect: false,
width: 325, size: "compact",
maxHeight: 545,
mode: "base",
buttonLabel: "Add members", buttonLabel: "Add members",
selectAllLabel: "Select all" 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,13 +2,14 @@ import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import styled from 'styled-components'; import styled from 'styled-components';
import { Icons } from '../icons'; import { Icons } from '../icons';
import isEqual from 'lodash/isEqual';
const StyledOuter = styled.div` const StyledOuter = styled.div`
width: ${props => props.size ? Math.abs(parseInt(props.size)) + "px" : "20px"}; width: ${props => props.size ? Math.abs(parseInt(props.size)) + "px" : "20px"};
cursor: ${props => props.isDisabled || !props.isClickable ? 'default' : 'pointer'}; cursor: ${props => props.isDisabled || !props.isClickable ? 'default' : 'pointer'};
line-height: 0; line-height: 0;
`; `;
class IconButton extends React.Component{ class IconButton extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -25,8 +26,8 @@ class IconButton extends React.Component{
} }
onMouseEnter(e){ onMouseEnter(e) {
if(!this.props.isDisabled){ if (!this.props.isDisabled) {
this.setState({ this.setState({
currentIconName: this.props.iconHoverName ? this.props.iconHoverName : this.props.iconName, currentIconName: this.props.iconHoverName ? this.props.iconHoverName : this.props.iconName,
currentIconColor: this.props.hoverColor ? this.props.hoverColor : this.props.color 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); this.props.onMouseEnter && this.props.onMouseEnter(e);
} }
} }
onMouseLeave(e){ onMouseLeave(e) {
if(!this.props.isDisabled){ if (!this.props.isDisabled) {
this.setState({ this.setState({
currentIconName: this.props.iconName, currentIconName: this.props.iconName,
currentIconColor: this.props.color currentIconColor: this.props.color
@ -43,22 +44,22 @@ class IconButton extends React.Component{
this.props.onMouseDown && this.props.onMouseDown(e); this.props.onMouseDown && this.props.onMouseDown(e);
} }
} }
onMouseDown(e){ onMouseDown(e) {
if(!this.props.isDisabled){ if (!this.props.isDisabled) {
this.setState({ this.setState({
currentIconName: this.props.iconClickName ? this.props.iconClickName : this.props.iconName, 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); this.props.onMouseDown && this.props.onMouseDown(e);
} }
} }
onMouseUp(e){ onMouseUp(e) {
if(!this.props.isDisabled){ if (!this.props.isDisabled) {
switch (e.nativeEvent.which) { switch (e.nativeEvent.which) {
case 1: //Left click case 1: //Left click
this.setState({ this.setState({
currentIconName: this.props.iconHoverName ? this.props.iconHoverName : this.props.iconName, 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.onClick && this.props.onClick(e);
this.props.onMouseUp && this.props.onMouseUp(e); this.props.onMouseUp && this.props.onMouseUp(e);
@ -69,35 +70,24 @@ class IconButton extends React.Component{
default: default:
break; break;
} }
} }
} }
shouldComponentUpdate(nextProps, nextState){ shouldComponentUpdate(nextProps, nextState) {
if(!this.isNeedUpdate){
for (let propsKey in this.props) { if (!isEqual(this.props, nextProps)) {
if(typeof this.props[propsKey] != "function" && typeof this.props[propsKey] != "object" && this.props[propsKey] != nextProps[propsKey]){ let newState = {
this.isNeedUpdate = true; currentIconName: this.state.currentIconName,
if(propsKey == "iconName"){ currentIconColor: this.state.currentIconColor
this.setState({
currentIconName: nextProps[propsKey]
});
break;
}
}
} }
for (let stateKey in this.state) { if (this.props.iconName !== nextProps.iconName) newState.currentIconName = nextProps.iconName;
if(typeof this.state[stateKey] != "function" && typeof this.state[stateKey] != "object" && this.state[stateKey] != nextState[stateKey]){ if (this.props.color !== nextProps.color) newState.currentIconColor = nextProps.color;
this.isNeedUpdate = true; this.setState(newState);
break; return true;
}
}
if(!this.isNeedUpdate) return false;
else return true;
} }
this.isNeedUpdate = false; return !isEqual(this.state, nextState);
return true;
} }
render(){ render() {
//console.log("IconButton render"); //console.log("IconButton render");
return ( return (
<StyledOuter <StyledOuter
@ -111,7 +101,7 @@ class IconButton extends React.Component{
isClickable={typeof this.props.onClick === 'function'} 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> </StyledOuter>
); );
} }
@ -127,7 +117,7 @@ IconButton.propTypes = {
iconName: PropTypes.string.isRequired, iconName: PropTypes.string.isRequired,
iconHoverName: PropTypes.string, iconHoverName: PropTypes.string,
iconClickName: PropTypes.string, iconClickName: PropTypes.string,
onClick:PropTypes.func onClick: PropTypes.func
}; };
IconButton.defaultProps = { IconButton.defaultProps = {

View File

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

View File

@ -2,40 +2,98 @@ import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import PasswordInput from '.'; 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 />', () => { describe('<PasswordInput />', () => {
it('renders without error', () => { it('renders without error', () => {
const settings = { const wrapper = mount(<PasswordInput {...baseProps} />);
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")}
/>
);
expect(wrapper).toExist(); 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);
});
}); });