Merge branch 'master' of https://github.com/ONLYOFFICE/CommunityServer-AspNetCore
This commit is contained in:
commit
aa7bb166bd
@ -20,6 +20,7 @@
|
||||
"oidc-client": "^1.9.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.9.0",
|
||||
"react-device-detect": "^1.7.5",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-i18next": "10.12.2",
|
||||
"react-redux": "7.1.1",
|
||||
|
@ -31,6 +31,8 @@ import {
|
||||
} from "../../../../../store/people/selectors";
|
||||
import { isAdmin, isMe } from "../../../../../store/auth/selectors";
|
||||
import { EmployeeStatus } from "../../../../../helpers/constants";
|
||||
import { resendUserInvites } from "../../../../../store/services/api";
|
||||
import { isMobileOnly } from "react-device-detect";
|
||||
|
||||
class SectionBodyContent extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@ -51,8 +53,8 @@ class SectionBodyContent extends React.PureComponent {
|
||||
window.open("mailto:" + email);
|
||||
};
|
||||
|
||||
onSendMessageClick = () => {
|
||||
toastr.success("Context action: Send message");
|
||||
onSendMessageClick = mobilePhone => {
|
||||
window.open(`sms:${mobilePhone}`);
|
||||
};
|
||||
|
||||
onEditClick = user => {
|
||||
@ -263,8 +265,13 @@ class SectionBodyContent extends React.PureComponent {
|
||||
});
|
||||
};
|
||||
|
||||
onInviteAgainClick = () => {
|
||||
toastr.success("Context action: Invite again");
|
||||
onInviteAgainClick = user => {
|
||||
const { onLoading } = this.props;
|
||||
onLoading(true);
|
||||
resendUserInvites([user.id])
|
||||
.then(() => toastr.success(<Text.Body>The email activation instructions have been sent to the <b>{user.email}</b> email address</Text.Body>))
|
||||
.catch(e => toastr.error("ERROR"))
|
||||
.finally(() => onLoading(false));
|
||||
};
|
||||
getUserContextOptions = (user, viewer) => {
|
||||
let status = "";
|
||||
@ -288,10 +295,11 @@ class SectionBodyContent extends React.PureComponent {
|
||||
label: t("LblSendEmail"),
|
||||
onClick: this.onEmailSentClick.bind(this, user.email)
|
||||
},
|
||||
user.mobilePhone && isMobileOnly &&
|
||||
{
|
||||
key: "send-message",
|
||||
label: t("LblSendMessage"),
|
||||
onClick: this.onSendMessageClick
|
||||
onClick: this.onSendMessageClick.bind(this, user.mobilePhone)
|
||||
},
|
||||
{ key: "separator", isSeparator: true },
|
||||
{
|
||||
@ -354,7 +362,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
{
|
||||
key: "invite-again",
|
||||
label: t("LblInviteAgain"),
|
||||
onClick: this.onInviteAgainClick
|
||||
onClick: this.onInviteAgainClick.bind(this, user)
|
||||
},
|
||||
!isSelf &&
|
||||
(user.status === EmployeeStatus.Active
|
||||
|
@ -7,6 +7,7 @@ import { withTranslation } from 'react-i18next';
|
||||
import { updateUserStatus, updateUserType } from '../../../../../store/people/actions';
|
||||
import { EmployeeStatus, EmployeeType } from '../../../../../helpers/constants';
|
||||
import { typeUser , typeGuest } from '../../../../../helpers/../helpers/customNames';
|
||||
import { resendUserInvites } from '../../../../../store/services/api';
|
||||
|
||||
const contextOptions = ( t ) => {
|
||||
return [
|
||||
@ -65,6 +66,14 @@ const SectionHeaderContent = React.memo(({
|
||||
toastr.success(t('SuccessChangeUserType'));
|
||||
}, [selectedUserIds, updateUserType, t]);
|
||||
|
||||
const onSentInviteAgain = useCallback(() => {
|
||||
resendUserInvites(selectedUserIds)
|
||||
.then(() =>
|
||||
toastr.success("The invitation was successfully sent")
|
||||
)
|
||||
.catch(e => toastr.error("ERROR"));
|
||||
}, [selectedUserIds]);
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: t('LblSelect'),
|
||||
@ -102,7 +111,7 @@ const SectionHeaderContent = React.memo(({
|
||||
{
|
||||
label: t('LblInviteAgain'),
|
||||
disabled: !selection.length,
|
||||
onClick: toastr.success.bind(this, "Invite again action")
|
||||
onClick: onSentInviteAgain
|
||||
},
|
||||
{
|
||||
label: t('LblSendEmail'),
|
||||
|
@ -4,14 +4,11 @@ import PropTypes from "prop-types";
|
||||
import { PageLayout, Loader } from "asc-web-components";
|
||||
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
|
||||
import { SectionHeaderContent, SectionBodyContent } from './Section';
|
||||
import { setProfile, fetchProfile, resetProfile } from '../../../store/profile/actions';
|
||||
import { fetchProfile } from '../../../store/profile/actions';
|
||||
import i18n from "./i18n";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
class Profile extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { match, fetchProfile } = this.props;
|
||||
@ -66,6 +63,7 @@ Profile.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
profile: PropTypes.object,
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
isLoaded: PropTypes.bool
|
||||
};
|
||||
|
||||
@ -76,7 +74,5 @@ function mapStateToProps(state) {
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
setProfile,
|
||||
fetchProfile,
|
||||
resetProfile
|
||||
fetchProfile
|
||||
})(Profile);
|
@ -17,7 +17,7 @@ const DepartmentField = React.memo((props) => {
|
||||
hasError={hasError}
|
||||
labelText={labelText}
|
||||
>
|
||||
{departments.map((department) => (
|
||||
{departments && departments.map((department) => (
|
||||
<SelectedItem
|
||||
key={`department_${department.id}`}
|
||||
text={department.name}
|
||||
|
@ -3,7 +3,7 @@ import { withRouter } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
import { Avatar, Button, Textarea, Text, toastr } from 'asc-web-components'
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { toEmployeeWrapper, getUserRole, profileEqual, createProfile } from '../../../../../store/profile/actions';
|
||||
import { toEmployeeWrapper, getUserRole, createProfile } from '../../../../../store/profile/actions';
|
||||
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
|
||||
import TextField from './FormFields/TextField'
|
||||
import PasswordField from './FormFields/PasswordField'
|
||||
@ -30,12 +30,14 @@ class CreateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!profileEqual(this.props.profile, prevProps.profile)) {
|
||||
if (this.props.match.params.type !== prevProps.match.params.type) {
|
||||
this.setState(this.mapPropsToState(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
mapPropsToState = (props) => {
|
||||
const isVisitor = props.match.params.type === "guest";
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
showPassword: false,
|
||||
@ -45,10 +47,7 @@ class CreateUserForm extends React.Component {
|
||||
email: false,
|
||||
password: false,
|
||||
},
|
||||
profile: {
|
||||
...{ passwordType: "link" },
|
||||
...toEmployeeWrapper(props.profile)
|
||||
}
|
||||
profile: toEmployeeWrapper({ isVisitor: isVisitor})
|
||||
};
|
||||
}
|
||||
|
||||
@ -99,9 +98,9 @@ class CreateUserForm extends React.Component {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
this.props.createProfile(this.state.profile)
|
||||
.then(() => {
|
||||
.then((profile) => {
|
||||
toastr.success("Success");
|
||||
this.props.history.goBack();
|
||||
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(error.message)
|
||||
@ -110,7 +109,7 @@ class CreateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this.props.history.goBack();
|
||||
this.props.history.push(this.props.settings.homepage)
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -236,7 +235,7 @@ class CreateUserForm extends React.Component {
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
profile: state.profile.targetUser
|
||||
settings: state.auth.settings
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { withRouter } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
import { Avatar, Button, Textarea, Text, toastr, ModalDialog } from 'asc-web-components'
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { toEmployeeWrapper, getUserRole, profileEqual, updateProfile } from '../../../../../store/profile/actions';
|
||||
import { toEmployeeWrapper, getUserRole, updateProfile } from '../../../../../store/profile/actions';
|
||||
import { MainContainer, AvatarContainer, MainFieldsContainer } from './FormFields/Form'
|
||||
import TextField from './FormFields/TextField'
|
||||
import TextChangeField from './FormFields/TextChangeField'
|
||||
@ -32,7 +32,7 @@ class UpdateUserForm extends React.Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!profileEqual(this.props.profile, prevProps.profile)) {
|
||||
if (this.props.match.params.userId !== prevProps.match.params.userId) {
|
||||
this.setState(this.mapPropsToState(this.props));
|
||||
}
|
||||
}
|
||||
@ -47,10 +47,7 @@ class UpdateUserForm extends React.Component {
|
||||
email: false,
|
||||
password: false,
|
||||
},
|
||||
profile: {
|
||||
...{ passwordType: "link" },
|
||||
...toEmployeeWrapper(props.profile)
|
||||
}
|
||||
profile: toEmployeeWrapper(props.profile)
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,9 +94,9 @@ class UpdateUserForm extends React.Component {
|
||||
this.setState({isLoading: true});
|
||||
|
||||
this.props.updateProfile(this.state.profile)
|
||||
.then(() => {
|
||||
.then((profile) => {
|
||||
toastr.success("Success");
|
||||
this.props.history.goBack();
|
||||
this.props.history.push(`${this.props.settings.homepage}/view/${profile.userName}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(error.message)
|
||||
@ -256,7 +253,8 @@ class UpdateUserForm extends React.Component {
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
profile: state.profile.targetUser
|
||||
profile: state.profile.targetUser,
|
||||
settings: state.auth.settings
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -15,14 +15,17 @@ const Header = styled(Text.ContentHeader)`
|
||||
`;
|
||||
|
||||
const SectionHeaderContent = (props) => {
|
||||
const {profile, history, settings} = props;
|
||||
const { profile, history, settings, match } = props;
|
||||
const { type } = match.params;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const headerText = profile && profile.displayName
|
||||
? profile.displayName
|
||||
: profile.isVisitor
|
||||
const headerText = type
|
||||
? type === "guest"
|
||||
? t('NewGuest')
|
||||
: t('NewEmployee');
|
||||
: t('NewEmployee')
|
||||
: profile
|
||||
? profile.displayName
|
||||
: "";
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
history.push(settings.homepage)
|
||||
|
@ -4,31 +4,27 @@ import PropTypes from "prop-types";
|
||||
import { PageLayout, Loader } from "asc-web-components";
|
||||
import { ArticleHeaderContent, ArticleMainButtonContent, ArticleBodyContent } from '../../Article';
|
||||
import { SectionHeaderContent, CreateUserForm, UpdateUserForm } from './Section';
|
||||
import { setProfile, fetchProfile, resetProfile } from '../../../store/profile/actions';
|
||||
import { fetchProfile } from '../../../store/profile/actions';
|
||||
import i18n from "./i18n";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
|
||||
class ProfileAction extends React.Component {
|
||||
componentDidMount() {
|
||||
const { match, setProfile, fetchProfile } = this.props;
|
||||
const { userId, type } = match.params;
|
||||
|
||||
if (!userId) {
|
||||
setProfile({ isVisitor: type === "guest" });
|
||||
} else {
|
||||
componentDidMount() {
|
||||
const { match, fetchProfile } = this.props;
|
||||
const { userId } = match.params;
|
||||
|
||||
if (userId) {
|
||||
fetchProfile(userId);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { match, setProfile, fetchProfile } = this.props;
|
||||
const { userId, type } = match.params;
|
||||
const { match, fetchProfile } = this.props;
|
||||
const { userId } = match.params;
|
||||
const prevUserId = prevProps.match.params.userId;
|
||||
const prevType = prevProps.match.params.type;
|
||||
|
||||
if (!userId && type !== prevType) {
|
||||
setProfile({ isVisitor: type === "guest" });
|
||||
} else if (userId !== prevUserId) {
|
||||
if (userId !== undefined && userId !== prevUserId) {
|
||||
fetchProfile(userId);
|
||||
}
|
||||
}
|
||||
@ -36,17 +32,17 @@ class ProfileAction extends React.Component {
|
||||
render() {
|
||||
console.log("ProfileAction render")
|
||||
|
||||
const { profile } = this.props;
|
||||
const { profile, match } = this.props;
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
{profile
|
||||
{profile || match.params.type
|
||||
? <PageLayout
|
||||
articleHeaderContent={<ArticleHeaderContent />}
|
||||
articleMainButtonContent={<ArticleMainButtonContent />}
|
||||
articleBodyContent={<ArticleBodyContent />}
|
||||
sectionHeaderContent={<SectionHeaderContent />}
|
||||
sectionBodyContent={profile.id ? <UpdateUserForm /> : <CreateUserForm />}
|
||||
sectionBodyContent={match.params.type ? <CreateUserForm /> : <UpdateUserForm />}
|
||||
/>
|
||||
: <PageLayout
|
||||
articleHeaderContent={<ArticleHeaderContent />}
|
||||
@ -62,9 +58,7 @@ class ProfileAction extends React.Component {
|
||||
ProfileAction.propTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
profile: PropTypes.object,
|
||||
setProfile: PropTypes.func.isRequired,
|
||||
fetchProfile: PropTypes.func.isRequired,
|
||||
resetProfile: PropTypes.func.isRequired
|
||||
fetchProfile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
@ -74,7 +68,5 @@ function mapStateToProps(state) {
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
setProfile,
|
||||
fetchProfile,
|
||||
resetProfile
|
||||
fetchProfile
|
||||
})(ProfileAction);
|
@ -33,32 +33,6 @@ export function getUserRole(profile) {
|
||||
return "user";
|
||||
};
|
||||
|
||||
export function profileEqual(profileA, profileB) {
|
||||
const keys = Object.keys(profileA);
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
|
||||
if (key === "groups") {
|
||||
if (profileA[key].length !== profileB[key].length)
|
||||
return false;
|
||||
|
||||
const groupsA = profileA[key].map(group => group.id);
|
||||
const groupsB = profileA[key].map(group => group.id);
|
||||
|
||||
for (let j = 0; j < groupsA.length; j++) {
|
||||
if (!groupsB.includes(groupsA[j]))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if(profileA[key] !== profileB[key])
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function toEmployeeWrapper(profile) {
|
||||
const emptyData = {
|
||||
id: "",
|
||||
@ -68,6 +42,7 @@ export function toEmployeeWrapper(profile) {
|
||||
password: "",
|
||||
birthday: "",
|
||||
sex: "male",
|
||||
passwordType: "link",
|
||||
workFrom: "",
|
||||
location: "",
|
||||
title: "",
|
||||
@ -111,12 +86,16 @@ export function createProfile(profile) {
|
||||
const {people} = getState();
|
||||
const {filter} = people;
|
||||
const member = employeeWrapperToMemberModel(profile);
|
||||
let result;
|
||||
|
||||
return api.createUser(member).then(res => {
|
||||
checkResponseError(res);
|
||||
return Promise.resolve(dispatch(setProfile(res.data.response)));
|
||||
result = res.data.response;
|
||||
return dispatch(setProfile(result));
|
||||
}).then(() => {
|
||||
return fetchPeopleByFilter(dispatch, filter);
|
||||
}).then(() => {
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
};
|
||||
};
|
||||
@ -126,12 +105,16 @@ export function updateProfile(profile) {
|
||||
const {people} = getState();
|
||||
const {filter} = people;
|
||||
const member = employeeWrapperToMemberModel(profile);
|
||||
let result;
|
||||
|
||||
return api.updateUser(member).then(res => {
|
||||
checkResponseError(res);
|
||||
return Promise.resolve(dispatch(setProfile(res.data.response)));
|
||||
result = res.data.response;
|
||||
return Promise.resolve(dispatch(setProfile(result)));
|
||||
}).then(() => {
|
||||
return fetchPeopleByFilter(dispatch, filter);
|
||||
}).then(() => {
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
};
|
||||
};
|
@ -86,3 +86,9 @@ export function updateUserType(type, userIds) {
|
||||
? fakeApi.updateUserType(type, userIds)
|
||||
: axios.put(`${API_URL}/people/type/${type}`, { userIds });
|
||||
}
|
||||
|
||||
export function resendUserInvites(userIds) {
|
||||
return IS_FAKE
|
||||
? fakeApi.resendUserInvites(userIds)
|
||||
: axios.put(`${API_URL}/people/invite`, { userIds });
|
||||
}
|
@ -359,3 +359,50 @@ export function updateUserType(type, userIds) {
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
export function resendUserInvites(userIds) {
|
||||
return fakeResponse([
|
||||
{
|
||||
id: userIds[0],
|
||||
userName: "Mike.Zanyatski",
|
||||
isVisitor: false,
|
||||
firstName: "Mike",
|
||||
lastName: "Zanyatski",
|
||||
email: "my@gmail.com",
|
||||
birthday: "2019-08-19T01:39:25.3240031Z",
|
||||
sex: "male",
|
||||
status: 1,
|
||||
activationStatus: 0,
|
||||
terminated: "2019-08-19T01:39:25.3240031Z",
|
||||
department: "Marketing",
|
||||
workFrom: "2019-08-19T01:39:25.3240031Z",
|
||||
location: "Palo Alto",
|
||||
notes: "Notes to worker",
|
||||
displayName: null,
|
||||
title: "Manager",
|
||||
contacts: [
|
||||
{
|
||||
type: "GTalk",
|
||||
value: "my@gmail.com"
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
name: "Group Name",
|
||||
manager: "Jake.Zazhitski"
|
||||
}
|
||||
],
|
||||
avatarMedium: "url to medium avatar",
|
||||
avatar: "url to big avatar",
|
||||
isAdmin: false,
|
||||
isLDAP: false,
|
||||
listAdminModules: ["projects", "crm"],
|
||||
isOwner: false,
|
||||
cultureName: "en-EN",
|
||||
isSSO: false,
|
||||
avatarSmall: "url to small avatar",
|
||||
profileUrl: ""
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
@ -1774,7 +1774,7 @@ asap@~2.0.3, asap@~2.0.6:
|
||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
||||
|
||||
"asc-web-components@file:../../../packages/asc-web-components":
|
||||
version "1.0.27"
|
||||
version "1.0.29"
|
||||
dependencies:
|
||||
"@emotion/core" "10.0.16"
|
||||
prop-types "^15.7.2"
|
||||
@ -8876,6 +8876,11 @@ react-dev-utils@^9.0.3:
|
||||
strip-ansi "5.2.0"
|
||||
text-table "0.2.0"
|
||||
|
||||
react-device-detect@^1.7.5:
|
||||
version "1.7.5"
|
||||
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-1.7.5.tgz#04c8a6475d67b5ac4f984c8d912ec11134f5b893"
|
||||
integrity sha512-ccgJuTHVCI3yfqvgU56gQDvjueFyaHVAXsa5rJuzWRasvsd0IalzfyccSECIlygjv3E+DmAGcwNYWUarUA82Fw==
|
||||
|
||||
react-dom@^16.9.0:
|
||||
version "16.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.9.0.tgz#5e65527a5e26f22ae3701131bcccaee9fb0d3962"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "asc-web-components",
|
||||
"version": "1.0.29",
|
||||
"version": "1.0.30",
|
||||
"description": "Ascensio System SIA component library",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "dist/asc-web-components.cjs.js",
|
||||
|
@ -43,7 +43,7 @@ const StyledComboBox = styled.div`
|
||||
|
||||
:hover{
|
||||
border-color: ${state => state.isOpen ? '#2DA7DB' : '#A3A9AE' };
|
||||
cursor: ${props => (props.isDisabled || !props.options.length) ? 'default' : 'pointer'};
|
||||
cursor: ${props => (props.isDisabled || !props.options.length ) ? (props.advancedOptions) ? 'pointer' : 'default' : 'pointer'};
|
||||
|
||||
${props => props.isDisabled && `
|
||||
border-color: #ECEEF1;
|
||||
@ -57,7 +57,6 @@ const StyledComboButton = styled.div`
|
||||
justify-content: center;
|
||||
|
||||
height: ${props => props.noBorder ? `18px` : `30px`};
|
||||
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
@ -68,7 +67,7 @@ const StyledIcon = styled.div`
|
||||
`;
|
||||
|
||||
const StyledOptionalItem = styled.div`
|
||||
margin-right: 8px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
@ -99,8 +98,8 @@ const StyledArrowIcon = styled.div`
|
||||
width: 8px;
|
||||
flex: 0 0 8px;
|
||||
margin-top: ${props => props.noBorder ? `5px` : `12px`};
|
||||
margin-right: ${props => props.options.length ? '8px' : '0px'};
|
||||
margin-left: ${props => props.options.length ? 'auto' : '0px'};
|
||||
margin-right: ${props => props.needDisplay ? '8px' : '0px'};
|
||||
margin-left: ${props => props.needDisplay ? 'auto' : '0px'};
|
||||
|
||||
${props => props.isOpen && `
|
||||
transform: scale(1, -1);
|
||||
@ -132,7 +131,7 @@ class ComboBox extends React.PureComponent {
|
||||
toggle = (isOpen) => this.setState({ isOpen: isOpen });
|
||||
|
||||
comboBoxClick = (e) => {
|
||||
if (this.props.isDisabled || !this.props.options.length || e.target.closest('.optionalBlock')) return;
|
||||
if (this.props.isDisabled || e.target.closest('.optionalBlock')) return;
|
||||
this.toggle(!this.state.isOpen);
|
||||
};
|
||||
|
||||
@ -165,8 +164,17 @@ class ComboBox extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
//console.log("ComboBox render");
|
||||
|
||||
const { dropDownMaxHeight, isDisabled, directionX, directionY, scaled, children, options, noBorder } = this.props;
|
||||
const {
|
||||
dropDownMaxHeight,
|
||||
isDisabled,
|
||||
directionX,
|
||||
directionY,
|
||||
scaled,
|
||||
children,
|
||||
options,
|
||||
noBorder,
|
||||
advancedOptions
|
||||
} = this.props;
|
||||
const { isOpen, selectedOption } = this.state;
|
||||
|
||||
const dropDownMaxHeightProp = dropDownMaxHeight ? { maxHeight: dropDownMaxHeight } : {};
|
||||
@ -183,9 +191,11 @@ class ComboBox extends React.PureComponent {
|
||||
onSelect={this.stopAction}
|
||||
>
|
||||
<StyledComboButton noBorder={noBorder}>
|
||||
<StyledOptionalItem className='optionalBlock'>
|
||||
{children}
|
||||
</StyledOptionalItem>
|
||||
{children &&
|
||||
<StyledOptionalItem className='optionalBlock'>
|
||||
{children}
|
||||
</StyledOptionalItem>
|
||||
}
|
||||
{selectedOption && selectedOption.icon &&
|
||||
<StyledIcon>
|
||||
{React.createElement(Icons[selectedOption.icon],
|
||||
@ -200,9 +210,8 @@ class ComboBox extends React.PureComponent {
|
||||
<StyledLabel noBorder={noBorder}>
|
||||
{selectedOption.label}
|
||||
</StyledLabel>
|
||||
|
||||
<StyledArrowIcon options={options} noBorder={noBorder} isOpen={isOpen}>
|
||||
{options.length > 0 &&
|
||||
<StyledArrowIcon needDisplay={options.length > 0 || advancedOptions !== undefined} noBorder={noBorder} isOpen={isOpen}>
|
||||
{(options.length > 0 || advancedOptions !== undefined) &&
|
||||
React.createElement(Icons['ExpanderDownIcon'],
|
||||
{
|
||||
size: 'scale',
|
||||
@ -211,7 +220,6 @@ class ComboBox extends React.PureComponent {
|
||||
})
|
||||
}
|
||||
</StyledArrowIcon>
|
||||
|
||||
</StyledComboButton>
|
||||
<DropDown
|
||||
directionX={directionX}
|
||||
@ -221,7 +229,9 @@ class ComboBox extends React.PureComponent {
|
||||
{...dropDownMaxHeightProp}
|
||||
{...dropDownManualWidthProp}
|
||||
>
|
||||
{options.map((option) =>
|
||||
{advancedOptions
|
||||
? advancedOptions
|
||||
: options.map((option) =>
|
||||
<DropDownItem {...option}
|
||||
disabled={option.disabled || (option.label === selectedOption.label)}
|
||||
onClick={this.optionClick.bind(this, option)}
|
||||
@ -237,12 +247,14 @@ ComboBox.propTypes = {
|
||||
noBorder: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
selectedOption: PropTypes.object.isRequired,
|
||||
|
||||
options: PropTypes.array.isRequired,
|
||||
advancedOptions: PropTypes.element,
|
||||
|
||||
onSelect: PropTypes.func,
|
||||
dropDownMaxHeight: PropTypes.number,
|
||||
|
||||
size: PropTypes.oneOf(['base', 'middle', 'big', 'huge', 'content']),
|
||||
scaled: PropTypes.bool,
|
||||
scaled: PropTypes.bool
|
||||
}
|
||||
|
||||
ComboBox.defaultProps = {
|
||||
|
@ -14,7 +14,7 @@ const fontStyle = css`
|
||||
font-style: normal;
|
||||
`;
|
||||
|
||||
const StyledDropdownItem = styled.button`
|
||||
const StyledDropdownItem = styled.div`
|
||||
width: ${props => (props.isSeparator ? 'calc(100% - 32px)' : '100%')};
|
||||
height: ${props => (props.isSeparator && '1px')};
|
||||
line-height: ${props => (props.isSeparator ? '1px' : '36px')};
|
||||
@ -80,7 +80,7 @@ const IconWrapper = styled.span`
|
||||
|
||||
const DropDownItem = props => {
|
||||
//console.log("DropDownItem render");
|
||||
const { isSeparator, label, icon } = props;
|
||||
const { isSeparator, label, icon, children } = props;
|
||||
const color = props.disabled ? '#A3A9AE' : '#333333';
|
||||
|
||||
return (
|
||||
@ -90,7 +90,7 @@ const DropDownItem = props => {
|
||||
{React.createElement(Icons[icon], {size: "scale", color: color, isfill: true})}
|
||||
</IconWrapper>
|
||||
}
|
||||
{isSeparator ? '\u00A0' : label}
|
||||
{isSeparator ? '\u00A0' : label ? label : children && children}
|
||||
</StyledDropdownItem>
|
||||
);
|
||||
};
|
||||
|
@ -5,9 +5,57 @@
|
||||
Custom combo box input
|
||||
|
||||
Options have options:
|
||||
key - Item key, may be a string or a number,
|
||||
label - Display text,
|
||||
icon - Optional name of icon that will be displayed before label
|
||||
|
||||
- key - Item key, may be a string or a number
|
||||
- label - Display text
|
||||
- icon - Optional name of icon that will be displayed before label
|
||||
- disabled - Make option disabled
|
||||
- onClick - On click function
|
||||
|
||||
ComboBox perceives all property`s for positioning from DropDown!
|
||||
|
||||
If you need to display a custom list of options, you must use advancedOptions property. Like this:
|
||||
|
||||
```js
|
||||
const advancedOptions = (
|
||||
<>
|
||||
<DropDownItem>
|
||||
<RadioButton value="asc" name="first" label="A-Z" isChecked={true} />
|
||||
</DropDownItem>
|
||||
<DropDownItem>
|
||||
<RadioButton value="desc" name="first" label="Z-A" />
|
||||
</DropDownItem>
|
||||
<DropDownItem isSeparator />
|
||||
<DropDownItem>
|
||||
<RadioButton value="first" name="second" label="First name" />
|
||||
</DropDownItem>
|
||||
<DropDownItem>
|
||||
<RadioButton
|
||||
value="last"
|
||||
name="second"
|
||||
label="Last name"
|
||||
isChecked={true}
|
||||
/>
|
||||
</DropDownItem>
|
||||
</>
|
||||
);
|
||||
|
||||
<ComboBox
|
||||
options={[]} // An empty array will enable advancedOptions
|
||||
advancedOptions={advancedOptions}
|
||||
onSelect={option => action("Selected option")(option)}
|
||||
selectedOption={{
|
||||
key: 0,
|
||||
label: "Select"
|
||||
}}
|
||||
isDisabled={false}
|
||||
scaled={false}
|
||||
size="content"
|
||||
directionX="right"
|
||||
>
|
||||
<Icons.NavLogoIcon size="medium" key="comboIcon" />
|
||||
</ComboBox>;
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
@ -17,38 +65,16 @@ import { ComboBox } from 'asc-web-components';
|
||||
const options = [
|
||||
{
|
||||
key: 1,
|
||||
icon: 'CatalogEmployeeIcon',
|
||||
label: 'Option 1'
|
||||
icon: 'CatalogEmployeeIcon', // optional item
|
||||
label: 'Option 1',
|
||||
disabled: false, // optional item
|
||||
onClick: clickFunction // optional item
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
icon: 'CatalogGuestIcon',
|
||||
label: 'Option 2',
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
label: 'Option 3'
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
label: 'Option 4'
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
icon: 'CopyIcon',
|
||||
label: 'Option 5'
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
label: 'Option 6'
|
||||
},
|
||||
{
|
||||
key: 7,
|
||||
label: 'Option 7'
|
||||
}
|
||||
...
|
||||
];
|
||||
|
||||
<ComboBox options={options}
|
||||
<ComboBox
|
||||
options={options}
|
||||
isDisabled={false}
|
||||
selectedOption={{
|
||||
key: 0,
|
||||
@ -64,13 +90,14 @@ const options = [
|
||||
|
||||
#### Properties
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ---------------------- | ----------------- | :------: | ---------------------------- | ------- | -------------------------------------------- |
|
||||
| `options` | `array` | ✅ | - | - | Combo box options |
|
||||
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
|
||||
| `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
|
||||
| `selectedOption` | `object` | ✅ | - | - | Selected option |
|
||||
| `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option |
|
||||
| `dropDownMaxHeight` | `number` | - | - | - | Height of Dropdown |
|
||||
| `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent |
|
||||
| `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default |
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------------- | --------- | :------: | ------------------------------------------ | ------- | --------------------------------------------------------- |
|
||||
| `options` | `array` | ✅ | - | - | Combo box options |
|
||||
| `isDisabled` | `bool` | - | - | `false` | Indicates that component is disabled |
|
||||
| `noBorder` | `bool` | - | - | `false` | Indicates that component is displayed without borders |
|
||||
| `selectedOption` | `object` | ✅ | - | - | Selected option |
|
||||
| `onSelect` | `func` | - | - | - | Will be triggered whenever an ComboBox is selected option |
|
||||
| `dropDownMaxHeight` | `number` | - | - | - | Height of Dropdown |
|
||||
| `scaled` | `bool` | - | - | `true` | Indicates that component is scaled by parent |
|
||||
| `size` | `oneOf` | - | `base`, `middle`, `big`, `huge`, `content` | `base` | Select component width, one of default |
|
||||
| `advancedOptions` | `element` | - | - | - | If you need display options not basic options |
|
||||
|
@ -5,8 +5,9 @@ import { withKnobs, boolean, select, number } from '@storybook/addon-knobs/react
|
||||
import { optionsKnob as options } from '@storybook/addon-knobs';
|
||||
import withReadme from 'storybook-readme/with-readme';
|
||||
import Readme from './README.md';
|
||||
import { ComboBox, Icons, Button } from 'asc-web-components'
|
||||
import { ComboBox, Icons, Button, RadioButton, DropDownItem } from 'asc-web-components'
|
||||
import Section from '../../../.storybook/decorators/section';
|
||||
import styled from 'styled-components'
|
||||
|
||||
const iconNames = Object.keys(Icons);
|
||||
const sizeOptions = ['base', 'middle', 'big', 'huge', 'content'];
|
||||
@ -79,23 +80,74 @@ storiesOf('Components|Input', module)
|
||||
}
|
||||
});
|
||||
|
||||
const advancedOptions =
|
||||
<>
|
||||
<DropDownItem>
|
||||
<RadioButton value='asc' name='first' label='A-Z' isChecked={true} />
|
||||
</DropDownItem>
|
||||
<DropDownItem >
|
||||
<RadioButton value='desc' name='first' label='Z-A' />
|
||||
</DropDownItem>
|
||||
<DropDownItem isSeparator />
|
||||
<DropDownItem>
|
||||
<RadioButton value='first' name='second' label='First name' />
|
||||
</DropDownItem>
|
||||
<DropDownItem>
|
||||
<RadioButton value='last' name='second' label='Last name' isChecked={true} />
|
||||
</DropDownItem>
|
||||
</>;
|
||||
|
||||
const childrenItems = children.length > 0 ? children : null;
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<ComboBox
|
||||
options={comboOptions}
|
||||
onSelect={option => action("Selected option")(option)}
|
||||
selectedOption={{
|
||||
key: 0,
|
||||
label: 'Select'
|
||||
}}
|
||||
isDisabled={boolean('isDisabled', false)}
|
||||
noBorder={boolean('noBorder', false)}
|
||||
dropDownMaxHeight={dropDownMaxHeight}
|
||||
scaled={boolean('scaled', false)}
|
||||
size={select('size', sizeOptions, 'content')}
|
||||
>
|
||||
{children}
|
||||
</ComboBox>
|
||||
<table style={{ width: 584, borderCollapse: "separate" }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Default</th>
|
||||
<th>Advanced</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ paddingBottom: 20 }}>
|
||||
<ComboBox
|
||||
options={comboOptions}
|
||||
onSelect={option => action("Selected option")(option)}
|
||||
selectedOption={{
|
||||
key: 0,
|
||||
label: 'Select'
|
||||
}}
|
||||
isDisabled={boolean('isDisabled', false)}
|
||||
noBorder={boolean('noBorder', false)}
|
||||
dropDownMaxHeight={dropDownMaxHeight}
|
||||
scaled={boolean('scaled', false)}
|
||||
size={select('size', sizeOptions, 'content')}
|
||||
>
|
||||
{childrenItems}
|
||||
</ComboBox>
|
||||
</td>
|
||||
<td style={{ paddingBottom: 20 }}>
|
||||
<ComboBox
|
||||
options={[]}
|
||||
advancedOptions={advancedOptions}
|
||||
onSelect={option => action("Selected option")(option)}
|
||||
selectedOption={{
|
||||
key: 0,
|
||||
label: 'Select'
|
||||
}}
|
||||
isDisabled={boolean('isDisabled', false)}
|
||||
scaled={false}
|
||||
size='content'
|
||||
directionX='right'
|
||||
>
|
||||
<Icons.NavLogoIcon size="medium" key='comboIcon' />
|
||||
</ComboBox>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
);
|
||||
});
|
@ -27,7 +27,26 @@ or
|
||||
{toastr.success('Some text for toast')}
|
||||
</Toast>
|
||||
```
|
||||
You can use simple html tags. For this action you should wrap your message by empty tags:
|
||||
```js
|
||||
|
||||
<Toast />
|
||||
<button onClick={() => toastr.success(<>You have <b>bold text</b></>)}>Click</button>
|
||||
```
|
||||
|
||||
If your notification include only text in html tags or data in JSX tags, you can omit empty tags:
|
||||
```js
|
||||
|
||||
<Toast />
|
||||
<button onClick={() => toastr.success(<b>Bold text</b>)}>Click</button>
|
||||
```
|
||||
|
||||
```js
|
||||
import { Text } from 'asc-web-components';
|
||||
|
||||
<Toast />
|
||||
<button onClick={() => toastr.success(<Text.Body>The email activation instructions have been sent to the <b>{user.email}</b> email address</Text.Body>))}>Click</button>
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user