Merge branch 'feature/virtual-rooms-1.2' of github.com:ONLYOFFICE/AppServer into feature/virtual-rooms-1.2
@ -380,10 +380,14 @@ export function getTfaConfirmLink() {
|
||||
});
|
||||
}
|
||||
|
||||
export function unlinkTfaApp() {
|
||||
export function unlinkTfaApp(id) {
|
||||
const data = {
|
||||
id,
|
||||
};
|
||||
return request({
|
||||
method: "put",
|
||||
url: "/settings/tfaappnewapp",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@ -406,6 +410,7 @@ export function validateTfaCode(code) {
|
||||
return request({
|
||||
method: "post",
|
||||
url: "/settings/tfaapp/validate",
|
||||
skipLogout: true,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ export function loginWithTfaCode(userName, passwordHash, code) {
|
||||
return request({
|
||||
method: "post",
|
||||
url: `/authentication/${code}`,
|
||||
skipLogout: true,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
@ -20,6 +20,43 @@ const StyledArticle = styled.article`
|
||||
overflow: hidden;
|
||||
background: ${(props) => props.theme.catalog.background};
|
||||
|
||||
min-width: 256px;
|
||||
max-width: 256px;
|
||||
|
||||
@media ${tablet} {
|
||||
min-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
max-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
min-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
max-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
display: ${(props) => (props.articleOpen ? "flex" : "none")};
|
||||
min-width: 100vw;
|
||||
width: 100vw;
|
||||
height: calc(100vh - 64px) !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
display: ${(props) => (props.articleOpen ? "flex" : "none")} !important;
|
||||
min-width: 100vw !important;
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
margin-top: 64px !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-bottom: 0px;
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
position: fixed;
|
||||
margin-top: 16px;
|
||||
@ -27,13 +64,6 @@ const StyledArticle = styled.article`
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
position: fixed;
|
||||
margin-top: 64px !important;
|
||||
height: calc(100vh - 64px) !important;
|
||||
`}
|
||||
|
||||
z-index: ${(props) =>
|
||||
props.showText && (isMobileOnly || isMobileUtils()) ? "205" : "100"};
|
||||
|
||||
@ -75,7 +105,7 @@ const StyledArticle = styled.article`
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
${isTablet &&
|
||||
${isMobile &&
|
||||
css`
|
||||
min-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
max-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
|
@ -1,5 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
import RectangleLoader from "../RectangleLoader";
|
||||
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { tablet, mobile } from "@appserver/components/utils/device";
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -13,6 +15,13 @@ const StyledContainer = styled.div`
|
||||
padding: ${(props) => (props.showText ? "0 16px" : "10px 16px")};
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
max-width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
width: ${(props) => (props.showText ? "240px" : "52px")};
|
||||
padding: ${(props) => (props.showText ? "0 16px" : "10px 16px")};
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
@ -42,6 +51,13 @@ const StyledRectangleLoader = styled(RectangleLoader)`
|
||||
width: 20px;
|
||||
padding: 0 0 24px;
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
padding: 0 0 24px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export { StyledBlock, StyledContainer, StyledRectangleLoader };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import styled from "styled-components";
|
||||
import { mobile } from "@appserver/components/utils/device";
|
||||
import styled, { css } from "styled-components";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { mobile, tablet } from "@appserver/components/utils/device";
|
||||
|
||||
const StyledFilter = styled.div`
|
||||
width: 100%;
|
||||
@ -8,6 +9,11 @@ const StyledFilter = styled.div`
|
||||
grid-template-rows: 1fr;
|
||||
grid-column-gap: 8px;
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
margin-top: -22px;
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
grid-template-columns: 1fr 50px;
|
||||
}
|
||||
|
@ -96,7 +96,6 @@ const Navigation = ({
|
||||
onBackToParentFolder={onBackToParentFolderAction}
|
||||
title={title}
|
||||
personal={personal}
|
||||
isRootFolder={isRootFolder}
|
||||
canCreate={canCreate}
|
||||
navigationItems={navigationItems}
|
||||
getContextOptionsFolder={getContextOptionsFolder}
|
||||
|
@ -23,10 +23,9 @@ const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
padding: ${(props) => (props.isDropBox ? "16px 0 5px" : "16px 0 0px")};
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
width: 100% !important;
|
||||
width: 100%;
|
||||
padding: ${(props) =>
|
||||
props.isDropBox ? "16px 0 5px" : " 16px 0 0px"} !important;
|
||||
`}
|
||||
@ -40,7 +39,7 @@ const StyledContainer = styled.div`
|
||||
css`
|
||||
width: 100% !important;
|
||||
padding: ${(props) =>
|
||||
props.isDropBox ? "12px 0 5px" : "12px 0 0"} !important;
|
||||
props.isDropBox ? "18px 0 5px" : "18px 0 0"} !important;
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -26,7 +26,7 @@ const StyledBox = styled.div`
|
||||
top: 0px;
|
||||
left: ${isMobile ? "-16px" : "-20px"};
|
||||
|
||||
padding: ${isMobile ? "0 12px 0 16px" : "0 20px"};
|
||||
padding: ${isMobile ? "0 16px 0 16px" : "0 20px"};
|
||||
|
||||
width: ${(props) => props.dropBoxWidth}px;
|
||||
|
||||
@ -44,7 +44,7 @@ const StyledBox = styled.div`
|
||||
|
||||
@media ${tablet} {
|
||||
left: -16px;
|
||||
padding: 0 12px 0 16px;
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
desktop,
|
||||
size,
|
||||
tablet,
|
||||
mobile,
|
||||
isMobile as isMobileUtils,
|
||||
isTablet as isTabletUtils,
|
||||
} from "@appserver/components/utils/device";
|
||||
@ -16,6 +17,7 @@ import SubSectionHeader from "./sub-components/section-header";
|
||||
import SubSectionFilter from "./sub-components/section-filter";
|
||||
import SubSectionBody from "./sub-components/section-body";
|
||||
import SubSectionBodyContent from "./sub-components/section-body-content";
|
||||
import SubSectionBar from "./sub-components/section-bar";
|
||||
import SubSectionPaging from "./sub-components/section-paging";
|
||||
|
||||
import ReactResizeDetector from "react-resize-detector";
|
||||
@ -33,20 +35,66 @@ const StyledSelectoWrapper = styled.div`
|
||||
const StyledMainBar = styled.div`
|
||||
box-sizing: border-box;
|
||||
|
||||
${!isMobile
|
||||
? css`
|
||||
padding-right: 20px;
|
||||
@media ${tablet} {
|
||||
padding-right: 16px;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
padding-right: 0px;
|
||||
margin-left: -20px;
|
||||
width: calc(100vw - 256px);
|
||||
max-width: calc(100vw - 256px);
|
||||
|
||||
@media ${desktop} {
|
||||
padding-right: 10px;
|
||||
}
|
||||
#bar-banner {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
|
||||
#bar-frame {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media ${tablet} {
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
|
||||
margin-left: -16px;
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
|
||||
margin-left: -16px;
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
|
||||
#bar-frame {
|
||||
min-width: 100vw;
|
||||
}
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
!props.isSectionHeaderAvailable &&
|
||||
css`
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
position: fixed;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
margin-left: 0 !important;
|
||||
box-sizing: border-box;
|
||||
`}
|
||||
`}
|
||||
`;
|
||||
|
||||
function SectionHeader() {
|
||||
@ -54,6 +102,12 @@ function SectionHeader() {
|
||||
}
|
||||
SectionHeader.displayName = "SectionHeader";
|
||||
|
||||
function SectionBar() {
|
||||
return null;
|
||||
}
|
||||
|
||||
SectionBar.displayName = "SectionBar";
|
||||
|
||||
function SectionFilter() {
|
||||
return null;
|
||||
}
|
||||
@ -73,6 +127,7 @@ class Section extends React.Component {
|
||||
static SectionHeader = SectionHeader;
|
||||
static SectionFilter = SectionFilter;
|
||||
static SectionBody = SectionBody;
|
||||
static SectionBar = SectionBar;
|
||||
static SectionPaging = SectionPaging;
|
||||
|
||||
constructor(props) {
|
||||
@ -124,7 +179,6 @@ class Section extends React.Component {
|
||||
onScroll = (e) => {
|
||||
this.scroll.scrollBy(e.direction[0] * 10, e.direction[1] * 10);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
onDrop,
|
||||
@ -150,9 +204,14 @@ class Section extends React.Component {
|
||||
isBackdropVisible,
|
||||
isDesktop,
|
||||
isHomepage,
|
||||
maintenanceExist,
|
||||
setMaintenanceExist,
|
||||
snackbarExist,
|
||||
showText,
|
||||
} = this.props;
|
||||
|
||||
let sectionHeaderContent = null;
|
||||
let sectionBarContent = null;
|
||||
let sectionFilterContent = null;
|
||||
let sectionPagingContent = null;
|
||||
let sectionBodyContent = null;
|
||||
@ -167,6 +226,9 @@ class Section extends React.Component {
|
||||
case SectionFilter.displayName:
|
||||
sectionFilterContent = child;
|
||||
break;
|
||||
case SectionBar.displayName:
|
||||
sectionBarContent = child;
|
||||
break;
|
||||
case SectionPaging.displayName:
|
||||
sectionPagingContent = child;
|
||||
break;
|
||||
@ -185,6 +247,7 @@ class Section extends React.Component {
|
||||
!!sectionBodyContent ||
|
||||
isSectionFilterAvailable ||
|
||||
isSectionPagingAvailable,
|
||||
isSectionBarAvailable = !!sectionBarContent,
|
||||
isSectionAvailable =
|
||||
isSectionHeaderAvailable ||
|
||||
isSectionFilterAvailable ||
|
||||
@ -207,22 +270,48 @@ class Section extends React.Component {
|
||||
sectionHeight: height,
|
||||
}}
|
||||
>
|
||||
<SectionContainer widthProp={width} viewAs={viewAs}>
|
||||
{isSectionHeaderAvailable && (
|
||||
<SectionContainer
|
||||
widthProp={width}
|
||||
showText={showText}
|
||||
viewAs={viewAs}
|
||||
maintenanceExist={maintenanceExist}
|
||||
isSectionBarAvailable={isSectionBarAvailable}
|
||||
isSectionHeaderAvailable={isSectionHeaderAvailable}
|
||||
>
|
||||
{!isMobile && (
|
||||
<StyledMainBar
|
||||
width={width}
|
||||
id="main-bar"
|
||||
className={"main-bar"}
|
||||
showText={showText}
|
||||
isSectionHeaderAvailable={isSectionHeaderAvailable}
|
||||
>
|
||||
<SubSectionBar
|
||||
setMaintenanceExist={setMaintenanceExist}
|
||||
>
|
||||
{sectionBarContent
|
||||
? sectionBarContent.props.children
|
||||
: null}
|
||||
</SubSectionBar>
|
||||
</StyledMainBar>
|
||||
)}
|
||||
|
||||
{isSectionHeaderAvailable && !isMobile && (
|
||||
<SubSectionHeader
|
||||
maintenanceExist={maintenanceExist}
|
||||
snackbarExist={snackbarExist}
|
||||
className="section-header_header"
|
||||
isHeaderVisible={isHeaderVisible}
|
||||
viewAs={viewAs}
|
||||
showText={showText}
|
||||
>
|
||||
{sectionHeaderContent
|
||||
? sectionHeaderContent.props.children
|
||||
: null}
|
||||
</SubSectionHeader>
|
||||
)}
|
||||
|
||||
{isSectionFilterAvailable && (
|
||||
{isSectionFilterAvailable && !isMobile && (
|
||||
<>
|
||||
<StyledMainBar id="main-bar" />
|
||||
<SubSectionFilter
|
||||
className="section-header_filter"
|
||||
viewAs={viewAs}
|
||||
@ -243,11 +332,32 @@ class Section extends React.Component {
|
||||
viewAs={viewAs}
|
||||
isHomepage={isHomepage}
|
||||
>
|
||||
{isSectionHeaderAvailable && (
|
||||
{isMobile && (
|
||||
<StyledMainBar
|
||||
width={width}
|
||||
id="main-bar"
|
||||
className={"main-bar"}
|
||||
showText={showText}
|
||||
isSectionHeaderAvailable={
|
||||
isSectionHeaderAvailable
|
||||
}
|
||||
>
|
||||
<SubSectionBar
|
||||
setMaintenanceExist={setMaintenanceExist}
|
||||
>
|
||||
{sectionBarContent
|
||||
? sectionBarContent.props.children
|
||||
: null}
|
||||
</SubSectionBar>
|
||||
</StyledMainBar>
|
||||
)}
|
||||
|
||||
{isSectionHeaderAvailable && isMobile && (
|
||||
<SubSectionHeader
|
||||
className="section-body_header"
|
||||
isHeaderVisible={isHeaderVisible}
|
||||
viewAs={viewAs}
|
||||
showText={showText}
|
||||
>
|
||||
{sectionHeaderContent
|
||||
? sectionHeaderContent.props.children
|
||||
@ -255,7 +365,7 @@ class Section extends React.Component {
|
||||
</SubSectionHeader>
|
||||
)}
|
||||
|
||||
{isSectionFilterAvailable && (
|
||||
{isSectionFilterAvailable && isMobile && (
|
||||
<SubSectionFilter className="section-body_filter">
|
||||
{sectionFilterContent
|
||||
? sectionFilterContent.props.children
|
||||
@ -406,6 +516,11 @@ export default inject(({ auth }) => {
|
||||
|
||||
setIsBackdropVisible,
|
||||
isDesktopClient,
|
||||
maintenanceExist,
|
||||
snackbarExist,
|
||||
setMaintenanceExist,
|
||||
|
||||
showText,
|
||||
} = settingsStore;
|
||||
|
||||
return {
|
||||
@ -415,6 +530,11 @@ export default inject(({ auth }) => {
|
||||
|
||||
isBackdropVisible,
|
||||
setIsBackdropVisible,
|
||||
maintenanceExist,
|
||||
snackbarExist,
|
||||
setMaintenanceExist,
|
||||
isDesktop: isDesktopClient,
|
||||
|
||||
showText,
|
||||
};
|
||||
})(observer(Section));
|
||||
|
@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
import equal from "fast-deep-equal/react";
|
||||
|
||||
class SectionBar extends React.Component {
|
||||
componentWillUnmount() {
|
||||
this.props.setMaintenanceExist && this.props.setMaintenanceExist(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
SectionBar.displayName = "SectionBar";
|
||||
|
||||
export default SectionBar;
|
@ -13,10 +13,10 @@ import { tablet, mobile, desktop } from "@appserver/components/utils/device";
|
||||
const paddingStyles = css`
|
||||
padding: 19px 7px 16px 20px;
|
||||
@media ${tablet} {
|
||||
padding: 0px 0 16px 24px;
|
||||
padding: 19px 0 16px 24px;
|
||||
}
|
||||
@media ${mobile} {
|
||||
padding: 0px 0 16px 24px;
|
||||
padding: 19px 0 16px 24px;
|
||||
}
|
||||
${isMobile &&
|
||||
css`
|
||||
@ -34,6 +34,8 @@ const commonStyles = css`
|
||||
|
||||
${(props) => !props.withScroll && `height: 100%;`}
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
|
||||
.section-wrapper {
|
||||
${(props) =>
|
||||
@ -45,7 +47,6 @@ const commonStyles = css`
|
||||
.section-wrapper-content {
|
||||
${paddingStyles}
|
||||
flex: 1 0 auto;
|
||||
padding-right: 0;
|
||||
outline: none;
|
||||
${(props) =>
|
||||
props.viewAs == "tile" &&
|
||||
@ -76,12 +77,18 @@ const StyledSectionBody = styled.div`
|
||||
${(props) =>
|
||||
props.withScroll &&
|
||||
`
|
||||
margin-top: -1px;
|
||||
margin-left: -20px;
|
||||
|
||||
@media ${tablet}{
|
||||
margin-left: -24px;
|
||||
}
|
||||
|
||||
${
|
||||
isMobile &&
|
||||
css`
|
||||
margin-left: -24px;
|
||||
`
|
||||
}
|
||||
`}
|
||||
|
||||
.additional-scroll-height {
|
||||
@ -110,6 +117,13 @@ const StyledDropZoneBody = styled(DragAndDrop)`
|
||||
@media ${tablet}{
|
||||
margin-left: -24px;
|
||||
}
|
||||
|
||||
${
|
||||
isMobile &&
|
||||
css`
|
||||
margin-left: -24px;
|
||||
`
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -12,18 +12,12 @@ import {
|
||||
import { Base } from "@appserver/components/themes";
|
||||
|
||||
const tabletProps = css`
|
||||
.section-header_header,
|
||||
.section-header_filter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-body_header {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: ${(props) => props.theme.section.header.background};
|
||||
z-index: 200;
|
||||
margin-right: -2px;
|
||||
z-index: 20;
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
@ -35,7 +29,6 @@ const tabletProps = css`
|
||||
display: block;
|
||||
margin: ${(props) =>
|
||||
props.viewAs === "tile" ? "4px 0 18px" : "4px 0 30px"};
|
||||
margin-right: -1px;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -45,52 +38,59 @@ const StyledSectionContainer = styled.section`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: calc(100vw - 256px);
|
||||
max-width: calc(100vw - 256px);
|
||||
|
||||
@media ${tablet} {
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
|
||||
padding: 0 0 0 16px;
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
|
||||
padding: 0 0 0 16px;
|
||||
${tabletProps};
|
||||
min-width: 100px;
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
margin-top: 48px !important;
|
||||
`}
|
||||
|
||||
.layout-progress-bar {
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
bottom: 21px;
|
||||
|
||||
${(props) =>
|
||||
!props.visible &&
|
||||
css`
|
||||
@media ${tablet} {
|
||||
bottom: 83px;
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
.layout-progress-second-bar {
|
||||
position: fixed;
|
||||
right: 15px;
|
||||
bottom: 83px;
|
||||
|
||||
${(props) =>
|
||||
!props.visible &&
|
||||
css`
|
||||
@media ${tablet} {
|
||||
bottom: 145px;
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
.section-header_header,
|
||||
.section-header_filter {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-body_header,
|
||||
.section-body_filter {
|
||||
display: none;
|
||||
}
|
||||
@media ${tablet} {
|
||||
padding: 0 0 0 16px;
|
||||
${tabletProps};
|
||||
}
|
||||
${isMobile &&
|
||||
css`
|
||||
${tabletProps};
|
||||
min-width: 100px;
|
||||
`}
|
||||
${(props) =>
|
||||
!props.isSectionHeaderAvailable &&
|
||||
css`
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
box-sizing: border-box;
|
||||
`}
|
||||
`;
|
||||
|
||||
StyledSectionContainer.defaultProps = { theme: Base };
|
||||
|
@ -1,123 +1,77 @@
|
||||
import React from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import equal from "fast-deep-equal/react";
|
||||
import classnames from "classnames";
|
||||
import PropTypes from "prop-types";
|
||||
import { LayoutContextConsumer } from "studio/Layout/context";
|
||||
import { isMobile, isMobileOnly } from "react-device-detect";
|
||||
import { tablet, desktop, mobile } from "@appserver/components/utils/device";
|
||||
import NoUserSelect from "@appserver/components/utils/commonStyles";
|
||||
|
||||
import Base from "@appserver/components/themes/base";
|
||||
|
||||
const StyledSectionHeader = styled.div`
|
||||
position: relative;
|
||||
height: 53px;
|
||||
min-height: 53px;
|
||||
margin-right: 20px;
|
||||
|
||||
${NoUserSelect}
|
||||
|
||||
width: calc(100vw - 296px);
|
||||
max-width: calc(100vw - 296px);
|
||||
|
||||
@media ${tablet} {
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 272px)" : "calc(100vw - 84px)"};
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 272px)" : "calc(100vw - 84px)"};
|
||||
height: 61px;
|
||||
min-height: 61px;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 272px)" : "calc(100vw - 84px)"} !important;
|
||||
max-width: ${(props) =>
|
||||
props.showText ? "calc(100vw - 272px)" : "calc(100vw - 84px)"} !important;
|
||||
height: 61px !important;
|
||||
min-height: 61px !important;
|
||||
margin-right: 0px !important;
|
||||
`}
|
||||
|
||||
@media ${mobile} {
|
||||
width: auto;
|
||||
width: calc(100vw - 32px) !important;
|
||||
max-width: calc(100vw - 32px) !important;
|
||||
height: 53px;
|
||||
margin-top: 0px;
|
||||
margin-right: 0px;
|
||||
min-height: 53px;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
|
||||
${isMobileOnly &&
|
||||
css`
|
||||
width: auto;
|
||||
height: 53px !important;
|
||||
margin-top: 48px !important;
|
||||
width: calc(100vw - 32px) !important;
|
||||
max-width: calc(100vw - 32px) !important;
|
||||
height: 53px;
|
||||
min-height: 53px;
|
||||
margin-top: -2px;
|
||||
margin-right: 0px !important;
|
||||
`}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
.section-header,
|
||||
.section-header--hidden {
|
||||
&,
|
||||
.group-button-menu-container > div:first-child {
|
||||
transition: top 0.3s cubic-bezier(0, 0, 0.8, 1);
|
||||
-moz-transition: top 0.3s cubic-bezier(0, 0, 0.8, 1);
|
||||
-ms-transition: top 0.3s cubic-bezier(0, 0, 0.8, 1);
|
||||
-webkit-transition: top 0.3s cubic-bezier(0, 0, 0.8, 1);
|
||||
-o-transition: top 0.3s cubic-bezier(0, 0, 0.8, 1);
|
||||
}
|
||||
.group-button-menu-container {
|
||||
padding-bottom: 0;
|
||||
> div:first-child {
|
||||
top: ${(props) => (!props.isSectionHeaderVisible ? "56px" : "0px")};
|
||||
|
||||
@media ${desktop} {
|
||||
${isMobile &&
|
||||
css`
|
||||
position: absolute;
|
||||
`}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`}
|
||||
.section-header--hidden {
|
||||
${isMobile &&
|
||||
css`
|
||||
top: -61px;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
StyledSectionHeader.defaultProps = { theme: Base };
|
||||
|
||||
class SectionHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const SectionHeader = (props) => {
|
||||
const { viewAs, className, ...rest } = props;
|
||||
|
||||
this.focusRef = React.createRef();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return !equal(this.props, nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
// console.log("PageLayout SectionHeader render");
|
||||
// eslint-disable-next-line react/prop-types
|
||||
|
||||
const { isHeaderVisible, viewAs, ...rest } = this.props;
|
||||
|
||||
return (
|
||||
<div className={rest.className}>
|
||||
<LayoutContextConsumer>
|
||||
{(value) => (
|
||||
<StyledSectionHeader
|
||||
isSectionHeaderVisible={value.isVisible}
|
||||
viewAs={viewAs}
|
||||
>
|
||||
<div
|
||||
className={classnames("section-header hidingHeader", {
|
||||
"section-header--hidden":
|
||||
value.isVisible === undefined ? false : !value.isVisible,
|
||||
})}
|
||||
{...rest}
|
||||
/>
|
||||
</StyledSectionHeader>
|
||||
)}
|
||||
</LayoutContextConsumer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<StyledSectionHeader
|
||||
className={`section-header ${className}`}
|
||||
viewAs={viewAs}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
SectionHeader.displayName = "SectionHeader";
|
||||
|
||||
|
@ -20,6 +20,9 @@ class SettingsStore {
|
||||
isLoading = false;
|
||||
isLoaded = false;
|
||||
|
||||
checkedMaintenance = false;
|
||||
maintenanceExist = false;
|
||||
snackbarExist = false;
|
||||
currentProductId = "";
|
||||
culture = "en";
|
||||
cultures = [];
|
||||
@ -145,6 +148,18 @@ class SettingsStore {
|
||||
this[key] = value;
|
||||
};
|
||||
|
||||
setCheckedMaintenance = (checkedMaintenance) => {
|
||||
this.checkedMaintenance = checkedMaintenance;
|
||||
};
|
||||
|
||||
setMaintenanceExist = (maintenanceExist) => {
|
||||
this.maintenanceExist = maintenanceExist;
|
||||
};
|
||||
|
||||
setSnackbarExist = (snackbar) => {
|
||||
this.snackbarExist = snackbar;
|
||||
};
|
||||
|
||||
setDefaultPage = (defaultPage) => {
|
||||
this.defaultPage = defaultPage;
|
||||
};
|
||||
|
@ -64,8 +64,8 @@ class TfaStore {
|
||||
return api.settings.getTfaNewBackupCodes();
|
||||
};
|
||||
|
||||
unlinkApp = async () => {
|
||||
return api.settings.unlinkTfaApp();
|
||||
unlinkApp = async (id) => {
|
||||
return api.settings.unlinkTfaApp(id);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,27 @@ class FirebaseHelper {
|
||||
return await Promise.resolve(JSON.parse(maintenance.asString()));
|
||||
}
|
||||
|
||||
async checkBar() {
|
||||
if (!this.isEnabled) return Promise.reject("Not enabled");
|
||||
|
||||
const res = await this.remoteConfig.fetchAndActivate();
|
||||
const barValue = this.remoteConfig.getValue("bar");
|
||||
const barString = barValue && barValue.asString();
|
||||
|
||||
if (!barValue || !barString) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const list = JSON.parse(barString);
|
||||
|
||||
if (!list || !(list instanceof Array)) return Promise.resolve([]);
|
||||
|
||||
const bar = list.filter((element) => {
|
||||
return typeof element === "string" && element.length > 0;
|
||||
});
|
||||
|
||||
return await Promise.resolve(bar);
|
||||
}
|
||||
|
||||
async checkCampaigns() {
|
||||
if (!this.isEnabled) return Promise.reject("Not enabled");
|
||||
|
||||
|
@ -17,10 +17,15 @@ const BannerWrapper = styled.div`
|
||||
color: ${(props) => props.theme.campaignsBanner.color};
|
||||
}
|
||||
|
||||
.banner-img-wrapper {
|
||||
height: 160px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.banner-sub-header {
|
||||
|
70
packages/asc-web-components/email-chips/README.md
Normal file
@ -0,0 +1,70 @@
|
||||
# EmailChips
|
||||
|
||||
Custom email-chips
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import EmailChips from "@appserver/components/email-chips";
|
||||
```
|
||||
|
||||
```jsx
|
||||
<EmailChips
|
||||
options={[]}
|
||||
onChange={(selected) => console.log(selected)}
|
||||
placeholder="Invite people by name or email"
|
||||
clearButtonLabel="Clear list"
|
||||
existEmailText="This email address has already been entered"
|
||||
invalidEmailText="Invalid email address"
|
||||
exceededLimitText="The limit on the number of emails has reached the maximum"
|
||||
exceededLimitInputText="The limit on the number of characters has reached the maximum value"
|
||||
chipOverLimitText="The limit on the number of characters has reached the maximum value"
|
||||
exceededLimit=500,
|
||||
/>
|
||||
```
|
||||
|
||||
#### Options - an array of objects that contains the following fields:
|
||||
|
||||
```js
|
||||
const options = [
|
||||
{
|
||||
name: "Ivan Petrov",
|
||||
email: "myname@gmul.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Options have options:
|
||||
|
||||
- name - Display text
|
||||
- email - Email address
|
||||
- isValid - Displays whether the email is valid
|
||||
|
||||
#### Actions that can be performed on chips and input:
|
||||
|
||||
- Enter a chip into the input (chips are checked for a valid email, and the same chips).
|
||||
- Add chips by pressing Enter or NumpadEnter.
|
||||
- By double-clicking on the mouse button or pressing enter on a specific selected chip, you can switch to the chip editing mode.
|
||||
- You can exit the editing mode by pressing Escape, Enter, NumpadEnter or by clicking ouside.
|
||||
- Remove the chips by clicking on the button in the form of a cross.
|
||||
- Click on the chip once, thereby highlighting it.
|
||||
- Hold down the shift button by moving the arrows to the left, right or clicking the mouse on the chips, thereby highlighting several chips.
|
||||
- The highlighted chip(s) can be removed by clicking on the button Backspace or Delete.
|
||||
- The selected chip(s) can be copied to the clipboard by pressing "ctrl + c".
|
||||
- You can remove all chips by clicking on the button "Clear list".
|
||||
|
||||
### Properties
|
||||
|
||||
| Props | Type | Required | Values | Default | Description |
|
||||
| ------------------------ | :------------: | :------: | :----: | :-----------------------------------------------------------------------------: | -------------------------------------------------------------------------------- |
|
||||
| `options` | `obj`, `array` | - | - | - | Array of objects with chips |
|
||||
| `onChange` | `func` | ✅ | - | - | displays valid email addresses. Called when changing chips |
|
||||
| `placeholder` | `string` | - | - | Invite people by name or email | Placeholder text for the input |
|
||||
| `clearButtonLabel` | `string` | - | - | Clear list | The text of the button for cleaning all chips |
|
||||
| `existEmailText` | `string` | - | - | This email address has already been entered | Warning text when entering an existing email |
|
||||
| `invalidEmailText` | `string` | - | - | Invalid email address | Warning text when entering an invalid email |
|
||||
| `exceededLimit` | `number` | - | - | 500 | Limit of chips (number) |
|
||||
| `exceededLimitText` | `string` | - | - | The limit on the number of emails has reached the maximum | Warning text when exceeding the limit of the number of chips |
|
||||
| `exceededLimitInputText` | `string` | - | - | The limit on the number of characters has reached the maximum value | Warning text when entering the number of characters in input exceeding the limit |
|
||||
| `chipOverLimitText` | `string` | - | - | The limit on the number of characters in an email has reached its maximum value | Warning text when entering the number of email characters exceeding the limit |
|
@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import EmailChips from ".";
|
||||
|
||||
const Options = [
|
||||
{ name: "Ivan Petrov", email: "myname@gmul.com", isValid: true },
|
||||
{ name: "Donna Cross", email: "myname45@gmul.com", isValid: true },
|
||||
{ name: "myname@gmul.co45", email: "myname@gmul.co45", isValid: false },
|
||||
{ name: "Lisa Cooper", email: "myn348ame@gmul.com", isValid: true },
|
||||
{ name: "myname19@gmail.com", email: "myname19@gmail.com", isValid: true },
|
||||
{ name: "myname@gmail.com", email: "myname@gmail.com", isValid: true },
|
||||
{
|
||||
name: "mynameiskonstantine1353434@gmail.com",
|
||||
email: "mynameiskonstantine1353434@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "mynameiskonstantine56454864846455488875454654846454@gmail.com",
|
||||
email: "mynameiskonstantine56454864846455488875454654846454@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "mynameiskonstantine3246@gmail.com",
|
||||
email: "mynameiskonstantine3246@gmail.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = (props) => (
|
||||
<div
|
||||
style={{
|
||||
height: "220px",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const Template = (args) => (
|
||||
<Wrapper>
|
||||
<EmailChips {...args} />
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
options: Options,
|
||||
onChange: (selected) => console.log(selected),
|
||||
placeholder: "Invite people by name or email",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
chipOverLimitText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 500,
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.args = {
|
||||
options: [],
|
||||
placeholder: "Type your chips...",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
chipOverLimitText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 500,
|
||||
};
|
@ -0,0 +1,72 @@
|
||||
import { Meta, Story, ArgsTable, Canvas } from "@storybook/addon-docs/blocks";
|
||||
import EmailChips from "./";
|
||||
import * as stories from "./email-chips.stories.js";
|
||||
|
||||
<Meta
|
||||
title="Components/EmailChips"
|
||||
component={EmailChips}
|
||||
argTypes={{
|
||||
onChange: { required: true },
|
||||
}}
|
||||
/>
|
||||
|
||||
# EmailChips
|
||||
|
||||
Custom email-chips
|
||||
|
||||
### Usage
|
||||
|
||||
```js
|
||||
import EmailChips from "@appserver/components/email-chips";
|
||||
```
|
||||
|
||||
### EmailChips - Default
|
||||
|
||||
<Canvas>
|
||||
<Story story={stories.Default} name="Default" />
|
||||
</Canvas>
|
||||
|
||||
#### Properties
|
||||
|
||||
<ArgsTable story="Default" />
|
||||
|
||||
#### Options - an array of objects that contains the following fields:
|
||||
|
||||
```js
|
||||
const options = [
|
||||
{
|
||||
name: "Ivan Petrov",
|
||||
email: "myname@gmul.com",
|
||||
isValid: true,
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
Options have options:
|
||||
|
||||
- name - Display text
|
||||
- email - Email address
|
||||
- isValid - Displays whether the email is valid
|
||||
|
||||
### EmailChips - Empty
|
||||
|
||||
<Canvas>
|
||||
<Story story={stories.Empty} name="Empty" />
|
||||
</Canvas>
|
||||
|
||||
#### Properties
|
||||
|
||||
<ArgsTable story="Empty" />
|
||||
|
||||
#### Actions that can be performed on chips and input:
|
||||
|
||||
- Enter a chip into the input (chips are checked for a valid email, and the same chips).
|
||||
- Add chips by pressing Enter or NumpadEnter.
|
||||
- By double-clicking on the mouse button or pressing enter on a specific selected chip, you can switch to the chip editing mode.
|
||||
- You can exit the editing mode by pressing Escape, Enter, NumpadEnter or by clicking ouside.
|
||||
- Remove the chips by clicking on the button in the form of a cross.
|
||||
- Click on the chip once, thereby highlighting it.
|
||||
- Hold down the shift button by moving the arrows to the left, right or clicking the mouse on the chips, thereby highlighting several chips.
|
||||
- The highlighted chip(s) can be removed by clicking on the button Backspace or Delete.
|
||||
- The selected chip(s) can be copied to the clipboard by pressing "ctrl + c".
|
||||
- You can remove all chips by clicking on the button "Clear list".
|
32
packages/asc-web-components/email-chips/email-chips.test.js
Normal file
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { mount } from "enzyme";
|
||||
import EmailChips from ".";
|
||||
|
||||
const baseProps = {
|
||||
placeholder: "Placeholder",
|
||||
clearButtonLabel: "Clear list ",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email",
|
||||
};
|
||||
|
||||
describe("<InputWithChips />", () => {
|
||||
it("accepts id", () => {
|
||||
const wrapper = mount(<EmailChips {...baseProps} id="testId" />);
|
||||
|
||||
expect(wrapper.prop("id")).toEqual("testId");
|
||||
});
|
||||
|
||||
it("accepts className", () => {
|
||||
const wrapper = mount(<EmailChips {...baseProps} className="test" />);
|
||||
|
||||
expect(wrapper.prop("className")).toEqual("test");
|
||||
});
|
||||
|
||||
it("accepts style", () => {
|
||||
const wrapper = mount(
|
||||
<EmailChips {...baseProps} style={{ color: "red" }} />
|
||||
);
|
||||
|
||||
expect(wrapper.getDOMNode().style).toHaveProperty("color", "red");
|
||||
});
|
||||
});
|
368
packages/asc-web-components/email-chips/index.js
Normal file
@ -0,0 +1,368 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import Scrollbar from "../scrollbar";
|
||||
import { useClickOutside } from "../utils/useClickOutside.js";
|
||||
|
||||
import {
|
||||
StyledContent,
|
||||
StyledChipGroup,
|
||||
StyledChipWithInput,
|
||||
} from "./styled-emailchips";
|
||||
import {
|
||||
MAX_EMAIL_LENGTH_WITH_DOTS,
|
||||
sliceEmail,
|
||||
} from "./sub-components/helpers";
|
||||
import InputGroup from "./sub-components/input-group";
|
||||
import ChipsRender from "./sub-components/chips-render";
|
||||
import { EmailSettings, parseAddresses } from "../utils/email";
|
||||
|
||||
const calcMaxLengthInput = (exceededLimit) =>
|
||||
exceededLimit * MAX_EMAIL_LENGTH_WITH_DOTS;
|
||||
|
||||
const EmailChips = ({
|
||||
options,
|
||||
placeholder,
|
||||
onChange,
|
||||
clearButtonLabel,
|
||||
existEmailText,
|
||||
invalidEmailText,
|
||||
exceededLimit,
|
||||
exceededLimitText,
|
||||
exceededLimitInputText,
|
||||
chipOverLimitText,
|
||||
...props
|
||||
}) => {
|
||||
const [chips, setChips] = useState(options || []);
|
||||
const [currentChip, setCurrentChip] = useState(null);
|
||||
const [selectedChips, setSelectedChips] = useState([]);
|
||||
|
||||
const [isExistedOn, setIsExistedOn] = useState(false);
|
||||
const [isExceededLimitChips, setIsExceededLimitChips] = useState(false);
|
||||
const [isExceededLimitInput, setIsExceededLimitInput] = useState(false);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const blockRef = useRef(null);
|
||||
const scrollbarRef = useRef(null);
|
||||
const chipsCount = useRef(options?.length);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(
|
||||
chips.map((it) => {
|
||||
if (it?.name === it?.email || it?.name === "") {
|
||||
return {
|
||||
email: it?.email,
|
||||
isValid: it?.isValid,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: it?.name,
|
||||
email: it?.email,
|
||||
isValid: it?.isValid,
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [chips]);
|
||||
|
||||
useEffect(() => {
|
||||
const isChipAdd = chips.length > chipsCount.current;
|
||||
if (scrollbarRef.current && isChipAdd) {
|
||||
scrollbarRef.current.scrollToBottom();
|
||||
}
|
||||
chipsCount.current = chips.length;
|
||||
}, [chips.length]);
|
||||
|
||||
useClickOutside(
|
||||
blockRef,
|
||||
() => {
|
||||
if (selectedChips.length > 0) {
|
||||
setSelectedChips([]);
|
||||
}
|
||||
},
|
||||
selectedChips
|
||||
);
|
||||
|
||||
useClickOutside(inputRef, () => {
|
||||
onHideAllTooltips();
|
||||
});
|
||||
|
||||
const onClick = (value, isShiftKey) => {
|
||||
if (isShiftKey) {
|
||||
const isExisted = !!selectedChips?.find((it) => it.email === value.email);
|
||||
return isExisted
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it.email != value.email)
|
||||
)
|
||||
: setSelectedChips([value, ...selectedChips]);
|
||||
} else {
|
||||
setSelectedChips([value]);
|
||||
}
|
||||
};
|
||||
|
||||
const onDoubleClick = (value) => {
|
||||
setCurrentChip(value);
|
||||
};
|
||||
|
||||
const onDelete = useCallback(
|
||||
(value) => {
|
||||
setChips(chips.filter((it) => it.email !== value.email));
|
||||
},
|
||||
[chips]
|
||||
);
|
||||
|
||||
const checkSelected = (value) => {
|
||||
return !!selectedChips?.find((item) => item?.email === value?.email);
|
||||
};
|
||||
|
||||
const onSaveNewChip = (value, newValue) => {
|
||||
const settings = new EmailSettings();
|
||||
settings.allowName = true;
|
||||
let parsed = parseAddresses(newValue, settings);
|
||||
parsed[0].isValid = parsed[0].isValid();
|
||||
if (newValue && newValue !== `"${value?.name}" <${value?.email}>`) {
|
||||
const newChips = chips.map((it) => {
|
||||
return it.email === value.email ? sliceEmail(parsed[0]) : it;
|
||||
});
|
||||
setChips(newChips);
|
||||
setSelectedChips([sliceEmail(parsed[0])]);
|
||||
}
|
||||
|
||||
containerRef.current.setAttribute("tabindex", "-1");
|
||||
containerRef.current.focus();
|
||||
|
||||
setCurrentChip(null);
|
||||
};
|
||||
|
||||
const copyToClipbord = () => {
|
||||
if (currentChip === null) {
|
||||
navigator.clipboard.writeText(
|
||||
selectedChips
|
||||
.map((it) => {
|
||||
if (it.name !== it.email) {
|
||||
let copyItem = `"${it.name}" <${it.email}>`;
|
||||
return copyItem;
|
||||
} else {
|
||||
return it.email;
|
||||
}
|
||||
})
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (e) => {
|
||||
const whiteList = [
|
||||
"Enter",
|
||||
"Escape",
|
||||
"Backspace",
|
||||
"Delete",
|
||||
"ArrowRigth",
|
||||
"ArrowLeft",
|
||||
"ArrowLeft",
|
||||
"ArrowRight",
|
||||
"KeyC",
|
||||
];
|
||||
|
||||
const code = e.code;
|
||||
|
||||
const isShiftDown = e.shiftKey;
|
||||
const isCtrlDown = e.ctrlKey;
|
||||
|
||||
if (!whiteList.includes(code) && !isCtrlDown && !isShiftDown) {
|
||||
return;
|
||||
}
|
||||
if (code === "Enter" && selectedChips.length == 1 && !currentChip) {
|
||||
e.stopPropagation();
|
||||
setCurrentChip(selectedChips[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === "Escape") {
|
||||
setSelectedChips(currentChip ? [currentChip] : []);
|
||||
containerRef.current.setAttribute("tabindex", "0");
|
||||
containerRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedChips.length > 0 &&
|
||||
(code === "Backspace" || code === "Delete") &&
|
||||
!currentChip
|
||||
) {
|
||||
const filteredChips = chips.filter((e) => !~selectedChips.indexOf(e));
|
||||
setChips(filteredChips);
|
||||
setSelectedChips([]);
|
||||
inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChips.length > 0 && !currentChip) {
|
||||
let chip = null;
|
||||
|
||||
if (isShiftDown && code === "ArrowRigth") {
|
||||
chip = selectedChips[selectedChips.length - 1];
|
||||
} else {
|
||||
chip = selectedChips[0];
|
||||
}
|
||||
|
||||
const index = chips.findIndex((it) => it.email === chip?.email);
|
||||
|
||||
switch (code) {
|
||||
case "ArrowLeft": {
|
||||
if (isShiftDown) {
|
||||
selectedChips.includes(chips[index - 1])
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it !== chips[index])
|
||||
)
|
||||
: chips[index - 1] &&
|
||||
setSelectedChips([chips[index - 1], ...selectedChips]);
|
||||
} else if (index != 0) {
|
||||
setSelectedChips([chips[index - 1]]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
if (isShiftDown) {
|
||||
selectedChips.includes(chips[index + 1])
|
||||
? setSelectedChips(
|
||||
selectedChips.filter((it) => it !== chips[index])
|
||||
)
|
||||
: chips[index + 1] &&
|
||||
setSelectedChips([chips[index + 1], ...selectedChips]);
|
||||
} else {
|
||||
if (index != chips.length - 1) {
|
||||
setSelectedChips([chips[index + 1]]);
|
||||
} else {
|
||||
setSelectedChips([]);
|
||||
if (inputRef) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "KeyC": {
|
||||
if (isCtrlDown) {
|
||||
copyToClipbord();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goFromInputToChips = () => {
|
||||
setSelectedChips([chips[chips?.length - 1]]);
|
||||
};
|
||||
|
||||
const onClearClick = () => {
|
||||
setChips([]);
|
||||
};
|
||||
|
||||
const onHideAllTooltips = () => {
|
||||
setIsExceededLimitChips(false);
|
||||
setIsExistedOn(false);
|
||||
setIsExceededLimitInput(false);
|
||||
};
|
||||
|
||||
const showTooltipOfLimit = () => {
|
||||
setIsExceededLimitInput(true);
|
||||
};
|
||||
|
||||
const onAddChip = (chipsToAdd) => {
|
||||
setIsExceededLimitChips(chips.length >= exceededLimit);
|
||||
if (chips.length >= exceededLimit) return;
|
||||
const filterLimit = exceededLimit - chips.length;
|
||||
|
||||
const filteredChips = chipsToAdd.map(sliceEmail).filter((it, index) => {
|
||||
const isExisted = !!chips.find(
|
||||
(chip) => chip.email === it || chip.email === it?.email
|
||||
);
|
||||
if (chipsToAdd.length === 1) {
|
||||
setIsExistedOn(isExisted);
|
||||
if (isExisted) return false;
|
||||
}
|
||||
return !isExisted && index < filterLimit;
|
||||
});
|
||||
setChips([...chips, ...filteredChips]);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContent {...props}>
|
||||
<StyledChipGroup onKeyDown={onKeyDown} ref={containerRef} tabindex="-1">
|
||||
<StyledChipWithInput length={chips.length}>
|
||||
<Scrollbar scrollclass={"scroll"} stype="thumbV" ref={scrollbarRef}>
|
||||
<ChipsRender
|
||||
chips={chips}
|
||||
checkSelected={checkSelected}
|
||||
currentChip={currentChip}
|
||||
blockRef={blockRef}
|
||||
onClick={onClick}
|
||||
invalidEmailText={invalidEmailText}
|
||||
chipOverLimitText={chipOverLimitText}
|
||||
onDelete={onDelete}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onSaveNewChip={onSaveNewChip}
|
||||
/>
|
||||
</Scrollbar>
|
||||
|
||||
<InputGroup
|
||||
placeholder={placeholder}
|
||||
exceededLimitText={exceededLimitText}
|
||||
existEmailText={existEmailText}
|
||||
exceededLimitInputText={exceededLimitInputText}
|
||||
clearButtonLabel={clearButtonLabel}
|
||||
inputRef={inputRef}
|
||||
containerRef={containerRef}
|
||||
maxLength={calcMaxLengthInput(exceededLimit)}
|
||||
goFromInputToChips={goFromInputToChips}
|
||||
onClearClick={onClearClick}
|
||||
isExistedOn={isExistedOn}
|
||||
isExceededLimitChips={isExceededLimitChips}
|
||||
isExceededLimitInput={isExceededLimitInput}
|
||||
onHideAllTooltips={onHideAllTooltips}
|
||||
showTooltipOfLimit={showTooltipOfLimit}
|
||||
onAddChip={onAddChip}
|
||||
/>
|
||||
</StyledChipWithInput>
|
||||
</StyledChipGroup>
|
||||
</StyledContent>
|
||||
);
|
||||
};
|
||||
|
||||
EmailChips.propTypes = {
|
||||
/** Array of objects with chips */
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
/** Placeholder text for the input */
|
||||
placeholder: PropTypes.string,
|
||||
/** The text that is displayed in the button for cleaning all chips */
|
||||
clearButtonLabel: PropTypes.string,
|
||||
/** Warning text when entering an existing email */
|
||||
existEmailText: PropTypes.string,
|
||||
/** Warning text when entering an invalid email */
|
||||
invalidEmailText: PropTypes.string,
|
||||
/** Limit of chips */
|
||||
exceededLimit: PropTypes.number,
|
||||
/** Warning text when entering the number of chips exceeding the limit */
|
||||
exceededLimitText: PropTypes.string,
|
||||
/** Warning text when entering the number of characters in input exceeding the limit */
|
||||
exceededLimitInputText: PropTypes.string,
|
||||
/** Warning text when entering the number of email characters exceeding the limit */
|
||||
chipOverLimitText: PropTypes.string,
|
||||
/** Will be called when the selected items are changed */
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
EmailChips.defaultProps = {
|
||||
placeholder: "Invite people by name or email",
|
||||
clearButtonLabel: "Clear list",
|
||||
existEmailText: "This email address has already been entered",
|
||||
invalidEmailText: "Invalid email address",
|
||||
exceededLimitText:
|
||||
"The limit on the number of emails has reached the maximum",
|
||||
exceededLimitInputText:
|
||||
"The limit on the number of characters has reached the maximum value",
|
||||
exceededLimit: 50,
|
||||
};
|
||||
|
||||
export default EmailChips;
|
164
packages/asc-web-components/email-chips/styled-emailchips.js
Normal file
@ -0,0 +1,164 @@
|
||||
import styled from "styled-components";
|
||||
import commonInputStyle from "../text-input/common-input-styles";
|
||||
import Base from "../themes/base";
|
||||
import TextInput from "../text-input";
|
||||
|
||||
const StyledChipWithInput = styled.div`
|
||||
min-height: 32px;
|
||||
max-height: 220px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: fit-content;
|
||||
width: ${(props) => props.length === 0 && "100%"};
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
position: relative;
|
||||
width: 469px;
|
||||
height: 220px;
|
||||
`;
|
||||
|
||||
const StyledChipGroup = styled.div`
|
||||
:focus-visible {
|
||||
outline: 0px solid #2da7db !important;
|
||||
}
|
||||
height: fit-content;
|
||||
${commonInputStyle} :focus-within {
|
||||
border-color: ${(props) => props.theme.inputBlock.borderColor};
|
||||
}
|
||||
|
||||
.scroll {
|
||||
height: fit-content;
|
||||
position: inherit !important;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
:focus-visible {
|
||||
outline: 0px solid #2da7db !important;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
`;
|
||||
StyledChipGroup.defaultProps = { theme: Base };
|
||||
|
||||
const StyledAllChips = styled.div`
|
||||
width: 448px;
|
||||
max-height: 180px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
const StyledChip = styled.div`
|
||||
width: fit-content;
|
||||
max-width: calc(100% - 18px);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
background: #eceef1;
|
||||
|
||||
height: 32px;
|
||||
margin: 2px 4px;
|
||||
padding: ${(props) => (props.isSelected ? "5px 7px" : "6px 8px")};
|
||||
|
||||
border-radius: 3px 0 0 3px;
|
||||
border: ${(props) => props.isSelected && "1px dashed #000"};
|
||||
background: ${(props) => (props.isValid ? "#ECEEF1" : "#F7CDBE")};
|
||||
|
||||
user-select: none;
|
||||
|
||||
.warning_icon_wrap {
|
||||
cursor: pointer;
|
||||
.warning_icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledChipValue = styled.div`
|
||||
margin-right: 4px;
|
||||
min-width: 0px;
|
||||
max-width: 395px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
|
||||
color: #333333;
|
||||
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledChipInput = styled(TextInput)`
|
||||
flex: ${(props) => `${props.flexvalue}!important`};
|
||||
`;
|
||||
|
||||
const StyledInputWithLink = styled.div`
|
||||
position: relative;
|
||||
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: auto 15%;
|
||||
align-content: space-between;
|
||||
width: calc(100% - 8px);
|
||||
|
||||
.textInput {
|
||||
width: calc(100% - 8px);
|
||||
padding: 0px;
|
||||
margin: 8px 0px 10px 8px;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-align: end;
|
||||
margin: 10px 0px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin-right: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled.div`
|
||||
position: absolute;
|
||||
top: -49px;
|
||||
left: 0;
|
||||
|
||||
max-width: 435px;
|
||||
padding: 16px;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
background: #f8f7bf;
|
||||
border-radius: 6px;
|
||||
opacity: 0.9;
|
||||
`;
|
||||
|
||||
export {
|
||||
StyledChipWithInput,
|
||||
StyledContent,
|
||||
StyledChipGroup,
|
||||
StyledAllChips,
|
||||
StyledChip,
|
||||
StyledChipValue,
|
||||
StyledContainer,
|
||||
StyledChipInput,
|
||||
StyledInputWithLink,
|
||||
StyledTooltip,
|
||||
};
|
190
packages/asc-web-components/email-chips/sub-components/chip.js
Normal file
@ -0,0 +1,190 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import IconButton from "../../icon-button";
|
||||
import Tooltip from "../../tooltip";
|
||||
import { useClickOutside } from "../../utils/useClickOutside.js";
|
||||
|
||||
import { DeleteIcon, WarningIcon } from "../svg";
|
||||
import {
|
||||
MAX_EMAIL_LENGTH,
|
||||
MAX_EMAIL_LENGTH_WITH_DOTS,
|
||||
sliceEmail,
|
||||
} from "./helpers";
|
||||
|
||||
import {
|
||||
StyledChip,
|
||||
StyledChipInput,
|
||||
StyledChipValue,
|
||||
StyledContainer,
|
||||
} from "../styled-emailchips.js";
|
||||
|
||||
const Chip = (props) => {
|
||||
const {
|
||||
value,
|
||||
currentChip,
|
||||
isSelected,
|
||||
isValid,
|
||||
invalidEmailText,
|
||||
chipOverLimitText,
|
||||
onDelete,
|
||||
onDoubleClick,
|
||||
onSaveNewChip,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
function initNewValue() {
|
||||
return value?.email === value?.name || value?.name === ""
|
||||
? value?.email
|
||||
: `"${value?.name}" <${value?.email}>`;
|
||||
}
|
||||
|
||||
const [newValue, setNewValue] = useState(initNewValue());
|
||||
const [chipWidth, setChipWidth] = useState(0);
|
||||
const [isChipOverLimit, setIsChipOverLimit] = useState(false);
|
||||
|
||||
const tooltipRef = useRef(null);
|
||||
const warningRef = useRef(null);
|
||||
const chipRef = useRef(null);
|
||||
const chipInputRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setChipWidth(chipRef.current?.clientWidth);
|
||||
}, [chipRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSelected) {
|
||||
chipRef.current?.scrollIntoView({ block: "end" });
|
||||
}
|
||||
}, [isSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newValue.length > MAX_EMAIL_LENGTH) {
|
||||
setIsChipOverLimit(true);
|
||||
} else {
|
||||
setIsChipOverLimit(false);
|
||||
}
|
||||
}, [newValue]);
|
||||
|
||||
useClickOutside(warningRef, () => tooltipRef.current.hideTooltip());
|
||||
useClickOutside(
|
||||
chipInputRef,
|
||||
() => {
|
||||
onSaveNewChip(value, newValue);
|
||||
},
|
||||
newValue
|
||||
);
|
||||
|
||||
const onChange = (e) => {
|
||||
if (
|
||||
e.target.value.length <= MAX_EMAIL_LENGTH_WITH_DOTS ||
|
||||
e.target.value.length < newValue.length
|
||||
) {
|
||||
setNewValue(e.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickHandler = (e) => {
|
||||
if (e.shiftKey) {
|
||||
document.getSelection().removeAllRanges();
|
||||
}
|
||||
onClick(value, e.shiftKey);
|
||||
};
|
||||
|
||||
const onDoubleClickHandler = () => {
|
||||
onDoubleClick(value);
|
||||
};
|
||||
|
||||
const onIconClick = () => {
|
||||
onDelete(value);
|
||||
};
|
||||
|
||||
const onInputKeyDown = useCallback(
|
||||
(e) => {
|
||||
const code = e.code;
|
||||
|
||||
switch (code) {
|
||||
case "Enter":
|
||||
case "NumpadEnter": {
|
||||
onSaveNewChip(value, newValue);
|
||||
setNewValue(sliceEmail(newValue).email);
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
setNewValue(initNewValue());
|
||||
onDoubleClick(null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
[newValue]
|
||||
);
|
||||
if (value?.email === currentChip?.email) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{isChipOverLimit && (
|
||||
<Tooltip getContent={() => {}} id="input" effect="float" />
|
||||
)}
|
||||
<StyledChipInput
|
||||
data-for="input"
|
||||
data-tip={chipOverLimitText}
|
||||
value={newValue}
|
||||
forwardedRef={chipInputRef}
|
||||
onChange={onChange}
|
||||
onKeyDown={onInputKeyDown}
|
||||
isAutoFocussed
|
||||
withBorder={false}
|
||||
maxLength={MAX_EMAIL_LENGTH_WITH_DOTS}
|
||||
flexvalue={
|
||||
value?.name !== value?.email ? "0 1 auto" : `0 0 ${chipWidth}px`
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledChip
|
||||
isSelected={isSelected}
|
||||
onDoubleClick={onDoubleClickHandler}
|
||||
onClick={onClickHandler}
|
||||
isValid={isValid}
|
||||
ref={chipRef}
|
||||
>
|
||||
{!isValid && (
|
||||
<div className="warning_icon_wrap" ref={warningRef}>
|
||||
<IconButton
|
||||
iconName={WarningIcon}
|
||||
size={12}
|
||||
className="warning_icon_wrap warning_icon "
|
||||
data-for="group"
|
||||
data-tip={invalidEmailText}
|
||||
/>
|
||||
<Tooltip
|
||||
getContent={() => {}}
|
||||
id="group"
|
||||
reference={tooltipRef}
|
||||
place={"top"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<StyledChipValue>{value?.name || value?.email}</StyledChipValue>
|
||||
<IconButton iconName={DeleteIcon} size={12} onClick={onIconClick} />
|
||||
</StyledChip>
|
||||
);
|
||||
};
|
||||
|
||||
Chip.propTypes = {
|
||||
value: PropTypes.object,
|
||||
currentChip: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
isValid: PropTypes.bool,
|
||||
invalidEmailText: PropTypes.string,
|
||||
chipOverLimitText: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onSaveNewChip: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Chip;
|
@ -0,0 +1,76 @@
|
||||
import React, { memo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { EmailSettings, parseAddress } from "../../utils/email";
|
||||
import Chip from "./chip";
|
||||
|
||||
import { StyledAllChips } from "../styled-emailchips";
|
||||
|
||||
const ChipsRender = memo(
|
||||
({
|
||||
chips,
|
||||
currentChip,
|
||||
blockRef,
|
||||
checkSelected,
|
||||
invalidEmailText,
|
||||
chipOverLimitText,
|
||||
onDelete,
|
||||
onDoubleClick,
|
||||
onSaveNewChip,
|
||||
onClick,
|
||||
...props
|
||||
}) => {
|
||||
const emailSettings = new EmailSettings();
|
||||
|
||||
const checkEmail = (email) => {
|
||||
const emailObj = parseAddress(email, emailSettings);
|
||||
return emailObj.isValid();
|
||||
};
|
||||
|
||||
const checkIsSelected = (value) => {
|
||||
return checkSelected(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledAllChips ref={blockRef}>
|
||||
{chips?.map((it) => {
|
||||
return (
|
||||
<Chip
|
||||
key={it?.email}
|
||||
value={it}
|
||||
currentChip={currentChip}
|
||||
isSelected={checkIsSelected(it)}
|
||||
isValid={checkEmail(it?.email)}
|
||||
invalidEmailText={invalidEmailText}
|
||||
chipOverLimitText={chipOverLimitText}
|
||||
onDelete={onDelete}
|
||||
onDoubleClick={onDoubleClick}
|
||||
onSaveNewChip={onSaveNewChip}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledAllChips>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChipsRender.propTypes = {
|
||||
chips: PropTypes.arrayOf(PropTypes.object),
|
||||
currentChip: PropTypes.object,
|
||||
|
||||
invalidEmailText: PropTypes.string,
|
||||
chipOverLimitText: PropTypes.string,
|
||||
|
||||
blockRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
|
||||
checkSelected: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDoubleClick: PropTypes.func,
|
||||
onSaveNewChip: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
ChipsRender.displayName = "ChipsRender";
|
||||
|
||||
export default ChipsRender;
|
@ -0,0 +1,24 @@
|
||||
// Maximum allowed email length
|
||||
// https://www.lifewire.com/is-email-address-length-limited-1171110
|
||||
export const MAX_EMAIL_LENGTH = 320;
|
||||
export const MAX_EMAIL_LENGTH_WITH_DOTS = 323;
|
||||
const MAX_DISPLAY_NAME_LENGTH = 64;
|
||||
const MAX_VALUE_LENGTH = 256;
|
||||
|
||||
export const truncate = (str, length) =>
|
||||
str?.length > length ? str?.slice(0, length) + "..." : str;
|
||||
|
||||
export const sliceEmail = (it) => {
|
||||
if (typeof it === "string") {
|
||||
const res = truncate(it, MAX_EMAIL_LENGTH);
|
||||
return {
|
||||
name: res,
|
||||
email: res,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...it,
|
||||
name: truncate(it?.name, MAX_DISPLAY_NAME_LENGTH),
|
||||
email: truncate(it?.email, MAX_VALUE_LENGTH),
|
||||
};
|
||||
};
|
@ -0,0 +1,137 @@
|
||||
import React, { memo, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import Link from "../../link";
|
||||
import TextInput from "../../text-input";
|
||||
|
||||
import { StyledInputWithLink, StyledTooltip } from "../styled-emailchips";
|
||||
import { EmailSettings, parseAddresses } from "../../utils/email";
|
||||
|
||||
const InputGroup = memo(
|
||||
({
|
||||
placeholder,
|
||||
exceededLimitText,
|
||||
existEmailText,
|
||||
exceededLimitInputText,
|
||||
clearButtonLabel,
|
||||
|
||||
inputRef,
|
||||
containerRef,
|
||||
|
||||
maxLength,
|
||||
|
||||
isExistedOn,
|
||||
isExceededLimitChips,
|
||||
isExceededLimitInput,
|
||||
|
||||
goFromInputToChips,
|
||||
onClearClick,
|
||||
onHideAllTooltips,
|
||||
showTooltipOfLimit,
|
||||
onAddChip,
|
||||
}) => {
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
const onInputChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
onHideAllTooltips();
|
||||
if (e.target.value.length >= maxLength) showTooltipOfLimit();
|
||||
};
|
||||
|
||||
const onInputKeyDown = (e) => {
|
||||
const code = e.code;
|
||||
|
||||
switch (code) {
|
||||
case "Enter":
|
||||
case "NumpadEnter": {
|
||||
onEnterPress();
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
const isCursorStart = inputRef.current.selectionStart === 0;
|
||||
if (!isCursorStart) return;
|
||||
goFromInputToChips();
|
||||
if (inputRef) {
|
||||
onHideAllTooltips();
|
||||
inputRef.current.blur();
|
||||
containerRef.current.setAttribute("tabindex", "0");
|
||||
containerRef.current.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onEnterPress = () => {
|
||||
if (isExceededLimitChips) return;
|
||||
if (isExistedOn) return;
|
||||
if (value.trim().length == 0) return;
|
||||
const settings = new EmailSettings();
|
||||
settings.allowName = true;
|
||||
const chipsFromString = parseAddresses(value, settings).map((it) => ({
|
||||
name: it.name === "" ? it.email : it.name,
|
||||
email: it.email,
|
||||
isValid: it.isValid(),
|
||||
parseErrors: it.parseErrors,
|
||||
}));
|
||||
onAddChip(chipsFromString);
|
||||
setValue("");
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledInputWithLink>
|
||||
{isExistedOn && <StyledTooltip>{existEmailText}</StyledTooltip>}
|
||||
{isExceededLimitChips && (
|
||||
<StyledTooltip>{exceededLimitText}</StyledTooltip>
|
||||
)}
|
||||
{isExceededLimitInput && (
|
||||
<StyledTooltip>{exceededLimitInputText}</StyledTooltip>
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
forwardedRef={inputRef}
|
||||
onKeyDown={onInputKeyDown}
|
||||
placeholder={placeholder}
|
||||
withBorder={false}
|
||||
className="textInput"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
<Link
|
||||
type="action"
|
||||
isHovered={true}
|
||||
className="link"
|
||||
onClick={onClearClick}
|
||||
>
|
||||
{clearButtonLabel}
|
||||
</Link>
|
||||
</StyledInputWithLink>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
InputGroup.propTypes = {
|
||||
inputRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
containerRef: PropTypes.shape({ current: PropTypes.any }),
|
||||
|
||||
placeholder: PropTypes.string,
|
||||
exceededLimitText: PropTypes.string,
|
||||
existEmailText: PropTypes.string,
|
||||
exceededLimitInputText: PropTypes.string,
|
||||
clearButtonLabel: PropTypes.string,
|
||||
|
||||
maxLength: PropTypes.number,
|
||||
|
||||
goFromInputToChips: PropTypes.func,
|
||||
onClearClick: PropTypes.func,
|
||||
isExistedOn: PropTypes.bool,
|
||||
isExceededLimitChips: PropTypes.bool,
|
||||
isExceededLimitInput: PropTypes.bool,
|
||||
onHideAllTooltips: PropTypes.func,
|
||||
showTooltipOfLimit: PropTypes.func,
|
||||
onAddChip: PropTypes.func,
|
||||
};
|
||||
|
||||
InputGroup.displayName = "InputGroup";
|
||||
|
||||
export default InputGroup;
|
10
packages/asc-web-components/email-chips/svg/Delete.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_19101_130319)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.41442 6.00033L10.707 9.29295L9.29284 10.7072L6.00033 7.41465L2.70919 10.7063L1.29486 9.29222L4.58611 6.00044L1.29284 2.70716L2.70705 1.29295L6.00021 4.58611L9.29278 1.29301L10.7071 2.70711L7.41442 6.00033Z" fill="#657077"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_19101_130319">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 531 B |
3
packages/asc-web-components/email-chips/svg/Warning.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 -5.24537e-07C2.68629 -8.1423e-07 8.1423e-07 2.68629 5.24537e-07 6C2.34843e-07 9.31371 2.68629 12 6 12C9.31371 12 12 9.31371 12 6C12 2.68629 9.31371 -2.34843e-07 6 -5.24537e-07ZM7 6C7 6.55228 6.55229 7 6 7C5.44772 7 5 6.55228 5 6L5 3C5 2.44771 5.44772 2 6 2C6.55229 2 7 2.44771 7 3L7 6ZM6 10C6.55228 10 7 9.55228 7 9C7 8.44771 6.55229 8 6 8C5.44772 8 5 8.44771 5 9C5 9.55228 5.44772 10 6 10Z" fill="#F21C0E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 564 B |
2
packages/asc-web-components/email-chips/svg/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as WarningIcon } from "./Warning.svg";
|
||||
export { default as DeleteIcon } from "./Delete.svg";
|
@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
|
||||
import PropType from "prop-types";
|
||||
import PropTypes from "prop-types";
|
||||
import Countdown, { zeroPad } from "react-countdown";
|
||||
import StyledSnackBar from "./styled-snackbar";
|
||||
import { StyledAction, StyledSnackBar, StyledIframe } from "./styled-snackbar";
|
||||
import StyledCrossIcon from "./styled-snackbar-action";
|
||||
import StyledLogoIcon from "./styled-snackbar-logo";
|
||||
import Box from "../box";
|
||||
@ -11,6 +11,10 @@ import Heading from "../heading";
|
||||
import Text from "../text";
|
||||
|
||||
class SnackBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { isLoaded: false };
|
||||
}
|
||||
static show(barConfig) {
|
||||
const { parentElementId, ...rest } = barConfig;
|
||||
|
||||
@ -31,14 +35,41 @@ class SnackBar extends React.Component {
|
||||
|
||||
static close() {
|
||||
if (window.snackbar && window.snackbar.parentElementId) {
|
||||
const bar = document.querySelector(`#${window.snackbar.parentElementId}`);
|
||||
bar.remove();
|
||||
const snackbar = document.querySelector("#snackbar-container");
|
||||
snackbar.remove();
|
||||
//ReactDOM.unmountComponentAtNode(window.snackbar.parentElementId);
|
||||
}
|
||||
}
|
||||
|
||||
onActionClick = (e) => {
|
||||
this.props.onAction && this.props.onAction(e);
|
||||
this.props.clickAction && this.props.clickAction(e);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { onLoad } = this.props;
|
||||
onLoad();
|
||||
}
|
||||
|
||||
bannerRenderer = () => {
|
||||
const { htmlContent, sectionWidth } = this.props;
|
||||
return (
|
||||
<div id="bar-banner" style={{ position: "relative" }}>
|
||||
<StyledIframe
|
||||
id="bar-frame"
|
||||
src={htmlContent}
|
||||
scrolling="no"
|
||||
sectionWidth={sectionWidth}
|
||||
onLoad={() => {
|
||||
this.setState({ isLoaded: true });
|
||||
}}
|
||||
></StyledIframe>
|
||||
{this.state.isLoaded && (
|
||||
<StyledAction className="action" onClick={this.onActionClick}>
|
||||
<StyledCrossIcon size="medium" />
|
||||
</StyledAction>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Renderer callback with condition
|
||||
@ -72,73 +103,82 @@ class SnackBar extends React.Component {
|
||||
htmlContent,
|
||||
style,
|
||||
countDownTime,
|
||||
isCampaigns,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const headerStyles = headerText ? {} : { display: "none" };
|
||||
|
||||
const bannerElement = this.bannerRenderer();
|
||||
|
||||
return (
|
||||
<StyledSnackBar style={style} {...rest}>
|
||||
{htmlContent ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: htmlContent,
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
{isCampaigns ? (
|
||||
<>{bannerElement}</>
|
||||
) : (
|
||||
<>
|
||||
{showIcon && (
|
||||
<Box className="logo">
|
||||
<StyledLogoIcon size="medium" color={textColor} />
|
||||
</Box>
|
||||
)}
|
||||
<Box className="text-container" textAlign={textAlign}>
|
||||
<Heading
|
||||
size="xsmall"
|
||||
isInline={true}
|
||||
className="text-header"
|
||||
style={headerStyles}
|
||||
color={textColor}
|
||||
>
|
||||
{headerText}
|
||||
</Heading>
|
||||
<div className="text-body" textAlign={textAlign}>
|
||||
<Text
|
||||
as="p"
|
||||
color={textColor}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
|
||||
{btnText && (
|
||||
<Text
|
||||
<StyledSnackBar id="snackbar-container" style={style} {...rest}>
|
||||
{htmlContent ? (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: htmlContent,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{showIcon && (
|
||||
<Box className="logo">
|
||||
<StyledLogoIcon size="medium" color={textColor} />
|
||||
</Box>
|
||||
)}
|
||||
<Box className="text-container" textalign={textAlign}>
|
||||
<Heading
|
||||
size="xsmall"
|
||||
isInline={true}
|
||||
className="text-header"
|
||||
style={headerStyles}
|
||||
color={textColor}
|
||||
className="button"
|
||||
onClick={this.onActionClick}
|
||||
>
|
||||
{btnText}
|
||||
</Text>
|
||||
)}
|
||||
{headerText}
|
||||
</Heading>
|
||||
<div className="text-body" textalign={textAlign}>
|
||||
<Text
|
||||
as="p"
|
||||
color={textColor}
|
||||
fontSize={fontSize}
|
||||
fontWeight={fontWeight}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
|
||||
{countDownTime > -1 && (
|
||||
<Countdown
|
||||
date={Date.now() + countDownTime}
|
||||
renderer={this.countDownRenderer}
|
||||
onComplete={this.onActionClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
{btnText && (
|
||||
<Text
|
||||
color={textColor}
|
||||
className="button"
|
||||
onClick={this.onActionClick}
|
||||
>
|
||||
{btnText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{countDownTime > -1 && (
|
||||
<Countdown
|
||||
date={Date.now() + countDownTime}
|
||||
renderer={this.countDownRenderer}
|
||||
onComplete={this.onActionClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{!btnText && (
|
||||
<button className="action" onClick={this.onActionClick}>
|
||||
<StyledCrossIcon size="medium" />
|
||||
</button>
|
||||
)}
|
||||
</StyledSnackBar>
|
||||
)}
|
||||
{!btnText && (
|
||||
<button className="action" onClick={this.onActionClick}>
|
||||
<StyledCrossIcon size="medium" />
|
||||
</button>
|
||||
)}
|
||||
</StyledSnackBar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -151,17 +191,21 @@ SnackBar.propTypes = {
|
||||
backgroundColor: PropType.string,
|
||||
textColor: PropType.string,
|
||||
showIcon: PropType.bool,
|
||||
onAction: PropType.func,
|
||||
clickAction: PropType.func,
|
||||
fontSize: PropType.string,
|
||||
fontWeight: PropType.string,
|
||||
textAlign: PropType.string,
|
||||
htmlContent: PropType.string,
|
||||
style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
|
||||
countDownTime: PropType.number,
|
||||
sectionWidth: PropTypes.number,
|
||||
isCampaigns: PropTypes.bool,
|
||||
onLoad: PropTypes.func,
|
||||
isMaintenance: PropTypes.bool,
|
||||
};
|
||||
|
||||
SnackBar.defaultProps = {
|
||||
backgroundColor: "#f8f7bf",
|
||||
backgroundColor: "#F7E6BE",
|
||||
textColor: "#000",
|
||||
showIcon: true,
|
||||
fontSize: "13px",
|
||||
@ -169,6 +213,7 @@ SnackBar.defaultProps = {
|
||||
textAlign: "left",
|
||||
htmlContent: "",
|
||||
countDownTime: -1,
|
||||
isCampaigns: false,
|
||||
};
|
||||
|
||||
export default SnackBar;
|
||||
|
@ -1,5 +1,17 @@
|
||||
import styled from "styled-components";
|
||||
import styled, { css } from "styled-components";
|
||||
import Box from "../box";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
const StyledIframe = styled.iframe`
|
||||
border: none;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
min-width: ${(props) => props.sectionWidth + 40 + "px"};
|
||||
`};
|
||||
`;
|
||||
|
||||
const StyledSnackBar = styled(Box)`
|
||||
transition: all 500ms ease;
|
||||
@ -13,12 +25,11 @@ const StyledSnackBar = styled(Box)`
|
||||
color: white;
|
||||
line-height: 16px;
|
||||
padding: 12px;
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0;
|
||||
opacity: ${(props) => props.opacity || 0};
|
||||
width: 100%;
|
||||
background-color: ${(props) => props.backgroundColor};
|
||||
background-image: url(${(props) => props.backgroundImg || ""});
|
||||
border-radius: 6px;
|
||||
|
||||
.logo {
|
||||
padding-right: 10px;
|
||||
@ -29,7 +40,7 @@ const StyledSnackBar = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
text-align: ${(props) => props.textAlign};
|
||||
text-align: ${(props) => props.textalign};
|
||||
|
||||
.text-header {
|
||||
margin: 0;
|
||||
@ -40,7 +51,7 @@ const StyledSnackBar = styled(Box)`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
justify-content: ${(props) => props.textAlign};
|
||||
justify-content: ${(props) => props.textalign};
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,7 +60,7 @@ const StyledSnackBar = styled(Box)`
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: "#000";
|
||||
color: "#333";
|
||||
margin: 0 0 0 24px;
|
||||
padding: 0;
|
||||
min-width: min-content;
|
||||
@ -70,4 +81,21 @@ const StyledSnackBar = styled(Box)`
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledSnackBar;
|
||||
const StyledAction = styled.div`
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
background: inherit;
|
||||
display: inline-block;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: "#333";
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
${isMobile &&
|
||||
css`
|
||||
right: 14px;
|
||||
`};
|
||||
`;
|
||||
|
||||
export { StyledAction, StyledSnackBar, StyledIframe };
|
||||
|
@ -529,7 +529,7 @@ const Base = {
|
||||
},
|
||||
|
||||
optionButton: {
|
||||
padding: "8px 0px 9px 7px",
|
||||
padding: "8px 9px 9px 7px",
|
||||
},
|
||||
},
|
||||
|
||||
@ -2115,7 +2115,7 @@ const Base = {
|
||||
|
||||
borderImageRight:
|
||||
"linear-gradient(to right, #ffffff 25px,#eceef1 24px)",
|
||||
borderImageLeft: "linear-gradient(to left, #ffffff 20px,#eceef1 24px)",
|
||||
borderImageLeft: "linear-gradient(to left, #ffffff 24px,#eceef1 24px)",
|
||||
|
||||
borderColor: "#ECEEf1",
|
||||
borderColorTransition: "#f3f4f4",
|
||||
|
@ -527,7 +527,7 @@ const Dark = {
|
||||
},
|
||||
|
||||
optionButton: {
|
||||
padding: "8px 0px 9px 7px",
|
||||
padding: "8px 9px 9px 7px",
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -16,6 +16,7 @@ const StyledTooltip = styled.div`
|
||||
pointer-events: ${(props) => props.theme.tooltip.pointerEvents};
|
||||
max-width: ${(props) =>
|
||||
props.maxWidth ? props.maxWidth : props.theme.tooltip.maxWidth};
|
||||
color: ${(props) => props.theme.tooltip.textColor} !important;
|
||||
|
||||
p {
|
||||
color: ${(props) => props.theme.tooltip.textColor} !important;
|
||||
|
62
packages/asc-web-components/utils/banner.js
Normal file
@ -0,0 +1,62 @@
|
||||
import difference from "lodash/difference";
|
||||
import { LANGUAGE } from "@appserver/common/constants";
|
||||
import { getLanguage } from "@appserver/common/utils";
|
||||
export const getBannerAttribute = () => {
|
||||
const bar = document.getElementById("bar-banner");
|
||||
const mainBar = document.getElementById("main-bar");
|
||||
const rects = mainBar ? mainBar.getBoundingClientRect() : null;
|
||||
|
||||
const headerHeight = bar ? 108 + 50 : mainBar ? rects.height + 40 : 48 + 50;
|
||||
const sectionHeaderTop = bar
|
||||
? "108px"
|
||||
: rects
|
||||
? rects.height + 40 + "px"
|
||||
: "48px";
|
||||
const sectionHeaderMarginTop = bar
|
||||
? "106px"
|
||||
: rects
|
||||
? rects.height + 36 + "px"
|
||||
: "46px";
|
||||
|
||||
const loadLanguagePath = async () => {
|
||||
if (!window.firebaseHelper) return;
|
||||
|
||||
const lng = localStorage.getItem(LANGUAGE) || "en";
|
||||
const language = getLanguage(lng instanceof Array ? lng[0] : lng);
|
||||
|
||||
const bar = (localStorage.getItem("bar") || "")
|
||||
.split(",")
|
||||
.filter((bar) => bar.length > 0);
|
||||
|
||||
const closed = JSON.parse(localStorage.getItem("barClose"));
|
||||
|
||||
const banner = difference(bar, closed);
|
||||
|
||||
let index = Number(localStorage.getItem("barIndex") || 0);
|
||||
if (index >= banner.length) {
|
||||
index -= 1;
|
||||
}
|
||||
const currentBar = banner[index];
|
||||
|
||||
let htmlUrl =
|
||||
currentBar && window.firebaseHelper.config.authDomain
|
||||
? `https://${window.firebaseHelper.config.authDomain}/${language}/${currentBar}/index.html`
|
||||
: null;
|
||||
|
||||
if (htmlUrl) {
|
||||
await fetch(htmlUrl).then((data) => {
|
||||
if (data.ok) return;
|
||||
htmlUrl = null;
|
||||
});
|
||||
}
|
||||
|
||||
return [htmlUrl, currentBar];
|
||||
};
|
||||
|
||||
return {
|
||||
headerHeight,
|
||||
sectionHeaderTop,
|
||||
sectionHeaderMarginTop,
|
||||
loadLanguagePath,
|
||||
};
|
||||
};
|
14
packages/asc-web-components/utils/useClickOutside.js
Normal file
@ -0,0 +1,14 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
export const useClickOutside = (ref, handler, ...deps) => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e) => {
|
||||
e.stopPropagation();
|
||||
if (ref.current && !ref.current.contains(e.target)) handler();
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [ref, ...deps]);
|
||||
};
|
@ -37,7 +37,7 @@ const Banner = () => {
|
||||
"en"
|
||||
);
|
||||
}
|
||||
return await (await fetch(translationUrl)).json();
|
||||
return await res.json();
|
||||
};
|
||||
|
||||
const getBanner = async () => {
|
||||
|
@ -0,0 +1,81 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ADS_TIMEOUT } from "../../../../helpers/constants";
|
||||
import SnackBar from "@appserver/components/snackbar";
|
||||
import { Consumer } from "@appserver/components/utils/context";
|
||||
import difference from "lodash/difference";
|
||||
import { getBannerAttribute } from "@appserver/components/utils/banner";
|
||||
|
||||
const bannerHOC = (props) => {
|
||||
const { firstLoad, setMaintenanceExist } = props;
|
||||
|
||||
const [htmlLink, setHtmlLink] = useState();
|
||||
const [campaigns, setCampaigns] = useState();
|
||||
|
||||
const { loadLanguagePath } = getBannerAttribute();
|
||||
|
||||
const bar = (localStorage.getItem("bar") || "")
|
||||
.split(",")
|
||||
.filter((bar) => bar.length > 0);
|
||||
|
||||
const updateBanner = async () => {
|
||||
const closed = JSON.parse(localStorage.getItem("barClose"));
|
||||
const banner = difference(bar, closed);
|
||||
let index = Number(localStorage.getItem("barIndex") || 0);
|
||||
|
||||
if (banner.length < 1 || index + 1 >= banner.length) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
|
||||
try {
|
||||
const [htmlUrl, campaigns] = await loadLanguagePath();
|
||||
setHtmlLink(htmlUrl);
|
||||
setCampaigns(campaigns);
|
||||
} catch (e) {
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
localStorage.setItem("barIndex", index);
|
||||
return;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => updateBanner(), 10000);
|
||||
const updateInterval = setInterval(updateBanner, ADS_TIMEOUT);
|
||||
return () => {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
setMaintenanceExist(false);
|
||||
const closeItems = JSON.parse(localStorage.getItem("barClose")) || [];
|
||||
const closed =
|
||||
closeItems.length > 0 ? [...closeItems, campaigns] : [campaigns];
|
||||
localStorage.setItem("barClose", JSON.stringify(closed));
|
||||
setHtmlLink(null);
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
setMaintenanceExist(true);
|
||||
};
|
||||
|
||||
return htmlLink && !firstLoad ? (
|
||||
<Consumer>
|
||||
{(context) => (
|
||||
<SnackBar
|
||||
sectionWidth={context.sectionWidth}
|
||||
onLoad={onLoad}
|
||||
clickAction={onClose}
|
||||
isCampaigns={true}
|
||||
htmlContent={htmlLink}
|
||||
/>
|
||||
)}
|
||||
</Consumer>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default bannerHOC;
|
@ -396,8 +396,6 @@ class Tile extends React.PureComponent {
|
||||
title: children[0].props.item.title,
|
||||
};
|
||||
|
||||
console.log({ item });
|
||||
|
||||
return (
|
||||
<StyledTile
|
||||
ref={this.tile}
|
||||
|
@ -52,13 +52,13 @@ const StyledContainer = styled.div`
|
||||
|
||||
@media ${tablet} {
|
||||
margin: 0 -16px;
|
||||
width: calc(100% + 28px);
|
||||
width: calc(100% + 32px);
|
||||
}
|
||||
|
||||
${isMobile &&
|
||||
css`
|
||||
margin: 0 -16px;
|
||||
width: calc(100% + 28px);
|
||||
width: calc(100% + 32px);
|
||||
`}
|
||||
|
||||
${isMobileOnly &&
|
||||
|
@ -2,3 +2,4 @@ export { default as SectionHeaderContent } from "./Header";
|
||||
export { default as SectionBodyContent } from "./Body";
|
||||
export { default as SectionFilterContent } from "./Filter";
|
||||
export { default as SectionPagingContent } from "./Paging";
|
||||
export { default as Bar } from "./Bar";
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
SectionFilterContent,
|
||||
SectionHeaderContent,
|
||||
SectionPagingContent,
|
||||
Bar,
|
||||
} from "./Section";
|
||||
|
||||
import { ArticleMainButtonContent } from "../../components/Article";
|
||||
@ -281,6 +282,10 @@ class PureHome extends React.Component {
|
||||
|
||||
dragging,
|
||||
tReady,
|
||||
personal,
|
||||
checkedMaintenance,
|
||||
setMaintenanceExist,
|
||||
snackbarExist,
|
||||
} = this.props;
|
||||
return (
|
||||
<>
|
||||
@ -316,6 +321,16 @@ class PureHome extends React.Component {
|
||||
<SectionHeaderContent />
|
||||
</Section.SectionHeader>
|
||||
|
||||
<Section.SectionBar>
|
||||
{checkedMaintenance && !snackbarExist && (
|
||||
<Bar
|
||||
firstLoad={firstLoad}
|
||||
personal={personal}
|
||||
setMaintenanceExist={setMaintenanceExist}
|
||||
/>
|
||||
)}
|
||||
</Section.SectionBar>
|
||||
|
||||
<Section.SectionFilter>
|
||||
<SectionFilterContent />
|
||||
</Section.SectionFilter>
|
||||
@ -427,6 +442,9 @@ export default inject(
|
||||
isRecycleBinFolder,
|
||||
isPrivacyFolder,
|
||||
isVisitor: auth.userStore.user.isVisitor,
|
||||
checkedMaintenance: auth.settingsStore.checkedMaintenance,
|
||||
setMaintenanceExist: auth.settingsStore.setMaintenanceExist,
|
||||
snackbarExist: auth.settingsStore.snackbarExist,
|
||||
expandedKeys,
|
||||
|
||||
primaryProgressDataVisible,
|
||||
@ -454,6 +472,7 @@ export default inject(
|
||||
startUpload,
|
||||
isHeaderVisible: auth.settingsStore.isHeaderVisible,
|
||||
setHeaderVisible: auth.settingsStore.setHeaderVisible,
|
||||
personal: auth.settingsStore.personal,
|
||||
setToPreviewFile,
|
||||
playlist,
|
||||
isMediaOrImage: settingsStore.isMediaOrImage,
|
||||
|
@ -13,9 +13,10 @@ class ResetApplicationDialogComponent extends React.Component {
|
||||
}
|
||||
|
||||
resetApp = async () => {
|
||||
const { resetTfaApp, history } = this.props;
|
||||
const { resetTfaApp, history, id } = this.props;
|
||||
|
||||
try {
|
||||
const res = await resetTfaApp();
|
||||
const res = await resetTfaApp(id);
|
||||
if (res) history.push(res);
|
||||
} catch (e) {
|
||||
toastr.error(e);
|
||||
@ -69,6 +70,7 @@ ResetApplicationDialog.propTypes = {
|
||||
visible: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
resetTfaApp: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ResetApplicationDialog;
|
||||
|
@ -1,2 +1,3 @@
|
||||
export const GUID_EMPTY = "00000000-0000-0000-0000-000000000000";
|
||||
export const ID_NO_GROUP_MANAGER = "4a515a15-d4d6-4b8e-828e-e0586f18f3a3";
|
||||
export const ADS_TIMEOUT = 300000; // 5 min
|
||||
|
@ -0,0 +1,82 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { ADS_TIMEOUT } from "../../../../helpers/constants";
|
||||
import SnackBar from "@appserver/components/snackbar";
|
||||
import { Consumer } from "@appserver/components/utils/context";
|
||||
import difference from "lodash/difference";
|
||||
import { getBannerAttribute } from "@appserver/components/utils/banner";
|
||||
|
||||
const bannerHOC = (props) => {
|
||||
const { firstLoad, setMaintenanceExist } = props;
|
||||
|
||||
const [htmlLink, setHtmlLink] = useState();
|
||||
const [campaigns, setCampaigns] = useState();
|
||||
|
||||
const { loadLanguagePath } = getBannerAttribute();
|
||||
|
||||
const bar = (localStorage.getItem("bar") || "")
|
||||
.split(",")
|
||||
.filter((bar) => bar.length > 0);
|
||||
|
||||
const updateBanner = async () => {
|
||||
const closed = JSON.parse(localStorage.getItem("barClose"));
|
||||
const banner = difference(bar, closed);
|
||||
let index = Number(localStorage.getItem("barIndex") || 0);
|
||||
|
||||
if (banner.length < 1 || index + 1 >= banner.length) {
|
||||
index = 0;
|
||||
} else {
|
||||
index++;
|
||||
}
|
||||
|
||||
try {
|
||||
const [htmlUrl, campaigns] = await loadLanguagePath();
|
||||
setHtmlLink(htmlUrl);
|
||||
setCampaigns(campaigns);
|
||||
} catch (e) {
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
localStorage.setItem("barIndex", index);
|
||||
return;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => updateBanner(), 10000);
|
||||
const updateInterval = setInterval(updateBanner, ADS_TIMEOUT);
|
||||
|
||||
return () => {
|
||||
if (updateInterval) {
|
||||
clearInterval(updateInterval);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
setMaintenanceExist(false);
|
||||
const closeItems = JSON.parse(localStorage.getItem("barClose")) || [];
|
||||
const closed =
|
||||
closeItems.length > 0 ? [...closeItems, campaigns] : [campaigns];
|
||||
localStorage.setItem("barClose", JSON.stringify(closed));
|
||||
setHtmlLink(null);
|
||||
};
|
||||
|
||||
const onLoad = () => {
|
||||
setMaintenanceExist(true);
|
||||
};
|
||||
|
||||
return htmlLink && !firstLoad ? (
|
||||
<Consumer>
|
||||
{(context) => (
|
||||
<SnackBar
|
||||
sectionWidth={context.sectionWidth}
|
||||
onLoad={onLoad}
|
||||
clickAction={onClose}
|
||||
isCampaigns={true}
|
||||
htmlContent={htmlLink}
|
||||
/>
|
||||
)}
|
||||
</Consumer>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default bannerHOC;
|
@ -2,3 +2,4 @@ export { default as SectionHeaderContent } from "./Header";
|
||||
export { default as SectionBodyContent } from "./Body";
|
||||
export { default as SectionFilterContent } from "./Filter";
|
||||
export { default as SectionPagingContent } from "./Paging";
|
||||
export { default as Bar } from "./Bar";
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
SectionBodyContent,
|
||||
SectionFilterContent,
|
||||
SectionPagingContent,
|
||||
Bar,
|
||||
} from "./Section";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { isMobile } from "react-device-detect";
|
||||
@ -29,6 +30,9 @@ const PureHome = ({
|
||||
firstLoad,
|
||||
setFirstLoad,
|
||||
viewAs,
|
||||
checkedMaintenance,
|
||||
snackbarExist,
|
||||
setMaintenanceExist,
|
||||
}) => {
|
||||
const { location } = history;
|
||||
const { pathname } = location;
|
||||
@ -72,6 +76,11 @@ const PureHome = ({
|
||||
<Section.SectionHeader>
|
||||
<SectionHeaderContent />
|
||||
</Section.SectionHeader>
|
||||
<Section.SectionBar>
|
||||
{checkedMaintenance && !snackbarExist && (
|
||||
<Bar setMaintenanceExist={setMaintenanceExist} />
|
||||
)}
|
||||
</Section.SectionBar>
|
||||
<Section.SectionFilter>
|
||||
<SectionFilterContent />
|
||||
</Section.SectionFilter>
|
||||
@ -119,5 +128,8 @@ export default inject(({ auth, peopleStore }) => {
|
||||
firstLoad,
|
||||
setFirstLoad,
|
||||
viewAs,
|
||||
checkedMaintenance: auth.settingsStore.checkedMaintenance,
|
||||
setMaintenanceExist: auth.settingsStore.setMaintenanceExist,
|
||||
snackbarExist: auth.settingsStore.snackbarExist,
|
||||
};
|
||||
})(observer(withRouter(Home)));
|
||||
|
@ -514,6 +514,7 @@ class SectionBodyContent extends React.PureComponent {
|
||||
visible={resetAppDialogVisible}
|
||||
onClose={this.toggleResetAppDialogVisible}
|
||||
resetTfaApp={this.props.resetTfaApp}
|
||||
id={profile.id}
|
||||
/>
|
||||
)}
|
||||
{backupCodesDialogVisible && (
|
||||
|
3
web/ASC.Web.Campaigns/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"defaultLanguage": "en"
|
||||
}
|
45
web/ASC.Web.Campaigns/gatsby-config.js
Normal file
@ -0,0 +1,45 @@
|
||||
const languages = require("./languages.json");
|
||||
|
||||
const availableLanguages = languages.map((el) => el.shortKey);
|
||||
|
||||
const {
|
||||
defaultLanguage,
|
||||
} = require("./config.json");
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{
|
||||
resolve: `gatsby-source-filesystem`,
|
||||
options: {
|
||||
path: `${__dirname}/src/locales`,
|
||||
name: `locale`
|
||||
}
|
||||
},
|
||||
{
|
||||
resolve: `gatsby-plugin-react-i18next`,
|
||||
options: {
|
||||
localeJsonSourceName: `locale`,
|
||||
languages: availableLanguages,
|
||||
defaultLanguage,
|
||||
redirect: false,
|
||||
generateDefaultLanguagePage: `/en`,
|
||||
|
||||
i18nextOptions: {
|
||||
fallbackLng: defaultLanguage,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
keySeparator: false,
|
||||
nsSeparator: false
|
||||
},
|
||||
pages: [
|
||||
{
|
||||
matchPath: '/preview',
|
||||
languages: [""],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
'gatsby-plugin-no-javascript'
|
||||
],
|
||||
}
|
9
web/ASC.Web.Campaigns/gatsby-node.js
Normal file
@ -0,0 +1,9 @@
|
||||
exports.createPages = async ({ actions }) => {
|
||||
const { createPage } = actions
|
||||
createPage({
|
||||
path: "/using-dsg",
|
||||
component: require.resolve("./src/templates/using-dsg.js"),
|
||||
context: {},
|
||||
defer: true,
|
||||
})
|
||||
}
|
137
web/ASC.Web.Campaigns/languages.json
Normal file
@ -0,0 +1,137 @@
|
||||
[
|
||||
{
|
||||
"key": "az-Latn-AZ",
|
||||
"shortKey": "az",
|
||||
"iconName": "az.svg"
|
||||
},
|
||||
{
|
||||
"key": "bg-BG",
|
||||
"shortKey": "bg",
|
||||
"iconName": "bg.svg"
|
||||
},
|
||||
{
|
||||
"key": "zh-CN",
|
||||
"shortKey": "zh",
|
||||
"iconName": "zh.svg"
|
||||
},
|
||||
{
|
||||
"key": "cs-CZ",
|
||||
"shortKey": "cs",
|
||||
"iconName": "cs.svg"
|
||||
},
|
||||
{
|
||||
"key": "nl-NL",
|
||||
"shortKey": "nl",
|
||||
"iconName": "nl.svg"
|
||||
},
|
||||
{
|
||||
"key": "en-GB",
|
||||
"shortKey": "en-GB",
|
||||
"iconName": "en-GB.svg"
|
||||
},
|
||||
{
|
||||
"key": "en-US",
|
||||
"shortKey": "en",
|
||||
"iconName": "en.svg"
|
||||
},
|
||||
{
|
||||
"key": "fi-FI",
|
||||
"shortKey": "fi",
|
||||
"iconName": "fi.svg"
|
||||
},
|
||||
{
|
||||
"key": "fr-FR",
|
||||
"shortKey": "fr",
|
||||
"iconName": "fr.svg"
|
||||
},
|
||||
{
|
||||
"key": "de-DE",
|
||||
"shortKey": "de",
|
||||
"iconName": "de.svg"
|
||||
},
|
||||
{
|
||||
"key": "de-CH",
|
||||
"shortKey": "de-CH",
|
||||
"iconName": "de-CH.svg"
|
||||
},
|
||||
{
|
||||
"key": "el-GR",
|
||||
"shortKey": "el",
|
||||
"iconName": "el.svg"
|
||||
},
|
||||
{
|
||||
"key": "it-IT",
|
||||
"shortKey": "it",
|
||||
"iconName": "it.svg"
|
||||
},
|
||||
{
|
||||
"key": "ja-JP",
|
||||
"shortKey": "ja",
|
||||
"iconName": "ja.svg"
|
||||
},
|
||||
{
|
||||
"key": "ko-KR",
|
||||
"shortKey": "ko",
|
||||
"iconName": "ko.svg"
|
||||
},
|
||||
{
|
||||
"key": "lv-LV",
|
||||
"shortKey": "lv",
|
||||
"iconName": "lv.svg"
|
||||
},
|
||||
{
|
||||
"key": "pl-PL",
|
||||
"shortKey": "pl",
|
||||
"iconName": "pl.svg"
|
||||
},
|
||||
{
|
||||
"key": "pt-BR",
|
||||
"shortKey": "pt-BR",
|
||||
"iconName": "pt-BR.svg"
|
||||
},
|
||||
{
|
||||
"key": "pt-PT",
|
||||
"shortKey": "pt",
|
||||
"iconName": "pt.svg"
|
||||
},
|
||||
{
|
||||
"key": "ru-RU",
|
||||
"shortKey": "ru",
|
||||
"iconName": "ru.svg"
|
||||
},
|
||||
{
|
||||
"key": "sk-SK",
|
||||
"shortKey": "sk",
|
||||
"iconName": "sk.svg"
|
||||
},
|
||||
{
|
||||
"key": "sl-SI",
|
||||
"shortKey": "sl",
|
||||
"iconName": "sl.svg"
|
||||
},
|
||||
{
|
||||
"key": "es-MX",
|
||||
"shortKey": "es-MX",
|
||||
"iconName": "es-MX.svg"
|
||||
},
|
||||
{
|
||||
"key": "es-ES",
|
||||
"shortKey": "es",
|
||||
"iconName": "es.svg"
|
||||
},
|
||||
{
|
||||
"key": "tr-TR",
|
||||
"shortKey": "tr",
|
||||
"iconName": "tr.svg"
|
||||
},
|
||||
{
|
||||
"key": "uk-UA",
|
||||
"shortKey": "uk",
|
||||
"iconName": "uk.svg"
|
||||
},
|
||||
{
|
||||
"key": "vi-VN",
|
||||
"shortKey": "vi",
|
||||
"iconName": "vi.svg"
|
||||
}
|
||||
]
|
@ -1,12 +1,39 @@
|
||||
{
|
||||
"name": "@appserver/campaigns",
|
||||
"version": "0.1.0",
|
||||
"name": "gatsby-starter-default",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"firebase-tools": "^10.2.0",
|
||||
"gatsby": "^4.4.0",
|
||||
"gatsby-plugin-gatsby-cloud": "^4.4.0",
|
||||
"gatsby-plugin-image": "^2.4.0",
|
||||
"gatsby-plugin-manifest": "^4.4.0",
|
||||
"gatsby-plugin-no-javascript": "^2.0.5",
|
||||
"gatsby-plugin-offline": "^5.4.0",
|
||||
"gatsby-plugin-react-helmet": "^5.4.0",
|
||||
"gatsby-plugin-react-i18next": "^1.2.2",
|
||||
"gatsby-plugin-sharp": "^4.4.0",
|
||||
"gatsby-source-filesystem": "^4.4.0",
|
||||
"gatsby-transformer-sharp": "^4.4.0",
|
||||
"i18next": "^21.6.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-i18next": "^11.15.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gatsby build",
|
||||
"develop": "gatsby develop",
|
||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
|
||||
"start": "gatsby develop",
|
||||
"serve": "gatsby serve",
|
||||
"clean": "gatsby clean",
|
||||
"test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",
|
||||
"firebase:login": "firebase login",
|
||||
"firebase:deploy": "firebase deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"firebase-tools": "^10.2.0"
|
||||
}
|
||||
}
|
||||
|
4
web/ASC.Web.Campaigns/src/locales/en/NewYear.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Title":"ONLYOFFICE Advent Calendar",
|
||||
"Text": "Spend 24 days of Christmas with ONLYOFFICE. Get <1>new gifts and discounts</1> each day - up to 99% off!"
|
||||
}
|
4
web/ASC.Web.Campaigns/src/locales/ru/NewYear.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"Title":"Адвент-календарь ONLYOFFICE",
|
||||
"Text": "Готовьтесь к Новому году с ONLYOFFICE и получите <1>ежедневные подарки и скидки</1> каждый день - до минус 99%!"
|
||||
}
|
163
web/ASC.Web.Campaigns/src/pages/NewYear/images/santa.svg
Normal file
After Width: | Height: | Size: 285 KiB |
28
web/ASC.Web.Campaigns/src/pages/NewYear/index.css
Normal file
@ -0,0 +1,28 @@
|
||||
div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.text {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 476px) {
|
||||
.text {
|
||||
font-size: 12px;
|
||||
}
|
||||
.content-box {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
53
web/ASC.Web.Campaigns/src/pages/NewYear/index.js
Normal file
@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import { graphql } from "gatsby";
|
||||
import {Trans, useTranslation} from 'gatsby-plugin-react-i18next';
|
||||
import logo from "./images/santa.svg"
|
||||
import '../../styles/base.css';
|
||||
import "./index.css";
|
||||
|
||||
const IndexPage = () => {
|
||||
|
||||
const {t, i18n: { language }} = useTranslation("NewYear");
|
||||
|
||||
const origin = "https://www.onlyoffice.com";
|
||||
const route = "advent-calendar.aspx"
|
||||
|
||||
const LinkHref =`${origin}/${language === "en" ? route : `${language}/${route}`}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="wrapper" style={{display:"flex", margin: "0 auto", backgroundColor: "#266281", minHeight: "60px"}}>
|
||||
<img src={logo} width={60}/>
|
||||
<div className="content-box" style={{backgroundColor:"#266281", color: "#fff", display: "flex", justifyContent: "center", flexDirection: "column", margin: "0 auto", fontSize: "18px"}}>
|
||||
<p>
|
||||
{t("Title")}
|
||||
</p>
|
||||
<p className="text" style={{padding: "0", margin: "0", paddingLeft: "10px", paddingRight: "10px"}}>
|
||||
<Trans i18nKey="Text">Get <a target="_blank" style={{color: "#fc9f06"}}
|
||||
href={LinkHref}>new gifts and discounts</a>each day - up to 99% off!</Trans>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
export default IndexPage
|
||||
|
||||
|
||||
export const query = graphql`
|
||||
query ($language: String!) {
|
||||
locales: allLocale(filter: {language: {eq: $language}}) {
|
||||
edges {
|
||||
node {
|
||||
ns
|
||||
data
|
||||
language
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
6
web/ASC.Web.Campaigns/src/pages/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
export default function Component () {
|
||||
return "";
|
||||
}
|
5
web/ASC.Web.Campaigns/src/styles/base.css
Normal file
@ -0,0 +1,5 @@
|
||||
p,
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
20
web/ASC.Web.Campaigns/src/templates/using-dsg.js
Normal file
@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
import { Link } from "gatsby"
|
||||
|
||||
|
||||
const UsingDSG = () => (
|
||||
<div>
|
||||
<h1>Hello from a DSG Page</h1>
|
||||
<p>This page is not created until requested by a user.</p>
|
||||
<p>
|
||||
To learn more, head over to our{" "}
|
||||
<a href="https://www.gatsbyjs.com/docs/reference/rendering-options/deferred-static-generation/">
|
||||
documentation about Deferred Static Generation
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<Link to="/">Go back to the homepage</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default UsingDSG
|
@ -2,6 +2,7 @@
|
||||
"ChangePasswordSuccess": "Password has been successfully changed",
|
||||
"ConfirmOwnerPortalSuccessMessage": "Portal owner has been successfully changed. In 10 seconds you will be redirected",
|
||||
"ConfirmOwnerPortalTitle": "Please confirm that you want to change portal owner to {{newOwner}}",
|
||||
"CurrentNumber": "You current mobile phone number",
|
||||
"DeleteProfileBtn": "Delete my account",
|
||||
"DeleteProfileConfirmation": "Attention! You are about to delete your account.",
|
||||
"DeleteProfileConfirmationInfo": "By clicking \"Delete my account\" you agree with our Privacy policy.",
|
||||
@ -11,11 +12,14 @@
|
||||
"EnterAppCodeDescription": "Enter 6-digit generated code from your app. If you don't have access to your phone, use the backup codes.",
|
||||
"EnterAppCodeTitle": "Enter code from authentication app",
|
||||
"EnterCodePlaceholder": "Enter code",
|
||||
"EnterPhone": "Enter mobile phone number",
|
||||
"FirstName": "First name",
|
||||
"GetCode": "Get code",
|
||||
"InviteTitle": "You are invited to join this portal!",
|
||||
"LoginRegistryButton": "Join",
|
||||
"LoginWithAccount": "or log in with:",
|
||||
"PassworResetTitle": "Now you can create a new password.",
|
||||
"PhoneSubtitle": "The two-factor authentication is enabled to provide additional portal security. Enter your mobile phone number to continue work on the portal. Mobile phone number must be entered using an international format with country code.",
|
||||
"SetAppButton": "Connect app",
|
||||
"SetAppDescription": "Two-factor authentication is enabled. Configure your authenticator app to continue work on the portal. You can use Google Authenticator for <1>Android</1> and <4>iOS</4> or Authenticator for <8>Windows Phone</8>.",
|
||||
"SetAppInstallDescription": "To connect the app, scan the QR code or manually enter your secret key <1>{{ secretKey }}</1>, and then enter a 6-digit code from your app in the field below.",
|
||||
|
@ -2,6 +2,7 @@
|
||||
"ChangePasswordSuccess": "Пароль был успешно изменен",
|
||||
"ConfirmOwnerPortalSuccessMessage": "Владелец портала успешно изменен. Через 10 секунд Вы будете перенаправлены.",
|
||||
"ConfirmOwnerPortalTitle": "Пожалуйста, подтвердите, что Вы хотите изменить владельца портала на {{newOwner}}",
|
||||
"CurrentNumber": "Ваш текущий номер мобильного телефона",
|
||||
"DeleteProfileBtn": "Удалить мой аккаунт",
|
||||
"DeleteProfileConfirmation": "Внимание! Вы собираетесь удалить свою учетную запись.",
|
||||
"DeleteProfileConfirmationInfo": "Нажимая \"Удалить мою учетную запись\", вы соглашаетесь с нашей Политикой конфиденциальности.",
|
||||
@ -11,11 +12,14 @@
|
||||
"EnterAppCodeDescription": "Введите 6-значный код, сгенерированный приложением. Если у вас нет доступа к телефону, используйте резервные коды.",
|
||||
"EnterAppCodeTitle": "Введите код из приложения для аутентификации",
|
||||
"EnterCodePlaceholder": "Введите код",
|
||||
"EnterPhone": "Введите номер мобильного телефона",
|
||||
"FirstName": "Имя",
|
||||
"GetCode": "Получить код",
|
||||
"InviteTitle": "Вы приглашены присоединиться к этому порталу!",
|
||||
"LoginRegistryButton": "Присоединиться",
|
||||
"LoginWithAccount": "или войдите с:",
|
||||
"PassworResetTitle": "Теперь вы можете создать новый пароль.",
|
||||
"PhoneSubtitle": "Двухфакторная аутентификация включена для обеспечения дополнительной безопасности портала. Введите номер мобильного телефона, чтобы продолжить работу на портале. Номер мобильного телефона должен быть введен в международном формате с кодом страны.",
|
||||
"SetAppButton": "Подключить приложение",
|
||||
"SetAppDescription": "Включена двухфакторная аутентификация. Чтобы продолжить работу на портале, настройте приложение для аутентификации. Вы можете использовать Google Authenticator для <1>Android</1> и <4>iOS</4> или Authenticator для <8>Windows Phone</8>.",
|
||||
"SetAppInstallDescription": "Для подключения приложения отсканируйте QR-код или вручную введите секретный ключ <1>{{ secretKey }}</1>, а затем введите 6-значный код из приложения в поле ниже.",
|
||||
|
@ -199,10 +199,13 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
language,
|
||||
FirebaseHelper,
|
||||
personal,
|
||||
setCheckedMaintenance,
|
||||
socketHelper,
|
||||
setPreparationPortalDialogVisible,
|
||||
setTheme,
|
||||
setMaintenanceExist,
|
||||
roomsMode,
|
||||
setSnackbarExist,
|
||||
} = rest;
|
||||
|
||||
useEffect(() => {
|
||||
@ -263,6 +266,8 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
const showSnackBar = (campaign) => {
|
||||
clearSnackBarTimer();
|
||||
|
||||
let skipMaintenance;
|
||||
|
||||
const { fromDate, toDate, desktop } = campaign;
|
||||
|
||||
console.log(
|
||||
@ -271,16 +276,17 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
|
||||
if (!campaign || !fromDate || !toDate) {
|
||||
console.log("Skip snackBar by empty campaign params");
|
||||
return;
|
||||
skipMaintenance = true;
|
||||
}
|
||||
|
||||
const to = moment(toDate).local();
|
||||
|
||||
const watchedCampaignDateStr = localStorage.getItem(LS_CAMPAIGN_DATE);
|
||||
|
||||
const campaignDateStr = to.format(DATE_FORMAT);
|
||||
if (campaignDateStr == watchedCampaignDateStr) {
|
||||
console.log("Skip snackBar by already watched");
|
||||
return;
|
||||
skipMaintenance = true;
|
||||
}
|
||||
|
||||
const from = moment(fromDate).local();
|
||||
@ -291,18 +297,23 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
|
||||
Snackbar.close();
|
||||
console.log(`Show snackBar has been delayed for 1 minute`, now);
|
||||
return;
|
||||
skipMaintenance = true;
|
||||
}
|
||||
|
||||
if (now.isAfter(to)) {
|
||||
console.log("Skip snackBar by current date", now);
|
||||
Snackbar.close();
|
||||
return;
|
||||
skipMaintenance = true;
|
||||
}
|
||||
|
||||
if (isDesktop && !desktop) {
|
||||
console.log("Skip snackBar by desktop", desktop);
|
||||
Snackbar.close();
|
||||
skipMaintenance = true;
|
||||
}
|
||||
|
||||
if (skipMaintenance) {
|
||||
setCheckedMaintenance(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -311,12 +322,11 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
if (!document.getElementById("main-bar")) return;
|
||||
|
||||
const campaignStr = JSON.stringify(campaign);
|
||||
let skipRender = lastCampaignStr === campaignStr;
|
||||
// let skipRender = lastCampaignStr === campaignStr;
|
||||
|
||||
skipRender =
|
||||
skipRender && document.getElementById("main-bar").hasChildNodes();
|
||||
const hasChild = document.getElementById("main-bar").hasChildNodes();
|
||||
|
||||
if (skipRender) return;
|
||||
if (hasChild) return;
|
||||
|
||||
lastCampaignStr = campaignStr;
|
||||
|
||||
@ -324,17 +334,23 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
|
||||
const barConfig = {
|
||||
parentElementId: "main-bar",
|
||||
headerText: "Atention",
|
||||
text: `${t("BarMaintenanceDescription", {
|
||||
targetDate: targetDate,
|
||||
productName: "ONLYOFFICE Personal",
|
||||
})} ${t("BarMaintenanceDisclaimer")}`,
|
||||
onAction: () => {
|
||||
isMaintenance: true,
|
||||
clickAction: () => {
|
||||
setMaintenanceExist(false);
|
||||
setSnackbarExist(false);
|
||||
Snackbar.close();
|
||||
localStorage.setItem(LS_CAMPAIGN_DATE, to.format(DATE_FORMAT));
|
||||
},
|
||||
opacity: 1,
|
||||
style: {
|
||||
marginTop: "10px",
|
||||
onLoad: () => {
|
||||
setCheckedMaintenance(true);
|
||||
setSnackbarExist(true);
|
||||
setMaintenanceExist(true);
|
||||
},
|
||||
};
|
||||
|
||||
@ -349,12 +365,13 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
.then((campaign) => {
|
||||
console.log("checkMaintenance", campaign);
|
||||
if (!campaign) {
|
||||
setCheckedMaintenance(true);
|
||||
clearSnackBarTimer();
|
||||
Snackbar.close();
|
||||
return;
|
||||
}
|
||||
|
||||
showSnackBar(campaign);
|
||||
setTimeout(() => showSnackBar(campaign), 10000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
@ -367,6 +384,14 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
const fetchBanners = () => {
|
||||
if (!FirebaseHelper.isEnabled) return;
|
||||
|
||||
FirebaseHelper.checkBar()
|
||||
.then((bar) => {
|
||||
localStorage.setItem("bar", bar);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
FirebaseHelper.checkCampaigns()
|
||||
.then((campaigns) => {
|
||||
localStorage.setItem("campaigns", campaigns);
|
||||
@ -382,6 +407,7 @@ const Shell = ({ items = [], page = "home", ...rest }) => {
|
||||
updateTempContent();
|
||||
|
||||
if (!FirebaseHelper.isEnabled) {
|
||||
setCheckedMaintenance(true);
|
||||
localStorage.setItem("campaigns", "");
|
||||
return;
|
||||
}
|
||||
@ -545,6 +571,9 @@ const ShellWrapper = inject(({ auth, backup }) => {
|
||||
isDesktopClient,
|
||||
firebaseHelper,
|
||||
setModuleInfo,
|
||||
setCheckedMaintenance,
|
||||
setMaintenanceExist,
|
||||
setSnackbarExist,
|
||||
socketHelper,
|
||||
setTheme,
|
||||
} = settingsStore;
|
||||
@ -566,10 +595,13 @@ const ShellWrapper = inject(({ auth, backup }) => {
|
||||
isDesktop: isDesktopClient,
|
||||
FirebaseHelper: firebaseHelper,
|
||||
personal,
|
||||
setCheckedMaintenance,
|
||||
setMaintenanceExist,
|
||||
socketHelper,
|
||||
setPreparationPortalDialogVisible,
|
||||
setTheme,
|
||||
roomsMode,
|
||||
setSnackbarExist,
|
||||
};
|
||||
})(observer(Shell));
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { Component, createRef } from "react";
|
||||
import { isTouchDevice } from "@appserver/components/utils/device";
|
||||
import Scrollbar from "@appserver/components/scrollbar";
|
||||
import { LayoutContextProvider } from "./context";
|
||||
import { getBannerAttribute } from "@appserver/components/utils/banner";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
isTablet,
|
||||
@ -48,6 +49,7 @@ class MobileLayout extends Component {
|
||||
|
||||
scrolledTheVerticalAxis = () => {
|
||||
const { prevScrollPosition, visibleContent } = this.state;
|
||||
const { headerHeight } = getBannerAttribute();
|
||||
|
||||
const currentScrollPosition =
|
||||
this.customScrollElm.scrollTop > 0 ? this.customScrollElm.scrollTop : 0;
|
||||
@ -68,7 +70,18 @@ class MobileLayout extends Component {
|
||||
if (
|
||||
(isSafari || isIOS) &&
|
||||
this.customScrollElm.scrollHeight - this.customScrollElm.clientHeight <
|
||||
112
|
||||
headerHeight
|
||||
) {
|
||||
if (!this.state.visibleContent)
|
||||
this.setState({
|
||||
visibleContent: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevScrollPosition - currentScrollPosition > 0 &&
|
||||
currentScrollPosition < headerHeight
|
||||
) {
|
||||
if (!this.state.visibleContent)
|
||||
this.setState({
|
||||
@ -79,7 +92,7 @@ class MobileLayout extends Component {
|
||||
|
||||
if (
|
||||
(isSafari || isIOS) &&
|
||||
Math.abs(currentScrollPosition - prevScrollPosition) <= 112 &&
|
||||
Math.abs(currentScrollPosition - prevScrollPosition) <= headerHeight &&
|
||||
currentScrollPosition === 0
|
||||
) {
|
||||
if (!this.state.visibleContent)
|
||||
@ -89,7 +102,7 @@ class MobileLayout extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Math.abs(currentScrollPosition - prevScrollPosition) <= 112) {
|
||||
if (Math.abs(currentScrollPosition - prevScrollPosition) <= headerHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
import MobileLayout from "./MobileLayout";
|
||||
import { size, isSmallTablet } from "@appserver/components/utils/device";
|
||||
import { size } from "@appserver/components/utils/device";
|
||||
import {
|
||||
isIOS,
|
||||
isFirefox,
|
||||
@ -27,29 +27,6 @@ const StyledContainer = styled.div`
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
}
|
||||
#articleScrollBar {
|
||||
> .scroll-body {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
position: ${isMobileOnly && !isSmallTablet() && "absolute"} !important;
|
||||
|
||||
${isMobileOnly &&
|
||||
!isSmallTablet() &&
|
||||
css`
|
||||
overflow-y: hidden !important;
|
||||
overflow-x: hidden !important;
|
||||
width: 192px;
|
||||
`}
|
||||
}
|
||||
|
||||
.nav-thumb-horizontal {
|
||||
${(props) =>
|
||||
props.isTabletView &&
|
||||
css`
|
||||
height: 0 !important;
|
||||
`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Layout = (props) => {
|
||||
|
@ -5,7 +5,7 @@ import { isIOS, isFirefox, isMobile, isMobileOnly } from "react-device-detect";
|
||||
const StyledMain = styled.main`
|
||||
height: ${(props) =>
|
||||
isIOS && !isFirefox
|
||||
? "calc(var(--vh, 1vh) * 100)"
|
||||
? "calc(100vh - 48px)"
|
||||
: props.isDesktop
|
||||
? "100vh"
|
||||
: "calc(100vh - 48px)"};
|
||||
|
@ -0,0 +1,84 @@
|
||||
import styled from "styled-components";
|
||||
import { mobile, tablet } from "@appserver/components/utils/device";
|
||||
|
||||
export const StyledPage = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 56px auto 0 auto;
|
||||
max-width: 960px;
|
||||
|
||||
@media ${tablet} {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
@media ${mobile} {
|
||||
margin-top: 72px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledHeader = styled.div`
|
||||
text-align: left;
|
||||
|
||||
.title {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledBody = styled.div`
|
||||
width: 320px;
|
||||
|
||||
@media ${tablet} {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media ${mobile} {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.password-field-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.confirm-button {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.password-change-form {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.confirm-subtitle {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-delete {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.phone-input {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1fr 1fr;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
@ -1,153 +1,112 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
import axios from "axios";
|
||||
import Text from "@appserver/components/text";
|
||||
import TextInput from "@appserver/components/text-input";
|
||||
import PasswordInput from "@appserver/components/password-input";
|
||||
import Button from "@appserver/components/button";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import FieldContainer from "@appserver/components/field-container";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { EmployeeActivationStatus } from "@appserver/common/constants";
|
||||
import {
|
||||
changePassword,
|
||||
updateActivationStatus,
|
||||
updateUser,
|
||||
} from "@appserver/common/api/people";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Button from "@appserver/components/button";
|
||||
import TextInput from "@appserver/components/text-input";
|
||||
import Text from "@appserver/components/text";
|
||||
import PasswordInput from "@appserver/components/password-input";
|
||||
import { createPasswordHash } from "@appserver/common/utils";
|
||||
import toastr from "@appserver/components/toast/toastr";
|
||||
import Loader from "@appserver/components/loader";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import {
|
||||
AppServerConfig,
|
||||
EmployeeActivationStatus,
|
||||
PasswordLimitSpecialCharacters,
|
||||
} from "@appserver/common/constants";
|
||||
import { combineUrl, createPasswordHash } from "@appserver/common/utils";
|
||||
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
|
||||
import withLoader from "../withLoader";
|
||||
import { getPasswordErrorMessage } from "../../../../helpers/utils";
|
||||
|
||||
const inputWidth = "320px";
|
||||
const ActivateUserForm = (props) => {
|
||||
const {
|
||||
t,
|
||||
greetingTitle,
|
||||
settings,
|
||||
linkData,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
login,
|
||||
} = props;
|
||||
|
||||
const ConfirmContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-left: 200px;
|
||||
const [name, setName] = useState(linkData.firstname);
|
||||
const [nameValid, setNameValid] = useState(true);
|
||||
const [surName, setSurName] = useState(linkData.lastname);
|
||||
const [surNameValid, setSurNameValid] = useState(true);
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordValid, setPasswordValid] = useState(true);
|
||||
const [isPasswordErrorShow, setIsPasswordErrorShow] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@media (max-width: 830px) {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.start-basis {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.margin-left {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: ${inputWidth};
|
||||
}
|
||||
|
||||
.confirm-row {
|
||||
margin: 23px 0 0;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const emailInputName = "email";
|
||||
|
||||
class Confirm extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
email: props.linkData.email,
|
||||
firstName: props.linkData.firstname,
|
||||
firstNameValid: true,
|
||||
lastName: props.linkData.lastname,
|
||||
lastNameValid: true,
|
||||
password: "",
|
||||
passwordValid: true,
|
||||
errorText: "",
|
||||
isLoading: false,
|
||||
passwordEmpty: false,
|
||||
key: props.linkData.confirmHeader,
|
||||
linkType: props.linkData.type,
|
||||
userId: props.linkData.uid,
|
||||
};
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
this.setState({ isLoading: true }, function () {
|
||||
const { hashSettings, defaultPage } = this.props;
|
||||
|
||||
this.setState({ errorText: "" });
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (!this.state.firstName.trim()) {
|
||||
hasError = true;
|
||||
this.setState({ firstNameValid: !hasError });
|
||||
}
|
||||
|
||||
if (!this.state.lastName.trim()) {
|
||||
hasError = true;
|
||||
this.setState({ lastNameValid: !hasError });
|
||||
}
|
||||
|
||||
if (!this.state.passwordValid) {
|
||||
hasError = true;
|
||||
this.setState({ passwordValid: !hasError });
|
||||
}
|
||||
|
||||
if (!this.state.password.trim()) {
|
||||
this.setState({ passwordEmpty: true });
|
||||
hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
this.setState({ isLoading: false });
|
||||
return false;
|
||||
}
|
||||
|
||||
const hash = createPasswordHash(this.state.password, hashSettings);
|
||||
|
||||
const loginData = {
|
||||
userName: this.state.email,
|
||||
passwordHash: hash,
|
||||
};
|
||||
|
||||
const personalData = {
|
||||
firstname: this.state.firstName,
|
||||
lastname: this.state.lastName,
|
||||
};
|
||||
|
||||
this.activateConfirmUser(
|
||||
personalData,
|
||||
loginData,
|
||||
this.state.key,
|
||||
this.state.userId,
|
||||
EmployeeActivationStatus.Activated
|
||||
)
|
||||
.then(() => window.location.replace(defaultPage))
|
||||
.catch((error) => {
|
||||
console.error("activate error", error);
|
||||
this.setState({
|
||||
errorText: error,
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
const onChangeName = (e) => {
|
||||
setName(e.target.value);
|
||||
setNameValid(true);
|
||||
};
|
||||
|
||||
activateConfirmUser = async (
|
||||
const onChangeSurName = (e) => {
|
||||
setSurName(e.target.value);
|
||||
setSurNameValid(true);
|
||||
};
|
||||
|
||||
const onChangePassword = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const onValidatePassword = (res) => {
|
||||
setPasswordValid(res);
|
||||
};
|
||||
|
||||
const onBlurPassword = () => {
|
||||
setIsPasswordErrorShow(true);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
setIsLoading(true);
|
||||
if (!name.trim()) setNameValid(false);
|
||||
if (!surName.trim()) setSurNameValid(false);
|
||||
if (!password.trim()) {
|
||||
setPasswordValid(false);
|
||||
setIsPasswordErrorShow(true);
|
||||
}
|
||||
|
||||
if (!nameValid || !surNameValid || !password.trim() || !passwordValid) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = createPasswordHash(password, hashSettings);
|
||||
|
||||
const loginData = {
|
||||
userName: linkData.email,
|
||||
passwordHash: hash,
|
||||
};
|
||||
|
||||
const personalData = {
|
||||
firstname: name,
|
||||
lastname: surName,
|
||||
};
|
||||
|
||||
activateConfirmUser(
|
||||
personalData,
|
||||
loginData,
|
||||
linkData.confirmHeader,
|
||||
linkData.uid,
|
||||
EmployeeActivationStatus.Activated
|
||||
)
|
||||
.then(() => {
|
||||
setIsLoading(false);
|
||||
window.location.replace(defaultPage);
|
||||
})
|
||||
.catch((error) => {
|
||||
//console.error(error);
|
||||
setIsLoading(false);
|
||||
toastr.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const activateConfirmUser = async (
|
||||
personalData,
|
||||
loginData,
|
||||
key,
|
||||
@ -160,224 +119,137 @@ class Confirm extends React.PureComponent {
|
||||
LastName: personalData.lastname,
|
||||
};
|
||||
|
||||
const res1 = await changePassword(userId, loginData.passwordHash, key);
|
||||
|
||||
console.log("changePassword", res1);
|
||||
|
||||
const res2 = await updateActivationStatus(activationStatus, userId, key);
|
||||
|
||||
console.log("updateActivationStatus", res2);
|
||||
|
||||
const { login } = this.props;
|
||||
const { userName, passwordHash } = loginData;
|
||||
|
||||
const res1 = await changePassword(userId, loginData.passwordHash, key);
|
||||
const res2 = await updateActivationStatus(activationStatus, userId, key);
|
||||
const res3 = await login(userName, passwordHash);
|
||||
|
||||
console.log("Login", res3);
|
||||
|
||||
const res4 = await updateUser(changedData);
|
||||
|
||||
console.log("updateUser", res4);
|
||||
};
|
||||
|
||||
onKeyPress = (event) => {
|
||||
const onKeyPress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
this.onSubmit();
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
onCopyToClipboard = () =>
|
||||
toastr.success(this.props.t("EmailAndPasswordCopiedToClipboard"));
|
||||
validatePassword = (value) => this.setState({ passwordValid: value });
|
||||
|
||||
componentDidMount() {
|
||||
const { getSettings, getPortalPasswordSettings, history } = this.props;
|
||||
const requests = [getSettings(), getPortalPasswordSettings(this.state.key)];
|
||||
|
||||
axios.all(requests).catch((e) => {
|
||||
console.error("get settings error", e);
|
||||
history.push(combineUrl(AppServerConfig.proxyURL, `/login/error=${e}`));
|
||||
});
|
||||
|
||||
window.addEventListener("keydown", this.onKeyPress);
|
||||
window.addEventListener("keyup", this.onKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.onKeyPress);
|
||||
window.removeEventListener("keyup", this.onKeyPress);
|
||||
}
|
||||
|
||||
onChangeName = (event) => {
|
||||
this.setState({ firstName: event.target.value });
|
||||
!this.state.firstNameValid &&
|
||||
this.setState({ firstNameValid: event.target.value });
|
||||
this.state.errorText && this.setState({ errorText: "" });
|
||||
};
|
||||
|
||||
onChangeSurname = (event) => {
|
||||
this.setState({ lastName: event.target.value });
|
||||
!this.state.lastNameValid && this.setState({ lastNameValid: true });
|
||||
this.state.errorText && this.setState({ errorText: "" });
|
||||
};
|
||||
|
||||
onChangePassword = (event) => {
|
||||
this.setState({ password: event.target.value });
|
||||
!this.state.passwordValid && this.setState({ passwordValid: true });
|
||||
event.target.value.trim() && this.setState({ passwordEmpty: false });
|
||||
this.state.errorText && this.setState({ errorText: "" });
|
||||
this.onKeyPress(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
console.log("ActivateUser render");
|
||||
const { settings, t, greetingTitle, theme } = this.props;
|
||||
return !settings ? (
|
||||
<Loader className="pageLoader" type="rombs" size="40px" />
|
||||
) : (
|
||||
<ConfirmContainer>
|
||||
<div className="start-basis">
|
||||
<div className="margin-left">
|
||||
<Text className="confirm-row" as="p" fontSize="18px">
|
||||
{t("InviteTitle")}
|
||||
</Text>
|
||||
|
||||
<div className="confirm-row full-width break-word">
|
||||
<a href="/login">
|
||||
<img src="images/dark_general.png" alt="Logo" />
|
||||
</a>
|
||||
<Text
|
||||
as="p"
|
||||
fontSize="24px"
|
||||
color={theme.studio.confirm.activateUser.textColor}
|
||||
>
|
||||
{greetingTitle}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="full-width">
|
||||
<TextInput
|
||||
className="confirm-row"
|
||||
id="name"
|
||||
name="name"
|
||||
value={this.state.firstName}
|
||||
placeholder={t("FirstName")}
|
||||
size="huge"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
isAutoFocussed={true}
|
||||
autoComplete="given-name"
|
||||
isDisabled={this.state.isLoading}
|
||||
hasError={!this.state.firstNameValid}
|
||||
onChange={this.onChangeName}
|
||||
onKeyDown={this.onKeyPress}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="confirm-row"
|
||||
id="surname"
|
||||
name="surname"
|
||||
value={this.state.lastName}
|
||||
placeholder={t("Common:LastName")}
|
||||
size="huge"
|
||||
scale={true}
|
||||
tabIndex={2}
|
||||
autoComplete="family-name"
|
||||
isDisabled={this.state.isLoading}
|
||||
hasError={!this.state.lastNameValid}
|
||||
onChange={this.onChangeSurname}
|
||||
onKeyDown={this.onKeyPress}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className="confirm-row display-none"
|
||||
id="email"
|
||||
name={emailInputName}
|
||||
value={this.state.email}
|
||||
size="huge"
|
||||
scale={true}
|
||||
isReadOnly={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PasswordInput
|
||||
className="confirm-row"
|
||||
id="password"
|
||||
inputName="password"
|
||||
emailInputName={emailInputName}
|
||||
inputValue={this.state.password}
|
||||
placeholder={t("Common:Password")}
|
||||
size="huge"
|
||||
scale={true}
|
||||
tabIndex={4}
|
||||
maxLength={30}
|
||||
inputWidth={inputWidth}
|
||||
hasError={this.state.passwordEmpty}
|
||||
onChange={this.onChangePassword}
|
||||
onCopyToClipboard={this.onCopyToClipboard}
|
||||
onValidateInput={this.validatePassword}
|
||||
clipActionResource={t("Common:CopyEmailAndPassword")}
|
||||
clipEmailResource={`${t("Common:Email")}: `}
|
||||
clipPasswordResource={`${t("Common:Password")}: `}
|
||||
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
|
||||
tooltipPasswordLength={`${t("Common:PasswordLimitLength", {
|
||||
fromNumber: settings ? settings.minLength : 8,
|
||||
toNumber: 30,
|
||||
})}:`}
|
||||
tooltipPasswordDigits={t("Common:PasswordLimitDigits")}
|
||||
tooltipPasswordCapital={t("Common:PasswordLimitUpperCase")}
|
||||
tooltipPasswordSpecial={`${t(
|
||||
"Common:PasswordLimitSpecialSymbols"
|
||||
)} (${PasswordLimitSpecialCharacters})`}
|
||||
generatorSpecial={PasswordLimitSpecialCharacters}
|
||||
passwordSettings={settings}
|
||||
isDisabled={this.state.isLoading}
|
||||
onKeyDown={this.onKeyPress}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="confirm-row"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("LoginRegistryButton")}
|
||||
tabIndex={5}
|
||||
isLoading={this.state.isLoading}
|
||||
onClick={this.onSubmit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <Row className='confirm-row'>
|
||||
|
||||
<Text as='p' fontSize='14px'>{t('LoginWithAccount')}</Text>
|
||||
|
||||
</Row>
|
||||
*/}
|
||||
<Text
|
||||
className="confirm-row"
|
||||
fontSize="14px"
|
||||
color={theme.studio.confirm.activateUser.textColorError}
|
||||
>
|
||||
{this.state.errorText}
|
||||
return (
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700" className="title">
|
||||
{greetingTitle}
|
||||
</Text>
|
||||
</div>
|
||||
</ConfirmContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Confirm.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
<Text className="subtitle">{t("InviteTitle")}</Text>
|
||||
</StyledHeader>
|
||||
|
||||
<FieldContainer
|
||||
className="form-field"
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={!nameValid}
|
||||
errorMessage={t("Common:RequiredField")}
|
||||
>
|
||||
<TextInput
|
||||
id="name"
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={t("FirstName")}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
isAutoFocussed={true}
|
||||
autoComplete="given-name"
|
||||
onChange={onChangeName}
|
||||
onKeyDown={onKeyPress}
|
||||
/>
|
||||
</FieldContainer>
|
||||
|
||||
<FieldContainer
|
||||
className="form-field"
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={!surNameValid}
|
||||
errorMessage={t("Common:RequiredField")}
|
||||
>
|
||||
<TextInput
|
||||
id="surname"
|
||||
name="surname"
|
||||
value={surName}
|
||||
placeholder={t("Common:LastName")}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={2}
|
||||
autoComplete="family-name"
|
||||
onChange={onChangeSurName}
|
||||
onKeyDown={onKeyPress}
|
||||
/>
|
||||
</FieldContainer>
|
||||
|
||||
<FieldContainer
|
||||
className="form-field"
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={isPasswordErrorShow && !passwordValid}
|
||||
errorMessage={`${t(
|
||||
"Common:PasswordLimitMessage"
|
||||
)}: ${getPasswordErrorMessage(t, settings)}`}
|
||||
>
|
||||
<PasswordInput
|
||||
className="confirm-input"
|
||||
simpleView={false}
|
||||
passwordSettings={settings}
|
||||
id="password"
|
||||
inputName="password"
|
||||
placeholder={t("Common:Password")}
|
||||
type="password"
|
||||
inputValue={password}
|
||||
hasError={isPasswordErrorShow && !passwordValid}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
autoComplete="current-password"
|
||||
onChange={onChangePassword}
|
||||
onValidateInput={onValidatePassword}
|
||||
onBlur={onBlurPassword}
|
||||
onKeyDown={onKeyPress}
|
||||
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
|
||||
tooltipPasswordLength={`${t("Common:PasswordMinimumLength")}: ${
|
||||
settings ? settings.minLength : 8
|
||||
}`}
|
||||
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
|
||||
tooltipPasswordCapital={`${t("Common:PasswordLimitUpperCase")}`}
|
||||
tooltipPasswordSpecial={`${t(
|
||||
"Common:PasswordLimitSpecialSymbols"
|
||||
)}`}
|
||||
generatePasswordTitle={t("Wizard:GeneratePassword")}
|
||||
/>
|
||||
</FieldContainer>
|
||||
|
||||
<Button
|
||||
className="confirm-button"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("LoginRegistryButton")}
|
||||
tabIndex={5}
|
||||
onClick={onSubmit}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
const ActivateUserFormWrapper = (props) => {
|
||||
return (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<ActivateUserForm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
const ActivateUserForm = (props) => (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<Confirm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default inject(({ auth }) => {
|
||||
const {
|
||||
@ -385,8 +257,6 @@ export default inject(({ auth }) => {
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
passwordSettings,
|
||||
getSettings,
|
||||
getPortalPasswordSettings,
|
||||
theme,
|
||||
} = auth.settingsStore;
|
||||
|
||||
@ -396,10 +266,12 @@ export default inject(({ auth }) => {
|
||||
greetingTitle: greetingSettings,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
getSettings,
|
||||
getPortalPasswordSettings,
|
||||
login: auth.login,
|
||||
};
|
||||
})(
|
||||
withRouter(withTranslation(["Confirm", "Common"])(observer(ActivateUserForm)))
|
||||
withRouter(
|
||||
withTranslation(["Confirm", "Common", "Wizard"])(
|
||||
withLoader(observer(ActivateUserFormWrapper))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -1,136 +1,67 @@
|
||||
import React from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Button from "@appserver/components/button";
|
||||
import Text from "@appserver/components/text";
|
||||
import toastr from "@appserver/components/toast/toastr";
|
||||
import Button from "@appserver/components/button";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import { tryRedirectTo } from "@appserver/common/utils";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import {
|
||||
StyledPage,
|
||||
StyledBody,
|
||||
StyledHeader,
|
||||
ButtonsWrapper,
|
||||
} from "./StyledConfirm";
|
||||
import withLoader from "../withLoader";
|
||||
import { Base } from "@appserver/components/themes";
|
||||
|
||||
const BodyStyle = styled.div`
|
||||
margin-top: 70px;
|
||||
const ChangeOwnerForm = (props) => {
|
||||
const { t, greetingTitle } = props;
|
||||
console.log(props.linkData);
|
||||
return (
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700" className="title">
|
||||
{greetingTitle}
|
||||
</Text>
|
||||
|
||||
.owner-container {
|
||||
display: grid;
|
||||
<Text className="subtitle">
|
||||
{t("ConfirmOwnerPortalTitle", { newOwner: "NEW OWNER" })}
|
||||
</Text>
|
||||
</StyledHeader>
|
||||
|
||||
.owner-wrapper {
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
.owner-img {
|
||||
max-width: 216px;
|
||||
max-height: 35px;
|
||||
}
|
||||
.owner-title {
|
||||
word-wrap: break-word;
|
||||
margin: 8px 0;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
color: ${(props) => props.theme.studio.confirm.change.titleColor};
|
||||
}
|
||||
.owner-confirm_text {
|
||||
margin: 20px 0 12px 0;
|
||||
}
|
||||
.owner-buttons {
|
||||
margin-top: 20px;
|
||||
min-width: 110px;
|
||||
}
|
||||
.owner-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
<ButtonsWrapper>
|
||||
<Button
|
||||
className="button"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("Common:SaveButton")}
|
||||
tabIndex={2}
|
||||
isDisabled={false}
|
||||
//onClick={this.onAcceptClick}
|
||||
/>
|
||||
<Button
|
||||
className="button"
|
||||
size="normal"
|
||||
label={t("Common:CancelButton")}
|
||||
tabIndex={2}
|
||||
isDisabled={false}
|
||||
//onClick={this.onCancelClick}
|
||||
/>
|
||||
</ButtonsWrapper>
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
.owner-confirm-message {
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
BodyStyle.defaultProps = { theme: Base };
|
||||
|
||||
class Form extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { showButtons: true };
|
||||
}
|
||||
|
||||
onAcceptClick = () => {
|
||||
this.setState({ showButtons: false });
|
||||
toastr.success(t("ConfirmOwnerPortalSuccessMessage"));
|
||||
setTimeout(this.onRedirect, 10000);
|
||||
};
|
||||
|
||||
onRedirect = () => {
|
||||
tryRedirectTo(this.props.defaultPage);
|
||||
};
|
||||
|
||||
onCancelClick = () => {
|
||||
tryRedirectTo(this.props.defaultPage);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, greetingTitle } = this.props;
|
||||
|
||||
return (
|
||||
<BodyStyle>
|
||||
<div className="owner-container">
|
||||
<div className="owner-wrapper">
|
||||
<img
|
||||
className="owner-img"
|
||||
src="images/dark_general.png"
|
||||
alt="Logo"
|
||||
/>
|
||||
<Text className="owner-title">{greetingTitle}</Text>
|
||||
<Text className="owner-confirm_text" fontSize="18px">
|
||||
{t("ConfirmOwnerPortalTitle", { newOwner: "NEW OWNER" })}
|
||||
</Text>
|
||||
{this.state.showButtons ? (
|
||||
<>
|
||||
<Button
|
||||
className="owner-button owner-buttons"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("Common:SaveButton")}
|
||||
tabIndex={2}
|
||||
isDisabled={false}
|
||||
onClick={this.onAcceptClick}
|
||||
/>
|
||||
<Button
|
||||
className="owner-buttons"
|
||||
size="normal"
|
||||
label={t("Common:CancelButton")}
|
||||
tabIndex={2}
|
||||
isDisabled={false}
|
||||
onClick={this.onCancelClick}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Text className="owner-confirm-message" fontSize="12px">
|
||||
{t("ConfirmOwnerPortalSuccessMessage")}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</BodyStyle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Form.propTypes = {};
|
||||
|
||||
Form.defaultProps = {};
|
||||
|
||||
const ChangeOwnerForm = (props) => (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<Form {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
const ChangeOwnerFormWrapper = (props) => {
|
||||
return (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<ChangeOwnerForm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ auth }) => ({
|
||||
greetingTitle: auth.settingsStore.greetingSettings,
|
||||
@ -138,7 +69,7 @@ export default inject(({ auth }) => ({
|
||||
}))(
|
||||
withRouter(
|
||||
withTranslation(["Confirm", "Common"])(
|
||||
withLoader(observer(ChangeOwnerForm))
|
||||
withLoader(observer(ChangeOwnerFormWrapper))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -1,242 +1,181 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import axios from "axios";
|
||||
import PropTypes from "prop-types";
|
||||
import styled from "styled-components";
|
||||
import Button from "@appserver/components/button";
|
||||
import Text from "@appserver/components/text";
|
||||
import PasswordInput from "@appserver/components/password-input";
|
||||
import toastr from "@appserver/components/toast/toastr";
|
||||
import Heading from "@appserver/components/heading";
|
||||
import Button from "@appserver/components/button";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import { createPasswordHash, tryRedirectTo } from "@appserver/common/utils";
|
||||
import { PasswordLimitSpecialCharacters } from "@appserver/common/constants";
|
||||
import FieldContainer from "@appserver/components/field-container";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
|
||||
import withLoader from "../withLoader";
|
||||
import { getPasswordErrorMessage } from "../../../../helpers/utils";
|
||||
import { createPasswordHash, tryRedirectTo } from "@appserver/common/utils";
|
||||
import toastr from "@appserver/components/toast/toastr";
|
||||
|
||||
const BodyStyle = styled.form`
|
||||
margin: 70px auto 0 auto;
|
||||
max-width: 500px;
|
||||
.password-header {
|
||||
margin-bottom: 24px;
|
||||
.password-logo {
|
||||
max-width: 216px;
|
||||
max-height: 35px;
|
||||
const ChangePasswordForm = (props) => {
|
||||
const {
|
||||
t,
|
||||
greetingTitle,
|
||||
settings,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
logout,
|
||||
changePassword,
|
||||
linkData,
|
||||
} = props;
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordValid, setPasswordValid] = useState(true);
|
||||
const [isPasswordErrorShow, setIsPasswordErrorShow] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onChangePassword = (e) => {
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
const onValidatePassword = (res) => {
|
||||
setPasswordValid(res);
|
||||
};
|
||||
|
||||
const onBlurPassword = () => {
|
||||
setIsPasswordErrorShow(true);
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!password.trim()) {
|
||||
setPasswordValid(false);
|
||||
setIsPasswordErrorShow(true);
|
||||
}
|
||||
.password-title {
|
||||
margin: 8px 0;
|
||||
if (!passwordValid || !password.trim()) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
.password-text {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.password-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.password-input {
|
||||
.password-field-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
`;
|
||||
const hash = createPasswordHash(password, hashSettings);
|
||||
const { uid, confirmHeader } = linkData;
|
||||
|
||||
class Form extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { linkData } = props;
|
||||
changePassword(uid, hash, confirmHeader)
|
||||
.then(() => logout())
|
||||
.then(() => {
|
||||
setIsLoading(false);
|
||||
toastr.success(t("ChangePasswordSuccess"));
|
||||
tryRedirectTo(defaultPage);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(t(`${error}`));
|
||||
setIsLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
this.state = {
|
||||
password: "",
|
||||
passwordValid: true,
|
||||
// isValidConfirmLink: false,
|
||||
isLoading: false,
|
||||
passwordEmpty: false,
|
||||
key: linkData.confirmHeader,
|
||||
userId: linkData.uid,
|
||||
};
|
||||
}
|
||||
|
||||
onKeyPress = (target) => {
|
||||
if (target.key === "Enter") {
|
||||
this.onSubmit();
|
||||
const onKeyPress = (event) => {
|
||||
if (event.key === "Enter") {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (event) => {
|
||||
this.setState({ password: event.target.value });
|
||||
!this.state.passwordValid && this.setState({ passwordValid: true });
|
||||
event.target.value.trim() && this.setState({ passwordEmpty: false });
|
||||
this.onKeyPress(event);
|
||||
};
|
||||
|
||||
onSubmit = (e) => {
|
||||
this.setState({ isLoading: true }, function () {
|
||||
const { userId, password, key } = this.state;
|
||||
const {
|
||||
t,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
logout,
|
||||
changePassword,
|
||||
} = this.props;
|
||||
let hasError = false;
|
||||
|
||||
if (!this.state.passwordValid) {
|
||||
hasError = true;
|
||||
this.setState({ passwordValid: !hasError });
|
||||
}
|
||||
|
||||
!this.state.password.trim() && this.setState({ passwordEmpty: true });
|
||||
|
||||
if (hasError) {
|
||||
this.setState({ isLoading: false });
|
||||
return false;
|
||||
}
|
||||
const hash = createPasswordHash(password, hashSettings);
|
||||
|
||||
changePassword(userId, hash, key)
|
||||
.then(() => logout())
|
||||
.then(() => {
|
||||
toastr.success(t("ChangePasswordSuccess"));
|
||||
tryRedirectTo(defaultPage);
|
||||
})
|
||||
.catch((error) => {
|
||||
toastr.error(t(`${error}`));
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("keydown", this.onKeyPress);
|
||||
window.addEventListener("keyup", this.onKeyPress);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener("keydown", this.onKeyPress);
|
||||
window.removeEventListener("keyup", this.onKeyPress);
|
||||
}
|
||||
|
||||
validatePassword = (value) => this.setState({ passwordValid: value });
|
||||
|
||||
render() {
|
||||
const { settings, t, greetingTitle, theme } = this.props;
|
||||
const { isLoading, password, passwordEmpty } = this.state;
|
||||
|
||||
return (
|
||||
<BodyStyle>
|
||||
<div className="password-header">
|
||||
<img
|
||||
className="password-logo"
|
||||
src="images/dark_general.png"
|
||||
alt="Logo"
|
||||
/>
|
||||
<Heading
|
||||
className="password-title"
|
||||
color={theme.studio.confirm.change.titleColor}
|
||||
>
|
||||
return (
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700">
|
||||
{greetingTitle}
|
||||
</Heading>
|
||||
</Text>
|
||||
</StyledHeader>
|
||||
|
||||
<div className="password-change-form">
|
||||
<Text className="confirm-subtitle">{t("PassworResetTitle")}</Text>
|
||||
<FieldContainer
|
||||
className="form-field"
|
||||
isVertical={true}
|
||||
labelVisible={false}
|
||||
hasError={isPasswordErrorShow && !passwordValid}
|
||||
errorMessage={`${t(
|
||||
"Common:PasswordLimitMessage"
|
||||
)}: ${getPasswordErrorMessage(t, settings)}`}
|
||||
>
|
||||
<PasswordInput
|
||||
className="confirm-input"
|
||||
simpleView={false}
|
||||
passwordSettings={settings}
|
||||
id="password"
|
||||
inputName="password"
|
||||
placeholder={t("Common:Password")}
|
||||
type="password"
|
||||
inputValue={password}
|
||||
hasError={isPasswordErrorShow && !passwordValid}
|
||||
size="large"
|
||||
scale={true}
|
||||
tabIndex={1}
|
||||
autoComplete="current-password"
|
||||
onChange={onChangePassword}
|
||||
onValidateInput={onValidatePassword}
|
||||
onBlur={onBlurPassword}
|
||||
onKeyDown={onKeyPress}
|
||||
tooltipPasswordTitle={`${t("Common:PasswordLimitMessage")}:`}
|
||||
tooltipPasswordLength={`${t("Common:PasswordMinimumLength")}: ${
|
||||
settings ? settings.minLength : 8
|
||||
}`}
|
||||
tooltipPasswordDigits={`${t("Common:PasswordLimitDigits")}`}
|
||||
tooltipPasswordCapital={`${t("Common:PasswordLimitUpperCase")}`}
|
||||
tooltipPasswordSpecial={`${t(
|
||||
"Common:PasswordLimitSpecialSymbols"
|
||||
)}`}
|
||||
generatePasswordTitle={t("Wizard:GeneratePassword")}
|
||||
/>
|
||||
</FieldContainer>
|
||||
</div>
|
||||
<Text className="password-text" fontSize="14px">
|
||||
{t("PassworResetTitle")}
|
||||
</Text>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
className="password-input"
|
||||
name="password"
|
||||
inputName="password"
|
||||
inputValue={password}
|
||||
size="huge"
|
||||
scale={true}
|
||||
type="password"
|
||||
isDisabled={isLoading}
|
||||
hasError={passwordEmpty}
|
||||
onValidateInput={this.validatePassword}
|
||||
generatorSpecial={PasswordLimitSpecialCharacters}
|
||||
tabIndex={1}
|
||||
value={password}
|
||||
onChange={this.onChange}
|
||||
emailInputName="E-mail"
|
||||
passwordSettings={settings}
|
||||
tooltipPasswordTitle="Password must contain:"
|
||||
tooltipPasswordLength={`${t("Common:PasswordLimitLength", {
|
||||
fromNumber: settings ? settings.minLength : 8,
|
||||
toNumber: 30,
|
||||
})}:`}
|
||||
placeholder={t("Common:Password")}
|
||||
maxLength={30}
|
||||
onKeyDown={this.onKeyPress}
|
||||
isAutoFocussed={true}
|
||||
inputWidth="490px"
|
||||
/>
|
||||
|
||||
<Button
|
||||
id="button"
|
||||
className="password-button"
|
||||
className="confirm-button"
|
||||
primary
|
||||
size="normal"
|
||||
tabIndex={2}
|
||||
label={
|
||||
isLoading ? t("Common:LoadingProcessing") : t("Common:OKButton")
|
||||
}
|
||||
label={t("Common:Create")}
|
||||
tabIndex={5}
|
||||
onClick={onSubmit}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={this.onSubmit}
|
||||
/>
|
||||
</BodyStyle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Form.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
logout: PropTypes.func.isRequired,
|
||||
linkData: PropTypes.object.isRequired,
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
Form.defaultProps = {
|
||||
password: "",
|
||||
const ChangePasswordFormWrapper = (props) => {
|
||||
return (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<ChangePasswordForm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangePasswordForm = (props) => (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<Form {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default inject(({ auth, setup }) => {
|
||||
const { settingsStore, logout, isAuthenticated } = auth;
|
||||
const {
|
||||
greetingSettings,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
passwordSettings,
|
||||
getSettings,
|
||||
getPortalPasswordSettings,
|
||||
theme,
|
||||
} = settingsStore;
|
||||
} = auth.settingsStore;
|
||||
const { changePassword } = setup;
|
||||
|
||||
return {
|
||||
theme,
|
||||
settings: passwordSettings,
|
||||
hashSettings,
|
||||
greetingTitle: greetingSettings,
|
||||
hashSettings,
|
||||
defaultPage,
|
||||
logout,
|
||||
isAuthenticated,
|
||||
getSettings,
|
||||
getPortalPasswordSettings,
|
||||
logout: auth.logout,
|
||||
changePassword,
|
||||
isAuthenticated: auth.isAuthenticated,
|
||||
};
|
||||
})(
|
||||
withRouter(
|
||||
withTranslation(["Confirm", "Common"])(
|
||||
withLoader(observer(ChangePasswordForm))
|
||||
withTranslation(["Confirm", "Common", "Wizard"])(
|
||||
withLoader(observer(ChangePasswordFormWrapper))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -1,138 +1,74 @@
|
||||
import React, { useState } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import { withTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Button from "@appserver/components/button";
|
||||
import TextInput from "@appserver/components/text-input";
|
||||
import Text from "@appserver/components/text";
|
||||
import TextInput from "@appserver/components/text-input";
|
||||
import Button from "@appserver/components/button";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
|
||||
import withLoader from "../withLoader";
|
||||
import { Base } from "@appserver/components/themes";
|
||||
|
||||
const BodyStyle = styled.div`
|
||||
margin: 70px auto 0 auto;
|
||||
max-width: 432px;
|
||||
|
||||
.edit-header {
|
||||
.header-logo {
|
||||
max-width: 216px;
|
||||
max-height: 35px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
word-wrap: break-word;
|
||||
margin: 8px 0;
|
||||
text-align: left;
|
||||
font-size: 24px;
|
||||
color: ${(props) => props.theme.studio.confirm.change.titleColor};
|
||||
}
|
||||
}
|
||||
|
||||
.edit-text {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
BodyStyle.defaultProps = { theme: Base };
|
||||
|
||||
const PhoneForm = (props) => {
|
||||
const { t, currentPhone, greetingTitle } = props;
|
||||
|
||||
const [phone, setPhone] = useState(currentPhone);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const subTitleTranslation = `Enter mobile phone number`;
|
||||
const infoTranslation = `Your current mobile phone number`;
|
||||
const subInfoTranslation = `The two-factor authentication is enabled to provide additional portal security.
|
||||
Enter your mobile phone number to continue work on the portal.
|
||||
Mobile phone number must be entered using an international format with country code.`;
|
||||
const phonePlaceholder = `Phone`;
|
||||
const buttonTranslation = `Enter number`;
|
||||
|
||||
const onSubmit = () => {
|
||||
console.log("onSubmit CHANGE"); //TODO: Why do nothing?
|
||||
};
|
||||
|
||||
const onKeyPress = (target) => {
|
||||
if (target.code === "Enter") onSubmit();
|
||||
};
|
||||
|
||||
const simplePhoneMask = new Array(15).fill(/\d/);
|
||||
const ChangePhoneForm = (props) => {
|
||||
const { t, greetingTitle } = props;
|
||||
const [currentNumber, setCurrentNumber] = useState("+00000000000");
|
||||
|
||||
return (
|
||||
<BodyStyle>
|
||||
<div className="edit-header">
|
||||
<img className="header-logo" src="images/dark_general.png" alt="Logo" />
|
||||
<div className="header-title">{greetingTitle}</div>
|
||||
</div>
|
||||
<Text className="edit-text" isBold fontSize="14px">
|
||||
{subTitleTranslation}
|
||||
</Text>
|
||||
<Text fontSize="13px">
|
||||
{infoTranslation}: <b>+{currentPhone}</b>
|
||||
</Text>
|
||||
<Text className="edit-text" fontSize="13px">
|
||||
{subInfoTranslation}
|
||||
</Text>
|
||||
<TextInput
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="text"
|
||||
size="huge"
|
||||
scale={true}
|
||||
isAutoFocussed={true}
|
||||
tabIndex={1}
|
||||
autocomple="off"
|
||||
placeholder={phonePlaceholder}
|
||||
onChange={(event) => {
|
||||
setPhone(event.target.value);
|
||||
onKeyPress(event.target);
|
||||
}}
|
||||
value={phone}
|
||||
hasError={false}
|
||||
isDisabled={isLoading}
|
||||
onKeyDown={(event) => onKeyPress(event.target)}
|
||||
guide={false}
|
||||
mask={simplePhoneMask}
|
||||
className="edit-input"
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
size="normal"
|
||||
tabIndex={3}
|
||||
label={isLoading ? t("Common:LoadingProcessing") : buttonTranslation}
|
||||
isDisabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onClick={onSubmit}
|
||||
/>
|
||||
</BodyStyle>
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700" className="title">
|
||||
{greetingTitle}
|
||||
</Text>
|
||||
<Text fontSize="16px" fontWeight="600" className="confirm-subtitle">
|
||||
{t("EnterPhone")}
|
||||
</Text>
|
||||
<Text>
|
||||
{t("CurrentNumber")}: {currentNumber}
|
||||
</Text>
|
||||
<Text>{t("PhoneSubtitle")}</Text>
|
||||
</StyledHeader>
|
||||
|
||||
<TextInput
|
||||
className="phone-input"
|
||||
id="phone"
|
||||
name="phone"
|
||||
type="phone"
|
||||
size="large"
|
||||
scale={true}
|
||||
isAutoFocussed={true}
|
||||
tabIndex={1}
|
||||
hasError={false}
|
||||
guide={false}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="confirm-button"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("GetCode")}
|
||||
tabIndex={2}
|
||||
isDisabled={false}
|
||||
/>
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangePhoneForm = (props) => {
|
||||
const ChangePhoneFormWrapper = (props) => {
|
||||
return (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<PhoneForm {...props} />
|
||||
<ChangePhoneForm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
export default inject(({ auth }) => ({
|
||||
isLoaded: auth.isLoaded,
|
||||
currentPhone: auth.userStore.mobilePhone,
|
||||
greetingTitle: auth.settingsStore.greetingSettings,
|
||||
}))(
|
||||
withRouter(
|
||||
withTranslation(["Confirm", "Common"])(
|
||||
withLoader(observer(ChangePhoneForm))
|
||||
)
|
||||
withTranslation("Confirm")(withLoader(observer(ChangePhoneFormWrapper)))
|
||||
)
|
||||
);
|
||||
|
@ -31,6 +31,7 @@ import MoreLoginModal from "login/moreLogin";
|
||||
import AppLoader from "@appserver/common/components/AppLoader";
|
||||
import EmailInput from "@appserver/components/email-input";
|
||||
import { smallTablet } from "@appserver/components/utils/device";
|
||||
import { getPasswordErrorMessage } from "../../../../helpers/utils";
|
||||
|
||||
export const ButtonsWrapper = styled.div`
|
||||
display: flex;
|
||||
@ -523,12 +524,6 @@ const Confirm = (props) => {
|
||||
setIsPasswordErrorShow(true);
|
||||
};
|
||||
|
||||
const passwordErrorMessage = `${t("Common:PasswordMinimumLength")} ${
|
||||
settings ? settings.minLength : 8
|
||||
} ${settings.digits ? t("Common:PasswordLimitDigits") : ""} ${
|
||||
settings.upperCase ? t("Common:PasswordLimitUpperCase") : ""
|
||||
} ${settings.specSymbols ? t("Common:PasswordLimitSpecialSymbols") : ""}`;
|
||||
|
||||
if (!isLoaded) return <AppLoader />;
|
||||
return (
|
||||
<ConfirmContainer>
|
||||
@ -675,7 +670,7 @@ const Confirm = (props) => {
|
||||
hasError={isPasswordErrorShow && !passwordValid}
|
||||
errorMessage={`${t(
|
||||
"Common:PasswordLimitMessage"
|
||||
)}: ${passwordErrorMessage}`}
|
||||
)}: ${getPasswordErrorMessage(t, settings)}`}
|
||||
>
|
||||
<PasswordInput
|
||||
simpleView={false}
|
||||
|
@ -1,127 +1,90 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { withRouter } from "react-router";
|
||||
import styled from "styled-components";
|
||||
import PropTypes from "prop-types";
|
||||
import { withTranslation } from "react-i18next";
|
||||
|
||||
import { inject, observer } from "mobx-react";
|
||||
import Button from "@appserver/components/button";
|
||||
import Text from "@appserver/components/text";
|
||||
import Button from "@appserver/components/button";
|
||||
import Section from "@appserver/common/components/Section";
|
||||
import { deleteSelf } from "@appserver/common/api/people"; //TODO: Move inside UserStore
|
||||
import { inject, observer } from "mobx-react";
|
||||
import { deleteSelf } from "@appserver/common/api/people";
|
||||
import toastr from "@appserver/components/toast/toastr";
|
||||
import { StyledPage, StyledBody, StyledHeader } from "./StyledConfirm";
|
||||
import withLoader from "../withLoader";
|
||||
|
||||
const ProfileRemoveContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
const ProfileRemoveForm = (props) => {
|
||||
const { t, greetingTitle, linkData, logout } = props;
|
||||
const [isProfileDeleted, setIsProfileDeleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
.start-basis {
|
||||
align-items: flex-start;
|
||||
}
|
||||
const onDeleteProfile = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
.confirm-row {
|
||||
margin: 23px 0 0;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
`;
|
||||
|
||||
class ProfileRemove extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isProfileDeleted: false,
|
||||
};
|
||||
}
|
||||
|
||||
onDeleteProfile = () => {
|
||||
this.setState({ isLoading: true }, function () {
|
||||
const { linkData, logout } = this.props;
|
||||
deleteSelf(linkData.confirmHeader)
|
||||
.then((res) => {
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
isProfileDeleted: true,
|
||||
});
|
||||
console.log("success delete", res);
|
||||
return logout();
|
||||
})
|
||||
.catch((e) => {
|
||||
this.setState({ isLoading: false });
|
||||
console.log("error delete", e);
|
||||
});
|
||||
});
|
||||
deleteSelf(linkData.confirmHeader)
|
||||
.then((res) => {
|
||||
setIsLoading(false);
|
||||
setIsProfileDeleted(true);
|
||||
return logout(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
setIsLoading(false);
|
||||
toastr.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
console.log("profileRemove render");
|
||||
const { t, greetingTitle, theme } = this.props;
|
||||
const { isProfileDeleted } = this.state;
|
||||
if (isProfileDeleted) {
|
||||
return (
|
||||
<ProfileRemoveContainer>
|
||||
<div className="start-basis">
|
||||
<div className="confirm-row full-width break-word">
|
||||
<a href="/login">
|
||||
<img src="images/dark_general.png" alt="Logo" />
|
||||
</a>
|
||||
<Text
|
||||
as="p"
|
||||
fontSize="24px"
|
||||
color={theme.studio.confirm.change.titleColor}
|
||||
>
|
||||
{greetingTitle}
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700" className="title">
|
||||
{t("DeleteProfileSuccessMessage")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{!isProfileDeleted ? (
|
||||
<>
|
||||
<Text className="confirm-row" as="p" fontSize="18px">
|
||||
{t("DeleteProfileConfirmation")}
|
||||
</Text>
|
||||
<Text className="confirm-row" as="p" fontSize="16px">
|
||||
{t("DeleteProfileConfirmationInfo")}
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
className="confirm-row"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("DeleteProfileBtn")}
|
||||
tabIndex={1}
|
||||
isLoading={this.state.isLoading}
|
||||
onClick={this.onDeleteProfile}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text className="confirm-row" as="p" fontSize="18px">
|
||||
{t("DeleteProfileSuccessMessage")}
|
||||
</Text>
|
||||
<Text className="confirm-row" as="p" fontSize="16px">
|
||||
{t("DeleteProfileSuccessMessageInfo")}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ProfileRemoveContainer>
|
||||
<Text fontSize="16px" fontWeight="600" className="confirm-subtitle">
|
||||
{t("DeleteProfileSuccessMessageInfo")}
|
||||
</Text>
|
||||
</StyledHeader>
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ProfileRemove.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
return (
|
||||
<StyledPage>
|
||||
<StyledBody>
|
||||
<StyledHeader>
|
||||
<Text fontSize="23px" fontWeight="700" className="title">
|
||||
{greetingTitle}
|
||||
</Text>
|
||||
<Text fontSize="16px" fontWeight="600" className="confirm-subtitle">
|
||||
{t("DeleteProfileConfirmation")}
|
||||
</Text>
|
||||
<Text className="info-delete">
|
||||
{t("DeleteProfileConfirmationInfo")}
|
||||
</Text>
|
||||
</StyledHeader>
|
||||
|
||||
<Button
|
||||
className="confirm-button"
|
||||
primary
|
||||
size="normal"
|
||||
label={t("DeleteProfileBtn")}
|
||||
tabIndex={1}
|
||||
isDisabled={isLoading}
|
||||
onClick={onDeleteProfile}
|
||||
/>
|
||||
</StyledBody>
|
||||
</StyledPage>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileRemoveFormWrapper = (props) => {
|
||||
return (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<ProfileRemoveForm {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
const ProfileRemoveForm = (props) => (
|
||||
<Section>
|
||||
<Section.SectionBody>
|
||||
<ProfileRemove {...props} />
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
);
|
||||
|
||||
export default inject(({ auth }) => ({
|
||||
greetingTitle: auth.settingsStore.greetingSettings,
|
||||
@ -129,6 +92,6 @@ export default inject(({ auth }) => ({
|
||||
logout: auth.logout,
|
||||
}))(
|
||||
withRouter(
|
||||
withTranslation("Confirm")(withLoader(observer(ProfileRemoveForm)))
|
||||
withTranslation("Confirm")(withLoader(observer(ProfileRemoveFormWrapper)))
|
||||
)
|
||||
);
|
||||
|
@ -17,15 +17,18 @@ import Link from "@appserver/components/link";
|
||||
|
||||
const StyledForm = styled(Box)`
|
||||
margin: 63px auto auto 216px;
|
||||
width: 570px;
|
||||
width: 960px;
|
||||
display: flex;
|
||||
flex: 1fr 1fr;
|
||||
gap: 50px;
|
||||
gap: 80px;
|
||||
flex-direction: row;
|
||||
|
||||
@media ${tablet} {
|
||||
margin: 120px auto;
|
||||
width: 480px;
|
||||
flex: 1fr;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
@media ${mobile} {
|
||||
@ -33,6 +36,7 @@ const StyledForm = styled(Box)`
|
||||
width: 311px;
|
||||
flex: 1fr;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
}
|
||||
|
||||
.app-code-wrapper {
|
||||
@ -41,12 +45,6 @@ const StyledForm = styled(Box)`
|
||||
}
|
||||
}
|
||||
|
||||
.app-code-continue-btn {
|
||||
@media ${tablet} {
|
||||
margin: 32px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.set-app-title {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@ -55,8 +53,16 @@ const StyledForm = styled(Box)`
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
@media ${tablet} {
|
||||
#qrcode {
|
||||
.qrcode-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px 80px;
|
||||
background: #f8f9f9;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
@media ${mobile} {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -144,7 +150,21 @@ const TfaActivationForm = withLoader((props) => {
|
||||
</Trans>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box displayProp="flex" className="app-code-wrapper">
|
||||
</div>
|
||||
<div>
|
||||
<Box
|
||||
displayProp="flex"
|
||||
flexDirection="column"
|
||||
className="app-code-wrapper"
|
||||
>
|
||||
<div className="qrcode-wrapper">
|
||||
<img
|
||||
src={qrCode}
|
||||
height="180px"
|
||||
width="180px"
|
||||
alt="QR-code"
|
||||
></img>
|
||||
</div>
|
||||
<Box className="app-code-input">
|
||||
<FieldContainer
|
||||
labelVisible={false}
|
||||
@ -155,7 +175,7 @@ const TfaActivationForm = withLoader((props) => {
|
||||
id="code"
|
||||
name="code"
|
||||
type="text"
|
||||
size={width <= 1024 ? "large" : "base"}
|
||||
size="large"
|
||||
scale
|
||||
isAutoFocussed
|
||||
tabIndex={1}
|
||||
@ -172,11 +192,11 @@ const TfaActivationForm = withLoader((props) => {
|
||||
/>
|
||||
</FieldContainer>
|
||||
</Box>
|
||||
<Box className="app-code-continue-btn" marginProp="0 0 0 8px">
|
||||
<Box className="app-code-continue-btn">
|
||||
<Button
|
||||
scale
|
||||
primary
|
||||
size={width <= 1024 ? "medium" : "normal"}
|
||||
size="medium"
|
||||
tabIndex={3}
|
||||
label={
|
||||
isLoading
|
||||
@ -190,9 +210,6 @@ const TfaActivationForm = withLoader((props) => {
|
||||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
<div id="qrcode">
|
||||
<img src={qrCode} height="180px" width="180px" alt="QR-code"></img>
|
||||
</div>
|
||||
</StyledForm>
|
||||
</Section.SectionBody>
|
||||
</Section>
|
||||
|
@ -14,10 +14,11 @@ import withLoader from "../withLoader";
|
||||
import { mobile, tablet } from "@appserver/components/utils/device";
|
||||
|
||||
const StyledForm = styled(Box)`
|
||||
margin: 63px auto auto 216px;
|
||||
width: 570px;
|
||||
margin: 63px auto;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1fr;
|
||||
|
||||
@media ${tablet} {
|
||||
margin: 120px auto;
|
||||
@ -34,12 +35,6 @@ const StyledForm = styled(Box)`
|
||||
}
|
||||
}
|
||||
|
||||
.app-code-continue-btn {
|
||||
@media ${tablet} {
|
||||
margin: 32px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.app-code-text {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@ -86,7 +81,11 @@ const TfaAuthForm = withLoader((props) => {
|
||||
</Text>
|
||||
<Text>{t("EnterAppCodeDescription")}</Text>
|
||||
</Box>
|
||||
<Box displayProp="flex" className="app-code-wrapper">
|
||||
<Box
|
||||
displayProp="flex"
|
||||
flexDirection="column"
|
||||
className="app-code-wrapper"
|
||||
>
|
||||
<Box className="app-code-input">
|
||||
<FieldContainer
|
||||
labelVisible={false}
|
||||
@ -97,7 +96,7 @@ const TfaAuthForm = withLoader((props) => {
|
||||
id="code"
|
||||
name="code"
|
||||
type="text"
|
||||
size={width <= 1024 ? "large" : "base"}
|
||||
size="huge"
|
||||
scale
|
||||
isAutoFocussed
|
||||
tabIndex={1}
|
||||
@ -114,11 +113,11 @@ const TfaAuthForm = withLoader((props) => {
|
||||
/>
|
||||
</FieldContainer>
|
||||
</Box>
|
||||
<Box className="app-code-continue-btn" marginProp="0 0 0 8px">
|
||||
<Box className="app-code-continue-btn">
|
||||
<Button
|
||||
scale
|
||||
primary
|
||||
size={width <= 1024 ? "medium" : "normal"}
|
||||
size="medium"
|
||||
tabIndex={3}
|
||||
label={
|
||||
isLoading
|
||||
|
@ -24,7 +24,9 @@ export default function withLoader(WrappedComponent) {
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(type === "PasswordChange" || type === "LinkInvite") &&
|
||||
(type === "PasswordChange" ||
|
||||
type === "LinkInvite" ||
|
||||
type === "Activation") &&
|
||||
!passwordSettings
|
||||
) {
|
||||
axios
|
||||
@ -41,7 +43,9 @@ export default function withLoader(WrappedComponent) {
|
||||
const isLoaded =
|
||||
type === "TfaActivation" || type === "TfaAuth"
|
||||
? props.isLoaded
|
||||
: type === "PasswordChange" || type === "LinkInvite"
|
||||
: type === "PasswordChange" ||
|
||||
type === "LinkInvite" ||
|
||||
type === "Activation"
|
||||
? !!passwordSettings
|
||||
: true;
|
||||
|
||||
|
@ -53,7 +53,14 @@ Tiles.propTypes = {
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
const Body = ({ match, isLoaded, availableModules, displayName, theme }) => {
|
||||
const Body = ({
|
||||
match,
|
||||
isLoaded,
|
||||
availableModules,
|
||||
displayName,
|
||||
snackbarExist,
|
||||
theme
|
||||
}) => {
|
||||
const { t } = useTranslation(["Home", "translation"]);
|
||||
const { error } = match.params;
|
||||
setDocumentTitle();
|
||||
@ -63,7 +70,7 @@ const Body = ({ match, isLoaded, availableModules, displayName, theme }) => {
|
||||
return !isLoaded ? (
|
||||
<></>
|
||||
) : (
|
||||
<HomeContainer>
|
||||
<HomeContainer snackbarExist={snackbarExist}>
|
||||
<Tiles
|
||||
availableModules={availableModules}
|
||||
displayName={displayName}
|
||||
@ -113,7 +120,7 @@ Home.propTypes = {
|
||||
|
||||
export default inject(({ auth }) => {
|
||||
const { isLoaded, settingsStore, availableModules, userStore } = auth;
|
||||
const { defaultPage, theme } = settingsStore;
|
||||
const { defaultPage, snackbarExist, theme } = settingsStore;
|
||||
const { displayName } = userStore.user;
|
||||
|
||||
return {
|
||||
@ -122,5 +129,6 @@ export default inject(({ auth }) => {
|
||||
isLoaded,
|
||||
availableModules,
|
||||
displayName,
|
||||
snackbarExist,
|
||||
};
|
||||
})(withRouter(observer(Home)));
|
||||
|
@ -9,7 +9,8 @@ const HomeContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: ${isMobile ? "center" : "space-between"};
|
||||
align-items: center;
|
||||
|
||||
margin-top: ${(props) =>
|
||||
props.snackbarExist && isMobile ? "150px" : "50px"};
|
||||
@media (max-width: 1024px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -69,3 +69,11 @@ export const onItemClick = (e) => {
|
||||
|
||||
history.push(link);
|
||||
};
|
||||
|
||||
export const getPasswordErrorMessage = (t, settings) => {
|
||||
return `${t("Common:PasswordMinimumLength")} ${
|
||||
settings ? settings.minLength : 8
|
||||
} ${settings.digits ? t("Common:PasswordLimitDigits") : ""} ${
|
||||
settings.upperCase ? t("Common:PasswordLimitUpperCase") : ""
|
||||
} ${settings.specSymbols ? t("Common:PasswordLimitSpecialSymbols") : ""}`;
|
||||
};
|
||||
|
@ -23,6 +23,7 @@ Scenario("Tfa auth success", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.build, "build");
|
||||
I.mockEndpoint(Endpoints.info, "info");
|
||||
I.mockEndpoint(Endpoints.self, "self");
|
||||
I.mockEndpoint(Endpoints.validation, "validation");
|
||||
|
||||
I.amOnPage("/confirm/TfaAuth");
|
||||
I.fillField("code", "123456");
|
||||
@ -33,6 +34,24 @@ Scenario("Tfa auth success", async ({ I }) => {
|
||||
I.see("Documents");
|
||||
});
|
||||
|
||||
Scenario("Tfa auth error", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.settings, "settings");
|
||||
I.mockEndpoint(Endpoints.build, "build");
|
||||
I.mockEndpoint(Endpoints.providers, "providers");
|
||||
I.mockEndpoint(Endpoints.capabilities, "capabilities");
|
||||
I.mockEndpoint(Endpoints.code, "codeError");
|
||||
I.mockEndpoint(Endpoints.validation, "validation");
|
||||
|
||||
I.amOnPage("/confirm/TfaAuth");
|
||||
I.fillField("code", "123456");
|
||||
I.click({
|
||||
react: "Button",
|
||||
});
|
||||
|
||||
I.see("Web Office");
|
||||
});
|
||||
|
||||
Scenario("Tfa activation success", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.setup, "setup");
|
||||
@ -53,6 +72,32 @@ Scenario("Tfa activation success", async ({ I }) => {
|
||||
I.see("Documents");
|
||||
});
|
||||
|
||||
Scenario("Tfa on from settings", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.common, "common");
|
||||
I.mockEndpoint(Endpoints.settings, "settings");
|
||||
I.mockEndpoint(Endpoints.build, "build");
|
||||
I.mockEndpoint(Endpoints.info, "infoSettings");
|
||||
I.mockEndpoint(Endpoints.self, "selfSettings");
|
||||
I.mockEndpoint(Endpoints.tfaapp, "tfaapp");
|
||||
I.mockEndpoint(Endpoints.tfaconfirm, "tfaconfirm");
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.setup, "setup");
|
||||
|
||||
I.amOnPage("/settings/security/access-portal/tfa");
|
||||
|
||||
I.see("Two-factor authentication");
|
||||
|
||||
I.click({
|
||||
react: "RadioButton",
|
||||
props: {
|
||||
value: "app",
|
||||
},
|
||||
});
|
||||
|
||||
I.click("Save");
|
||||
I.see("Configure your authenticator application");
|
||||
});
|
||||
|
||||
Scenario("Profile remove success", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
|
||||
@ -66,19 +111,6 @@ Scenario("Profile remove success", async ({ I }) => {
|
||||
I.see("Web Office");
|
||||
});
|
||||
|
||||
Scenario("Tfa auth error", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.code, "codeError");
|
||||
|
||||
I.amOnPage("/confirm/TfaAuth");
|
||||
I.fillField("code", "123456");
|
||||
I.click({
|
||||
react: "Button",
|
||||
});
|
||||
|
||||
I.see("Web Office");
|
||||
});
|
||||
|
||||
Scenario("Change email", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.settings, "settings");
|
||||
@ -110,9 +142,6 @@ Scenario("Change password", async ({ I }) => {
|
||||
I.fillField("password", "qwerty12");
|
||||
I.click({
|
||||
react: "Button",
|
||||
props: {
|
||||
className: "password-button",
|
||||
},
|
||||
});
|
||||
|
||||
I.see("Documents");
|
||||
|
@ -97,4 +97,22 @@ module.exports = class Endpoints {
|
||||
method: "GET",
|
||||
baseDir: "people",
|
||||
};
|
||||
|
||||
static tfaapp = {
|
||||
url: ["http://localhost:8092/api/2.0/settings/tfaapp"],
|
||||
method: "GET",
|
||||
baseDir: "settings",
|
||||
};
|
||||
|
||||
static settfaapp = {
|
||||
url: ["http://localhost:8092/api/2.0/settings/tfaapp"],
|
||||
method: "PUT",
|
||||
baseDir: "settings",
|
||||
};
|
||||
|
||||
static tfaconfirm = {
|
||||
url: ["http://localhost:8092/api/2.0/settings/tfaapp/confirm"],
|
||||
method: "GET",
|
||||
baseDir: "settings",
|
||||
};
|
||||
};
|
||||
|
@ -1,10 +1,5 @@
|
||||
{
|
||||
"error": {
|
||||
"message": "User authentication failed",
|
||||
"type": "System.Security.Authentication.AuthenticationException",
|
||||
"stack": " at ASC.Web.Api.Controllers.AuthenticationController.GetUser(AuthModel memberModel, Boolean& viaEmail) in C:\\ONLYOFFICE\\AppServer\\web\\ASC.Web.Api\\Controllers\\AuthenticationController.cs:line 462\r\n at ASC.Web.Api.Controllers.AuthenticationController.AuthenticateMe(AuthModel auth) in C:\\ONLYOFFICE\\AppServer\\web\\ASC.Web.Api\\Controllers\\AuthenticationController.cs:line 257\r\n at ASC.Web.Api.Controllers.AuthenticationController.AuthenticateMeFromBody(AuthModel auth) in C:\\ONLYOFFICE\\AppServer\\web\\ASC.Web.Api\\Controllers\\AuthenticationController.cs:line 167\r\n at lambda_method1086(Closure , Object , Object[] )\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n--- End of stack trace from previous location ---\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n--- End of stack trace from previous location ---\r\n at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)",
|
||||
"hresult": -2146233087
|
||||
},
|
||||
"error": { "message": "User authentication failed", "hresult": 0 },
|
||||
"status": 1,
|
||||
"statusCode": 500
|
||||
"statusCode": 401
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
{ "count": 1, "response": true, "status": 0, "statusCode": 200 }
|
@ -0,0 +1,19 @@
|
||||
{
|
||||
"count": 2,
|
||||
"response": [
|
||||
{
|
||||
"id": "sms",
|
||||
"title": "By SMS",
|
||||
"enabled": false,
|
||||
"avaliable": false
|
||||
},
|
||||
{
|
||||
"id": "app",
|
||||
"title": "By authenticator app",
|
||||
"enabled": false,
|
||||
"avaliable": true
|
||||
}
|
||||
],
|
||||
"status": 0,
|
||||
"statusCode": 200
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"count": 1,
|
||||
"response": "http://localhost:8092/confirm/TfaActivation",
|
||||
"status": 0,
|
||||
"statusCode": 200
|
||||
}
|
@ -151,16 +151,10 @@ Scenario("Change owner page render test", async ({ I }) => {
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
className: "owner-button owner-buttons",
|
||||
},
|
||||
});
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
className: "owner-buttons",
|
||||
},
|
||||
});
|
||||
|
||||
I.saveScreenshot(`5.change-owner.png`);
|
||||
@ -186,7 +180,6 @@ Scenario("Activate user page render test", async ({ I }) => {
|
||||
I.seeElement({
|
||||
react: "TextInput",
|
||||
props: {
|
||||
className: "confirm-row",
|
||||
id: "name",
|
||||
},
|
||||
});
|
||||
@ -194,7 +187,6 @@ Scenario("Activate user page render test", async ({ I }) => {
|
||||
I.seeElement({
|
||||
react: "TextInput",
|
||||
props: {
|
||||
className: "confirm-row",
|
||||
id: "surname",
|
||||
},
|
||||
});
|
||||
@ -202,16 +194,12 @@ Scenario("Activate user page render test", async ({ I }) => {
|
||||
I.seeElement({
|
||||
react: "PasswordInput",
|
||||
props: {
|
||||
className: "confirm-row",
|
||||
id: "password",
|
||||
},
|
||||
});
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
className: "confirm-row",
|
||||
},
|
||||
});
|
||||
|
||||
I.saveScreenshot(`6.activate-user.png`);
|
||||
@ -235,16 +223,10 @@ Scenario("Change password page render test", async ({ I }) => {
|
||||
|
||||
I.seeElement({
|
||||
react: "PasswordInput",
|
||||
props: {
|
||||
className: "password-input",
|
||||
},
|
||||
});
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
className: "password-button",
|
||||
},
|
||||
});
|
||||
|
||||
I.saveScreenshot(`7.change-password.png`);
|
||||
@ -259,6 +241,9 @@ Scenario("Change password page render test", async ({ I }) => {
|
||||
Scenario("TfaActivation page render test", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
I.mockEndpoint(Endpoints.setup, "setup");
|
||||
I.mockEndpoint(Endpoints.settings, "settings");
|
||||
I.mockEndpoint(Endpoints.build, "build");
|
||||
|
||||
I.amOnPage("/confirm/TfaActivation");
|
||||
|
||||
I.see("Configure your authenticator application");
|
||||
@ -297,9 +282,9 @@ Scenario(
|
||||
I.mockEndpoint(Endpoints.info, "infoSettings");
|
||||
I.mockEndpoint(Endpoints.self, "selfSettings");
|
||||
|
||||
if (deviceType === "mobile") {
|
||||
I.amOnPage("/settings/common/customization/language-and-time-zone");
|
||||
I.amOnPage("/settings/common/customization/language-and-time-zone");
|
||||
|
||||
if (deviceType === "mobile") {
|
||||
I.see("Language and Time Zone Settings");
|
||||
|
||||
I.seeElement("div", ".settings-block");
|
||||
@ -330,3 +315,43 @@ Scenario(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (deviceType === "mobile") {
|
||||
Scenario("Tfa settings page mobile render test", async ({ I }) => {
|
||||
I.mockEndpoint(Endpoints.common, "common");
|
||||
I.mockEndpoint(Endpoints.settings, "settings");
|
||||
I.mockEndpoint(Endpoints.build, "build");
|
||||
I.mockEndpoint(Endpoints.info, "infoSettings");
|
||||
I.mockEndpoint(Endpoints.self, "selfSettings");
|
||||
I.mockEndpoint(Endpoints.tfaapp, "tfaapp");
|
||||
I.mockEndpoint(Endpoints.tfaconfirm, "tfaconfirm");
|
||||
I.mockEndpoint(Endpoints.confirm, "confirm");
|
||||
|
||||
I.amOnPage("/settings/security/access-portal/tfa");
|
||||
|
||||
I.see("Two-factor authentication");
|
||||
|
||||
I.seeElement({
|
||||
react: "RadioButtonGroup",
|
||||
props: {
|
||||
className: "box",
|
||||
},
|
||||
});
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
label: "Save",
|
||||
isDisabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
I.seeElement({
|
||||
react: "Button",
|
||||
props: {
|
||||
label: "Cancel",
|
||||
isDisabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 20 KiB |