Merge branch 'feature/virtual-rooms-1.2' of github.com:ONLYOFFICE/AppServer into feature/virtual-rooms-1.2

This commit is contained in:
Vlada Gazizova 2022-04-03 19:07:56 +03:00
commit 638cb62520
188 changed files with 12096 additions and 1514 deletions

View File

@ -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,
});
}

View File

@ -59,6 +59,7 @@ export function loginWithTfaCode(userName, passwordHash, code) {
return request({
method: "post",
url: `/authentication/${code}`,
skipLogout: true,
data,
});
}

View File

@ -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")};

View File

@ -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 };

View File

@ -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;
}

View File

@ -96,7 +96,6 @@ const Navigation = ({
onBackToParentFolder={onBackToParentFolderAction}
title={title}
personal={personal}
isRootFolder={isRootFolder}
canCreate={canCreate}
navigationItems={navigationItems}
getContextOptionsFolder={getContextOptionsFolder}

View File

@ -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;
`}
`;

View File

@ -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 &&

View File

@ -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));

View File

@ -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;

View File

@ -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;
`
}
`}
`;

View File

@ -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 };

View File

@ -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";

View File

@ -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;
};

View File

@ -64,8 +64,8 @@ class TfaStore {
return api.settings.getTfaNewBackupCodes();
};
unlinkApp = async () => {
return api.settings.unlinkTfaApp();
unlinkApp = async (id) => {
return api.settings.unlinkTfaApp(id);
};
}

View File

@ -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");

View File

@ -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 {

View 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 |

View File

@ -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,
};

View File

@ -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".

View 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");
});
});

View 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;

View 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,
};

View 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;

View File

@ -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;

View File

@ -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),
};
};

View File

@ -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;

View 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

View 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

View File

@ -0,0 +1,2 @@
export { default as WarningIcon } from "./Warning.svg";
export { default as DeleteIcon } from "./Delete.svg";

View File

@ -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;

View File

@ -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 };

View File

@ -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",

View File

@ -527,7 +527,7 @@ const Dark = {
},
optionButton: {
padding: "8px 0px 9px 7px",
padding: "8px 9px 9px 7px",
},
},

View File

@ -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;

View 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,
};
};

View 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]);
};

View File

@ -37,7 +37,7 @@ const Banner = () => {
"en"
);
}
return await (await fetch(translationUrl)).json();
return await res.json();
};
const getBanner = async () => {

View File

@ -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;

View File

@ -396,8 +396,6 @@ class Tile extends React.PureComponent {
title: children[0].props.item.title,
};
console.log({ item });
return (
<StyledTile
ref={this.tile}

View File

@ -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 &&

View File

@ -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";

View File

@ -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,

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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";

View File

@ -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)));

View File

@ -514,6 +514,7 @@ class SectionBodyContent extends React.PureComponent {
visible={resetAppDialogVisible}
onClose={this.toggleResetAppDialogVisible}
resetTfaApp={this.props.resetTfaApp}
id={profile.id}
/>
)}
{backupCodesDialogVisible && (

View File

@ -0,0 +1,3 @@
{
"defaultLanguage": "en"
}

View 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'
],
}

View 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,
})
}

View 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"
}
]

View File

@ -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"
}
}

View 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!"
}

View File

@ -0,0 +1,4 @@
{
"Title":"Адвент-календарь ONLYOFFICE",
"Text": "Готовьтесь к Новому году с ONLYOFFICE и получите <1>ежедневные подарки и скидки</1> каждый день - до минус 99%!"
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 285 KiB

View 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;
}
}

View 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
}
}
}
}
`;

View File

@ -0,0 +1,6 @@
import * as React from "react"
export default function Component () {
return "";
}

View File

@ -0,0 +1,5 @@
p,
body {
margin: 0px;
padding: 0px;
}

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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.",

View File

@ -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-значный код из приложения в поле ниже.",

View File

@ -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));

View File

@ -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;
}

View File

@ -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) => {

View File

@ -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)"};

View File

@ -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%;
}
`;

View File

@ -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))
)
)
);

View File

@ -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))
)
)
);

View File

@ -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))
)
)
);

View File

@ -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)))
)
);

View File

@ -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}

View File

@ -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)))
)
);

View File

@ -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>

View File

@ -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

View File

@ -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;

View File

@ -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)));

View File

@ -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;
}

View File

@ -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") : ""}`;
};

View File

@ -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");

View File

@ -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",
};
};

View File

@ -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
}

View File

@ -0,0 +1 @@
{ "count": 1, "response": true, "status": 0, "statusCode": 200 }

View File

@ -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
}

View File

@ -0,0 +1,6 @@
{
"count": 1,
"response": "http://localhost:8092/confirm/TfaActivation",
"status": 0,
"statusCode": 200
}

View File

@ -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,
},
});
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Some files were not shown because too many files have changed in this diff Show More