Merge branch 'develop' into feature/VDR-room

# Conflicts:
#	packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/Rooms/index.js
This commit is contained in:
Nikita Gopienko 2024-05-22 12:57:07 +03:00
commit d2b6d23d9a
98 changed files with 1521 additions and 1151 deletions

30
.github/workflows/update-version.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Update packages versions
on:
create:
jobs:
change-version:
if: (startsWith(github.ref, 'refs/heads/release/') ||
startsWith(github.ref, 'refs/heads/hotfix/'))
name: "Update packages versions"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: '${{ github.token }}'
- name: Filter changes and update versions
run: |
VERSION=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/[^0-9.]*//g')
echo "VERSION=$VERSION" >> $GITHUB_ENV
sed -i "s/\(\"version\":\).*/\1 \"$VERSION\",/g" packages/*/package.json
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: github-actions[bot]
author_email: github-actions[bot]@users.noreply.github.com
message: Update version in packages.json to v${{ env.VERSION }}

View File

@ -26,16 +26,19 @@
import { useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";
import { inject, observer } from "mobx-react";
export default function ScrollToTop() {
function ScrollToTop({ currentDeviceType }) {
const { pathname, state } = useLocation();
const scrollRef = useRef();
useEffect(() => {
const scrollId =
currentDeviceType === "mobile" ? "#customScrollBar" : "#sectionScroll";
scrollRef.current = document.querySelector(
"#customScrollBar > .scroll-wrapper > .scroller",
`${scrollId} > .scroll-wrapper > .scroller`,
);
}, []);
}, [pathname, currentDeviceType]);
useEffect(() => {
!state?.disableScrollToTop &&
@ -45,3 +48,7 @@ export default function ScrollToTop() {
return null;
}
export default inject(({ settingsStore }) => ({
currentDeviceType: settingsStore.currentDeviceType,
}))(observer(ScrollToTop));

View File

@ -31,7 +31,7 @@ import { useTranslation } from "react-i18next";
import styled from "styled-components";
import PropTypes from "prop-types";
import { InputBlock } from "@docspace/shared/components/input-block";
import { globalColors } from "@docspace/shared/themes";
import { globalColors } from "@docspace/shared/themes/globalColors";
const iconColor = globalColors.gray;

View File

@ -291,24 +291,25 @@ const InvitePanel = ({
setIsLoading(false);
const invitedViaEmail = data.invitations
.filter((inv) => inv.email && !inv.id)
.map((invitation) => ({
access: invitation.access,
sharedTo: {
name: invitation.email,
userName: invitation.email,
email: invitation.email,
displayName: invitation.email,
status: 1,
activationStatus: 2,
usedSpace: 0,
hasAvatar: false,
},
canEditAccess: false,
}));
if (isRooms) {
const newInfoPanelMembers = [
...result.members,
...data.invitations.map((invitation) => ({
access: invitation.access,
sharedTo: {
name: invitation.email,
userName: invitation.email,
email: invitation.email,
displayName: invitation.email,
status: 1,
activationStatus: 2,
usedSpace: 0,
hasAvatar: false,
},
canEditAccess: false,
})),
];
const newInfoPanelMembers = [...result.members, ...invitedViaEmail];
addInfoPanelMembers(t, newInfoPanelMembers);
}

View File

@ -124,7 +124,7 @@ const CategoryFilterMobile = ({
>
<Scrollbar
style={{ position: "absolute" }}
scrollclass="section-scroll"
scrollClass="section-scroll"
ref={scrollRef}
>
<DropDownItem

View File

@ -29,7 +29,6 @@ import { inject, observer } from "mobx-react";
import ViewHelper from "./helpers/ViewHelper";
import ItemTitle from "./sub-components/ItemTitle";
import Search from "./sub-components/Search";
import { StyledInfoPanelBody } from "./styles/common";
import { useParams } from "react-router-dom";
@ -174,8 +173,6 @@ const InfoPanelBodyContent = ({
return (
<StyledInfoPanelBody>
{showSearchBlock && <Search />}
{!isNoItem && (
<ItemTitle
{...defaultProps}

View File

@ -211,17 +211,24 @@ const StyledSearchContainer = styled.div`
box-sizing: border-box;
position: absolute;
top: 0;
left: 0;
inset-inline: 0 -20px;
display: flex;
align-items: center;
gap: 8px;
height: 68px;
width: 100%;
padding: 0 16px;
border-radius: 0 0 6px 6px;
background-color: ${(props) => props.theme.infoPanel.backgroundColor};
z-index: 101;
box-shadow: ${({ theme }) => theme.infoPanel.search.boxShadow};
@media ${tablet} {
inset-inline: 0;
}
@media ${mobile} {
inset-inline: 0 -14px;
}
`;
const StyledLink = styled.div`

View File

@ -42,6 +42,7 @@ import {
RoomsType,
ShareAccessRights,
} from "@docspace/shared/enums";
import Search from "../../Search";
const RoomsItemHeader = ({
t,
@ -56,6 +57,7 @@ const RoomsItemHeader = ({
setBufferSelection,
isArchive,
hasLinks,
showSearchBlock,
setCalendarDay,
openHistory,
setShowSearchBlock,
@ -105,6 +107,8 @@ const RoomsItemHeader = ({
return (
<StyledTitle ref={itemTitleRef}>
{isRoomMembersPanel && showSearchBlock && <Search />}
<div className="item-icon">
<RoomIcon
color={selection.logo?.color}
@ -172,6 +176,7 @@ export default inject(
infoPanelSelection,
roomsView,
setIsMobileHidden,
showSearchBlock,
setShowSearchBlock,
setCalendarDay,
} = infoPanelStore;
@ -189,6 +194,7 @@ export default inject(
roomsView,
infoPanelSelection,
setIsMobileHidden,
showSearchBlock,
setShowSearchBlock,
isGracePeriod: currentTariffStatusStore.isGracePeriod,

View File

@ -24,7 +24,7 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useEffect } from "react";
import { useContext, useEffect } from "react";
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import { toastr } from "@docspace/shared/components/toast";
@ -36,7 +36,11 @@ import MembersHelper from "../../helpers/MembersHelper";
import MembersList from "./sub-components/MembersList";
import User from "./User";
import PublicRoomBar from "@docspace/shared/components/public-room-bar";
import { LinksBlock, StyledLinkRow } from "./sub-components/Styled";
import {
LinksBlock,
StyledLinkRow,
StyledPublicRoomBarContainer,
} from "./sub-components/Styled";
import EmptyContainer from "./sub-components/EmptyContainer";
import { Text } from "@docspace/shared/components/text";
@ -46,6 +50,7 @@ import { Tooltip } from "@docspace/shared/components/tooltip";
import { isDesktop } from "@docspace/shared/utils";
import LinksToViewingIconUrl from "PUBLIC_DIR/images/links-to-viewing.react.svg?url";
import PlusIcon from "PUBLIC_DIR/images/plus.react.svg?url";
import { ScrollbarContext } from "@docspace/shared/components/scrollbar";
import { Avatar } from "@docspace/shared/components/avatar";
import { copyShareLink } from "@docspace/shared/utils/copy";
@ -79,6 +84,8 @@ const Members = ({
const withoutTitlesAndLinks = !!searchValue;
const membersHelper = new MembersHelper({ t });
const scrollContext = useContext(ScrollbarContext);
const updateInfoPanelMembers = async () => {
if (
!infoPanelSelection ||
@ -96,6 +103,11 @@ const Members = ({
updateInfoPanelMembers();
}, [infoPanelSelection, searchValue]);
useEffect(() => {
if (searchResultIsLoading) return;
scrollContext?.parentScrollbar?.scrollToTop();
}, [searchResultIsLoading]);
const loadNextPage = async () => {
await fetchMoreMembers(t, withoutTitlesAndLinks);
};
@ -236,10 +248,12 @@ const Members = ({
return (
<>
{showPublicRoomBar && (
<PublicRoomBar
headerText={t("Files:RoomAvailableViaExternalLink")}
bodyText={t("CreateEditRoomDialog:PublicRoomBarDescription")}
/>
<StyledPublicRoomBarContainer>
<PublicRoomBar
headerText={t("Files:RoomAvailableViaExternalLink")}
bodyText={t("CreateEditRoomDialog:PublicRoomBarDescription")}
/>
</StyledPublicRoomBarContainer>
)}
<MembersList
@ -251,6 +265,7 @@ const Members = ({
itemCount={membersFilter.total + headersCount + publicRoomItemsLength}
showPublicRoomBar={showPublicRoomBar}
linksBlockLength={publicRoomItemsLength}
withoutTitlesAndLinks={withoutTitlesAndLinks}
>
{publicRoomItems}
{membersList.map((user, index) => {
@ -258,7 +273,7 @@ const Members = ({
<User
t={t}
user={user}
key={user.id}
key={user.id || user.email} // user.email for users added via email
showTooltip={isAdmin}
index={index + publicRoomItemsLength}
membersHelper={membersHelper}

View File

@ -24,16 +24,21 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useCallback, useEffect, useRef, memo } from "react";
import styled, { useTheme } from "styled-components";
import { FixedSizeList as List, areEqual } from "react-window";
import InfiniteLoader from "react-window-infinite-loader";
import React, {
useState,
useCallback,
useEffect,
useRef,
useContext,
} from "react";
import styled from "styled-components";
import { InfiniteLoader, WindowScroller, List } from "react-virtualized";
import { RowLoader } from "@docspace/shared/skeletons/selector";
import { CustomScrollbarsVirtualList } from "@docspace/shared/components/scrollbar";
import { isMobile } from "@docspace/shared/utils";
import { isMobile, mobile } from "@docspace/shared/utils";
import { Text } from "@docspace/shared/components/text";
import { StyledUserTypeHeader } from "../../../styles/members";
import { ScrollbarContext } from "@docspace/shared/components/scrollbar";
const MainStyles = styled.div`
#members-list-header {
@ -41,7 +46,6 @@ const MainStyles = styled.div`
position: fixed;
height: 52px;
width: calc(100% - 32px);
max-width: 440px;
padding: 0;
z-index: 1;
background: ${(props) => props.theme.infoPanel.backgroundColor};
@ -52,23 +56,25 @@ const StyledMembersList = styled.div`
height: 100%;
`;
const Item = memo(({ data, index, style }) => {
const item = data[index];
const StyledList = styled(List)`
width: calc(100% + 20px) !important;
margin-bottom: 24px;
if (!item) {
return (
<div style={{ ...style, width: "calc(100% - 20px)", margin: "0 -16px" }}>
<RowLoader isMultiSelect={false} isContainer={true} isUser={true} />
</div>
);
.members-list-item {
left: unset !important;
inset-inline-start: 0;
width: calc(100% - 20px) !important;
}
return (
<div key={item.id} style={{ ...style, width: "calc(100% - 20px)" }}>
{item}
</div>
);
}, areEqual);
.members-list-loader-item {
margin: 0 -16px;
}
@media ${mobile} {
width: calc(100% + 16px) !important;
margin-bottom: 48px;
}
`;
const itemSize = 48;
@ -77,11 +83,14 @@ const MembersList = (props) => {
hasNextPage,
itemCount,
loadNextPage,
showPublicRoomBar,
linksBlockLength,
withoutTitlesAndLinks,
children,
} = props;
const scrollContext = useContext(ScrollbarContext);
const scrollElement = scrollContext.parentScrollbar?.scrollerElement;
const list = [];
React.Children.map(children, (item) => {
@ -97,56 +106,41 @@ const MembersList = (props) => {
};
});
const { interfaceDirection } = useTheme();
const renderRow = ({ key, index, style }) => {
const item = list[index];
if (!item) {
return (
<div
key={key}
className="members-list-item members-list-loader-item"
style={style}
>
<RowLoader isMultiSelect={false} isContainer={true} isUser={true} />
</div>
);
}
return (
<div className="members-list-item" key={key} style={style}>
{item}
</div>
);
};
const itemsCount = hasNextPage ? list.length + 1 : list.length;
const [isNextPageLoading, setIsNextPageLoading] = useState(false);
const [isMobileView, setIsMobileView] = useState(isMobile());
const [bodyHeight, setBodyHeight] = useState(0);
const bodyRef = useRef(null);
const onBodyResize = useCallback(() => {
if (bodyRef && bodyRef.current) {
const infoPanelContainer =
document.getElementsByClassName("info-panel-scroll");
const containerHeight = infoPanelContainer[0]?.clientHeight ?? 0;
const offsetTop = bodyRef?.current?.offsetTop ?? 0;
const containerMargin = 26; //
const bodyHeight = containerHeight - offsetTop - containerMargin;
setBodyHeight(bodyHeight);
}
if (isMobile()) {
setIsMobileView(true);
} else {
setIsMobileView(false);
}
}, [bodyRef?.current?.offsetHeight]);
useEffect(() => {
window.addEventListener("resize", onBodyResize);
return () => {
window.removeEventListener("resize", onBodyResize);
};
}, []);
useEffect(() => {
onBodyResize();
}, [showPublicRoomBar, list.length]);
const isItemLoaded = useCallback(
(index) => {
({ index }) => {
return !hasNextPage || index < itemsCount;
},
[hasNextPage, itemsCount],
);
const loadMoreItems = useCallback(
async (startIndex) => {
async ({ startIndex }) => {
setIsNextPageLoading(true);
if (!isNextPageLoading) {
await loadNextPage(startIndex - 1);
@ -158,55 +152,86 @@ const MembersList = (props) => {
const onScroll = (e) => {
const header = document.getElementById("members-list-header");
if (!header) {
return;
}
const headerTitle = header.children[0];
const scrollOffset = e.target.scrollTop;
for (let titleIndex in listOfTitles) {
const title = listOfTitles[titleIndex];
const titleOffsetTop = title.index * itemSize;
if (e.scrollOffset > titleOffsetTop) {
if (scrollOffset > titleOffsetTop) {
if (title.displayName) headerTitle.innerText = title.displayName;
header.style.display = "flex";
} else if (e.scrollOffset <= linksBlockLength * itemSize) {
} else if (scrollOffset <= linksBlockLength * itemSize) {
header.style.display = "none";
}
}
};
useEffect(() => {
if (withoutTitlesAndLinks) return;
scrollElement?.addEventListener("scroll", onScroll);
return () => {
scrollElement?.removeEventListener("scroll", onScroll);
};
}, [scrollElement, linksBlockLength, withoutTitlesAndLinks]);
if (!scrollElement) {
return null;
}
return (
<MainStyles>
<StyledUserTypeHeader
id="members-list-header"
className="members-list-header"
>
<Text className="members-list-header_title title" />
</StyledUserTypeHeader>
<StyledMembersList ref={bodyRef}>
<InfiniteLoader
isItemLoaded={isItemLoaded}
itemCount={hasNextPage ? itemCount + 1 : itemCount}
loadMoreItems={loadMoreItems}
{!withoutTitlesAndLinks && (
<StyledUserTypeHeader
id="members-list-header"
className="members-list-header"
>
{({ onItemsRendered, ref }) => {
const listWidth = isMobileView
? "calc(100% + 16px)"
: "calc(100% + 20px)"; // for scroll
<Text className="members-list-header_title title" />
</StyledUserTypeHeader>
)}
<StyledMembersList>
<InfiniteLoader
isRowLoaded={isItemLoaded}
rowCount={itemCount}
loadMoreRows={loadMoreItems}
>
{({ onRowsRendered, registerChild }) => {
return (
<List
direction={interfaceDirection}
ref={ref}
width={listWidth}
height={bodyHeight}
itemCount={itemsCount}
itemSize={itemSize}
itemData={list}
outerElementType={CustomScrollbarsVirtualList}
onItemsRendered={onItemsRendered}
onScroll={onScroll}
>
{Item}
</List>
<WindowScroller scrollElement={scrollElement}>
{({ height, isScrolling, scrollTop }) => {
if (height === undefined) {
height = scrollElement.getBoundingClientRect().height;
}
const width = scrollElement.getBoundingClientRect().width;
return (
<StyledList
autoHeight
height={height}
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={itemsCount}
rowHeight={itemSize}
rowRenderer={renderRow}
width={width}
isScrolling={isScrolling}
overscanRowCount={3}
scrollTop={scrollTop}
// React virtualized sets "LTR" by default.
style={{ direction: "inherit" }}
/>
);
}}
</WindowScroller>
);
}}
</InfiniteLoader>

View File

@ -141,6 +141,16 @@ const StyledLinkRow = styled.div`
}
`;
const ROOMS_ITEM_HEADER_HEIGHT = "80px";
export const StyledPublicRoomBarContainer = styled.div`
position: sticky;
top: ${ROOMS_ITEM_HEADER_HEIGHT};
background: ${(props) => props.theme.backgroundColor};
overflow: hidden;
z-index: 1;
`;
StyledLinkRow.defaultProps = { theme: Base };
export { StyledCrossIcon, LinksBlock, StyledLinkRow };

View File

@ -252,6 +252,7 @@ const FilesMediaViewer = (props) => {
state: {
...location.state,
fromMediaViewer: true,
disableScrollToTop: true,
},
});
},

View File

@ -38,6 +38,7 @@ import withLoading from "SRC_DIR/HOCs/withLoading";
import LoaderSubmenu from "./sub-components/loaderSubmenu";
import { resetSessionStorage } from "../../utils";
import { DeviceType } from "@docspace/shared/enums";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const SubmenuCommon = (props) => {
const {
@ -117,13 +118,7 @@ const SubmenuCommon = (props) => {
data={data}
startSelect={currentTab}
onSelect={(e) => onSelect(e)}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -44,6 +44,7 @@ import ManualBackup from "./backup/manual-backup";
import AutoBackup from "./backup/auto-backup";
import { DeviceType } from "@docspace/shared/enums";
import { isManagement } from "@docspace/shared/utils/common";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const DataManagementWrapper = (props) => {
const {
@ -144,13 +145,7 @@ const DataManagementWrapper = (props) => {
data={data}
startSelect={currentTab}
onSelect={(e) => onSelect(e)}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -44,9 +44,9 @@ import { isMobile, isMobileOnly } from "react-device-detect";
import AppLoader from "@docspace/shared/components/app-loader";
import SSOLoader from "./sub-components/ssoLoader";
import { WebhookConfigsLoader } from "./Webhooks/sub-components/Loaders";
import { DeviceType } from "@docspace/shared/enums";
import PluginSDK from "./PluginSDK";
import { Badge } from "@docspace/shared/components/badge";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const StyledSubmenu = styled(Submenu)`
.sticky {
@ -151,13 +151,7 @@ const DeveloperToolsWrapper = (props) => {
data={data}
startSelect={currentTab}
onSelect={onSelect}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
</Suspense>
);

View File

@ -38,9 +38,9 @@ import ThirdParty from "./ThirdPartyServicesSettings";
import SMTPSettings from "./SMTPSettings";
import DocumentService from "./DocumentService";
import PluginPage from "./Plugins";
import { DeviceType } from "@docspace/shared/enums";
import { Badge } from "@docspace/shared/components/badge";
import { Box } from "@docspace/shared/components/box";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const IntegrationWrapper = (props) => {
const {
@ -136,13 +136,7 @@ const IntegrationWrapper = (props) => {
data={data}
startSelect={currentTab}
onSelect={onSelect}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -40,6 +40,7 @@ import AccessLoader from "./sub-components/loaders/access-loader";
import AuditTrail from "./audit-trail/index.js";
import { resetSessionStorage } from "../../utils";
import { DeviceType } from "@docspace/shared/enums";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const SecurityWrapper = (props) => {
const { t, loadBaseInfo, resetIsInit, currentDeviceType } = props;
@ -110,13 +111,7 @@ const SecurityWrapper = (props) => {
data={data}
startSelect={currentTab}
onSelect={(e) => onSelect(e)}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
);
};

View File

@ -41,7 +41,7 @@ import FileManagement from "./sub-components/file-management";
import InterfaceTheme from "./sub-components/interface-theme";
import { tablet } from "@docspace/shared/utils";
import { DeviceType } from "@docspace/shared/enums";
import { SECTION_HEADER_HEIGHT } from "@docspace/shared/components/section/Section.constants";
const Wrapper = styled.div`
display: flex;
@ -54,6 +54,14 @@ const Wrapper = styled.div`
}
`;
const StyledSubMenu = styled(Submenu)`
> .sticky {
z-index: 201;
margin-inline-end: -17px;
padding-inline-end: 17px;
}
`;
const SectionBodyContent = (props) => {
const { showProfileLoader, profile, currentDeviceType, t } = props;
const navigate = useNavigate();
@ -102,17 +110,11 @@ const SectionBodyContent = (props) => {
return (
<Wrapper>
<MainProfile />
<Submenu
<StyledSubMenu
data={data}
startSelect={currentTab}
onSelect={onSelect}
topProps={
currentDeviceType === DeviceType.desktop
? 0
: currentDeviceType === DeviceType.mobile
? "53px"
: "61px"
}
topProps={SECTION_HEADER_HEIGHT[currentDeviceType]}
/>
</Wrapper>
);

View File

@ -2379,7 +2379,7 @@ class FilesActionStore {
const url = getUrl(id);
window.DocSpace.navigate(url);
window.DocSpace.navigate(url, { state: { disableScrollToTop: true } });
return;
}

View File

@ -156,7 +156,7 @@ class MediaViewerDataStore {
changeUrl = (id) => {
const url = this.getUrl(id);
window.DocSpace.navigate(url);
window.DocSpace.navigate(url, { state: { disableScrollToTop: true } });
};
nextMedia = () => {

View File

@ -19,5 +19,6 @@
"RegisterTitle": "طلب التسجيل",
"RegistrationEmailWatermark": "بريد إلكتروني",
"RememberHelper": "العمر الافتراضي للجلسة هو 20 دقيقة. حدد هذا الخيار لتعيينه على عام واحد. لتعيين القيمة الخاصة بك ، انتقل إلى الإعدادات.",
"ResendCode": "أعد إرسال الرمز"
"ResendCode": "أعد إرسال الرمز",
"UserIsAlreadyRegistered": "المستخدم <1>{{email}}</1> مسجل بالفعل في DocSpace، أدخل كلمة المرور الخاصة بك أو ارجع للمتابعة باستخدام بريد إلكتروني آخر."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Sorğunun qeydiyyatı",
"RegistrationEmailWatermark": "Elektron poçt",
"RememberHelper": "Susmaya görə sessiya müddəti 20 dəqiqədir. Müddəti 1 ilə uzatmaq üçün qutunu klikləyin. Digər müddəti təyin etmək üçün, Ayarlar bölməsinə keçin.",
"ResendCode": "Kodu yenidən göndərin"
"ResendCode": "Kodu yenidən göndərin",
"UserIsAlreadyRegistered": "<1>{{email}}</1> istifadəçisi artıq bu DocSpace-də qeydiyyatdan keçib, parolunuzu daxil edin və ya başqa e-poçtla davam etmək üçün geri qayıdın."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Заявка за регистрация",
"RegistrationEmailWatermark": "Имейл",
"RememberHelper": "Продължителността на сесията по подразбиране е 20 минути. Проверете тази опция, за да я настроите за 1 година. За да зададете собствена стойност, отидете в Настройки.",
"ResendCode": "Код за препращане"
"ResendCode": "Код за препращане",
"UserIsAlreadyRegistered": "Потребителят <1>{{email}}</1> вече е регистриран в този DocSpace, въведете паролата си или се върнете, за да продължите с друг имейл."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Žádost o registraci",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "Výchozí doba trvání relace je 20 minut. Zaškrtnutím této možnosti ji nastavíte na 1 rok. Chcete-li nastavit vlastní hodnotu, přejděte do Nastavení.",
"ResendCode": "Opětovné zaslání kódu"
"ResendCode": "Opětovné zaslání kódu",
"UserIsAlreadyRegistered": "Uživatel <1>{{email}}</1> je již v tomto DocSpace zaregistrován, zadejte své heslo nebo se vraťte zpět a pokračujte jiným e-mailem."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Registrierungsanfrage",
"RegistrationEmailWatermark": "E-Mail",
"RememberHelper": "Lebensdauer der Sitzung ist standardmäßig 20 Minuten. Wählen Sie diese Option aus, um den Wert 1 Jahr zu setzen. Für benutzerdefinierte Werte öffnen Sie Einstellungen.",
"ResendCode": "Code nochmals senden"
"ResendCode": "Code nochmals senden",
"UserIsAlreadyRegistered": "Benutzer <1>{{email}}</1> ist bereits in diesem DocSpace registriert. Geben Sie Ihr Passwort ein oder gehen Sie zurück, um mit einer anderen E-Mail fortzufahren."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Αίτημα εγγραφής",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "Η προεπιλεγμένη διάρκεια περιόδου λειτουργίας είναι 20 λεπτά. Ενεργοποιήστε αυτή την επιλογή για να την ορίσετε σε 1 έτος. Για να ορίσετε τη δική σας τιμή, μεταβείτε στις Ρυθμίσεις.",
"ResendCode": "Επαναποστολή κωδικού"
"ResendCode": "Επαναποστολή κωδικού",
"UserIsAlreadyRegistered": "Ο χρήστης <1>{{email}}</1> είναι ήδη εγγεγραμμένος σε αυτό το DocSpace. Πληκτρολογήστε τον κωδικό πρόσβασής σας ή επιστρέψτε για να συνεχίσετε με άλλο email."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Solicitud de registro",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "La duración de la sesión por defecto es de 20 minutos. Marque esta opción para establecerla en 1 año. Para establecer su propio valor, vaya a Ajustes.",
"ResendCode": "Reenviar código"
"ResendCode": "Reenviar código",
"UserIsAlreadyRegistered": "El usuario <1>{{email}}</1> ya está registrado en este DocSpace, introduzca su contraseña o regrese para continuar con otro correo electrónico."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Rekisteröintipyyntö",
"RegistrationEmailWatermark": "sähköposti",
"RememberHelper": "Istunnon oletuskesto on 20 minuuttia. Valitse tämä vaihtoehto, jos haluat asettaa sen 1 vuodeksi. Voit asettaa oman arvon Asetuksissa.",
"ResendCode": "Lähetä koodi uudelleen"
"ResendCode": "Lähetä koodi uudelleen",
"UserIsAlreadyRegistered": "Käyttäjä <1>{{email}}</1> on jo rekisteröity tähän DocSpaceen, syötä salasanasi tai mene takaisin jatkaaksesi toisella sähköpostilla."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Demande d'inscription",
"RegistrationEmailWatermark": "Adresse de courriel",
"RememberHelper": "Par défaut, la durée de validité de la session est de 20 minutes. Cochez cette option pour la définir sur 1 an. Vous pouvez définir votre propre valeur en accédant aux paramètres.",
"ResendCode": "Renvoyer le code"
"ResendCode": "Renvoyer le code",
"UserIsAlreadyRegistered": "L'utilisateur <1>{{email}}</1> est déjà enregistré dans ce DocSpace, saisissez votre mot de passe ou revenez en arrière pour continuer avec un autre e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Գրանցման հայցում",
"RegistrationEmailWatermark": "Էլ․փոստ",
"RememberHelper": "Նախնական աշխատաշրջանի աշխատաժամը 20 րոպե է: Նշեք այս տարբերակը՝ այն 1 տարի սահմանելու համար: Ձեր սեփական արժեքը սահմանելու համար անցեք Կարգավորումներ:",
"ResendCode": "Կրկին ուղարկել կոդը"
"ResendCode": "Կրկին ուղարկել կոդը",
"UserIsAlreadyRegistered": "Օգտվող <1>{{email}}</1>-ն արդեն գրանցված է այս DocSpace-ում, մուտքագրեք ձեր գաղտնաբառը կամ վերադարձեք՝ շարունակելու մեկ այլ էլ։"
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Richiesta di inscrizione ",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "La durata predefinita della sessione è di 20 minuti. Selezionare questa opzione per impostarla su 1 anno. Per impostare il proprio valore, passare a Impostazioni.",
"ResendCode": "Invia nuovamente il codice"
"ResendCode": "Invia nuovamente il codice",
"UserIsAlreadyRegistered": "L'utente <1>{{email}}</1> è già registrato in questo DocSpace, inserisci la tua password o torna indietro per continuare con un'altra email."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "登録申請",
"RegistrationEmailWatermark": "メール",
"RememberHelper": "デフォルトのセッションライフタイムは20分です。このオプションをチェックすると、1年間に設定されます。独自の値を設定するには、「設定」で設定します。",
"ResendCode": "コードの再送信"
"ResendCode": "コードの再送信",
"UserIsAlreadyRegistered": "ユーザー<1>{{email}}</1>はすでにこのDocSpaceに登録されています。パスワードを入力するか、前に戻って別のメールアドレスでログインしてください。"
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "가입 요청",
"RegistrationEmailWatermark": "이메일",
"RememberHelper": "기본 세션 기간은 20분입니다. 1년으로 설정하려면 이 옵션을 확인하세요. 원하시는 값으로 설정하려면 설정으로 이동하세요.",
"ResendCode": "코드 재전송"
"ResendCode": "코드 재전송",
"UserIsAlreadyRegistered": "<1>{{email}}</1> 사용자는 이미 이 DocSpace에 등록되어 있습니다. 비밀번호를 입력하거나 돌아가서 다른 이메일로 계속 진행하세요."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Reģistrācijas pieprasījums",
"RegistrationEmailWatermark": "E-pasts",
"RememberHelper": "Noklusējuma sesijas ilgums ir 20 minūtes. Atzīmējiet šo opciju, lai iestatītu to uz vienu gadu. Lai iestatītu savu vērtību, dodieties uz Iestatījumi.",
"ResendCode": "Atkārtoti nosūtīt kodu"
"ResendCode": "Atkārtoti nosūtīt kodu",
"UserIsAlreadyRegistered": "Lietotājs <1>{{email}}</1> jau ir reģistrēts šajā DocSpace, ievadiet savu paroli vai dodieties atpakaļ, lai turpinātu ar citu e-pasta adresi."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Registratieverzoek",
"RegistrationEmailWatermark": "E-mail",
"RememberHelper": "De standaard sessieduur is 20 minuten. Vink deze optie aan om deze in te stellen op 1 jaar. Om uw eigen waarde in te stellen, ga naar Instellingen.",
"ResendCode": "Code opnieuw versturen"
"ResendCode": "Code opnieuw versturen",
"UserIsAlreadyRegistered": "Gebruiker <1>{{email}}</1> is al geregistreerd in deze DocSpace, voer uw wachtwoord in of ga terug om verder te gaan met een andere e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Wniosek o rejestrację",
"RegistrationEmailWatermark": "E-mail",
"RememberHelper": "Domyślny czas trwania sesji to 20 min. Zaznacz tę opcję, aby ustawić go na 1 rok. Aby ustawić wartość niestandardową, przejdź do Ustawień.",
"ResendCode": "Wyślij kod jeszcze raz"
"ResendCode": "Wyślij kod jeszcze raz",
"UserIsAlreadyRegistered": "Użytkownik <1>{{email}}</1> jest już zarejestrowany w tym DocSpace, wpisz swoje hasło lub wróć, aby kontynuować z innym adresem e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Pedido de registro",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "A duração padrão da sessão é de 20 minutos. Marque esta opção para defini-la como 1 ano. Para definir seu próprio valor, vá para Configurações",
"ResendCode": "Reenviar código"
"ResendCode": "Reenviar código",
"UserIsAlreadyRegistered": "O usuário <1>{{email}}</1> já está cadastrado neste DocSpace, digite sua senha ou volte para continuar com outro e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Pedido de registo",
"RegistrationEmailWatermark": "E-mail",
"RememberHelper": "A duração da sessão predefinida é de 20 minutos. Clique nesta opção para configurá-la para 1 ano. Para introduzir um período à sua escolha, aceda às definições.",
"ResendCode": "Reenviar código"
"ResendCode": "Reenviar código",
"UserIsAlreadyRegistered": "O usuário <1>{{email}}</1> já está cadastrado neste DocSpace, digite sua senha ou volte para continuar com outro e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Solicitarea de înregistrare",
"RegistrationEmailWatermark": "E-mail",
"RememberHelper": "Durata sesiunii implicită este de 20 de minute. Bifați caseta de selectare pentru a prelungi durata până la un an. Pentru stabilirea perioadei personalizate, accesați Setările.",
"ResendCode": "Retrimite codul"
"ResendCode": "Retrimite codul",
"UserIsAlreadyRegistered": "Utilizatorul <1>{{email}}</1> este deja înregistrat în acest spațiu DocSpace, introduceţi parola dvs sau reveniți pentru a continua cu un alt e-mail."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Запрос на регистрацию",
"RegistrationEmailWatermark": "Регистрационный email",
"RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.",
"ResendCode": "Отправить код повторно"
"ResendCode": "Отправить код повторно",
"UserIsAlreadyRegistered": "Пользователь <1>{{email}}</1> уже зарегистрирован в этом DocSpace. Введите свой пароль или вернитесь назад, чтобы продолжить с другим адресом электронной почты."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Požiadavka registrácie",
"RegistrationEmailWatermark": "E-mail",
"RememberHelper": "Predvolená doba trvania relácie je 20 minút. Začiarknutím tejto možnosti ju nastavíte na 1 rok. Ak chcete nastaviť vlastnú hodnotu, prejdite do časti Nastavenia.",
"ResendCode": "Poslať kód ešte raz"
"ResendCode": "Poslať kód ešte raz",
"UserIsAlreadyRegistered": "Používateľ <1>{{email}}</1> je už zaregistrovaný v tomto priestore DocSpace, zadajte svoje heslo alebo sa vráťte späť a pokračujte s iným e-mailom."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Zahteva za registracijo",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "Privzeta življenjska doba seje je 20 minut. Preverite to možnost, če jo želite nastaviti na 1 leto. Če želite nastaviti poljubno vrednost, pojdite v Nastavitve.",
"ResendCode": "Ponovno pošlji kodo"
"ResendCode": "Ponovno pošlji kodo",
"UserIsAlreadyRegistered": "Uporabnik <1>{{email}}</1> je že registriran v tem prostoru DocSpace. Vnesite svoje geslo ali se vrnite nazaj in nadaljujte z drugim e-mail naslovom."
}

View File

@ -1 +1,20 @@
{}
{
"ErrorConfirmURLError": "Nevažeći email ili istekao link",
"ErrorExpiredActivationLink": "Link je istekao",
"ErrorInvalidActivationLink": "Nevažeći aktivacioni link",
"ErrorNotAllowedOption": "Vaš cenovni plan ne podržava ovu opciju",
"ErrorUserNotFound": "Korisnik nije pronađen",
"InvalidUsernameOrPassword": "Nevažeće korisničko ime ili lozinka",
"LoginWithAccountNotFound": "Ne mogu da pronađem povezani nalog treće strane. Prvo morate da povežete svoj nalog na društvenoj mreži na stranici za uređivanje profila.",
"LoginWithBruteForce": "Ovlašćenje je privremeno blokirano",
"LoginWithBruteForceCaptcha": "Potvrdi da nisi robot",
"RecaptchaInvalid": "Nevažeći Recaptcha",
"SsoAttributesNotFound": "Neuspela autentifikacija (atributi tvrdnje nisu pronađeni)",
"SsoAuthFailed": "Autentifikacija neuspela",
"SsoError": "Interna greška servera",
"SsoSettingsCantCreateUser": "Nije moguće kreirati korisnika sa ovim autentifikacionim tokenom",
"SsoSettingsDisabled": "Jedinstvena prijava (Single sign-on) je onemogućena",
"SsoSettingsEmptyToken": "Autentifikacioni token nije pronađen",
"SsoSettingsNotValidToken": "Nevažeći autentifikacioni token",
"SsoSettingsUserTerminated": "Ovaj korisnik je onemogućen"
}

View File

@ -1 +1,25 @@
{}
{
"CodeSubtitle": "Poslali smo šestocifreni kod na {{email}}. Kod ima ograničen period važenja, pa ga unesite što je pre moguće.",
"CodeTitle": "Kod vam je poslat email-om",
"CookieSettingsTitle": "Trajanje sesije",
"ErrorInvalidText": "Za 10 sekundi bićete preusmereni na <1>DocSpace</1>",
"ExpiredCode": "Ovaj kod više ne važi. Zatražite novi kod i pokušajte ponovo.",
"ForgotPassword": "Zaboravili ste vašu lozinku?",
"InvalidCode": "Ovaj kod je nevažeći. Pokušajte ponovo.",
"MessageAuthorize": "Prijavite se da biste nastavili",
"MessageEmailConfirmed": "Vaša email adresa je uspešno aktivirana.",
"MessageSendPasswordRecoveryInstructionsOnEmail": "Molim vas unesite email adresu koju ste koristili za registraciju. Uputstva za oporavak lozinke će tamo biti poslata.",
"NotFoundCode": "Ne možete pronaći kod? Proverite vaš spam folder.",
"PasswordRecoveryTitle": "Oporavak lozinke",
"RecoverAccess": "Obnovi pristup",
"RecoverContactEmailPlaceholder": "Kontakt email",
"RecoverTextBody": "Ako ne možete da se prijavite sa svojim postojećim nalogom ili želite da se registrujete kao novi korisnik, kontaktirajte administratora portala.",
"Register": "Registrujte se",
"RegisterTextBodyAfterDomainsList": "Da biste se registrovali, unesite svoju email adresu i kliknite na Pošalji zahtev. Poruka sa linkom za aktivaciju vašeg naloga biće poslata na navedenu adresu.",
"RegisterTextBodyBeforeDomainsList": "Registracija je dostupna korisnicima sa email nalogom na",
"RegisterTitle": "Zahtev za registraciju",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "Podrazumevano trajanje sesije je 20 minuta. Označite ovu opciju da biste je postavili na 1 godinu. Da biste podesili sopstvenu vrednost, idite na Podešavanja.",
"ResendCode": "Pošalji ponovo kod",
"UserIsAlreadyRegistered": "Korisnik <1>{{email}}</1> je već registrovan u ovom DocSpace-u, unesite svoju lozinku ili se vratite da biste nastavili sa drugim email-om."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Kayıt talebi",
"RegistrationEmailWatermark": "E-posta",
"RememberHelper": "Varsayılan oturum ömrü 20 dakikadır. 1 yıla ayarlamak için bu seçeneği işaretleyin. Kendi sürenizi ayarlamak için Ayarlar'a gidin.",
"ResendCode": "Kodu yeniden gönder"
"ResendCode": "Kodu yeniden gönder",
"UserIsAlreadyRegistered": "Kullanıcı <1>{{email}}</1> bu DocSpace'e zaten kayıtlı, şifrenizi girin veya başka bir e-posta ile devam etmek için geri dönün."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Запит на реєстрацію",
"RegistrationEmailWatermark": "Електронна пошта",
"RememberHelper": "Термін дії сеансу за замовчуванням складає 20 хвилин. Оберіть цей параметр, щоб задати для нього значення 1 рік. Щоб задати власне значення, перейдіть до налаштувань.",
"ResendCode": "Надіслати код повторно"
"ResendCode": "Надіслати код повторно",
"UserIsAlreadyRegistered": "Користувач <1>{{email}}</1> вже зареєстрований у цьому просторі DocSpace. Введіть свій пароль або поверніться й продовжте з іншою адресою електронної пошти."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "Yêu cầu đăng ký",
"RegistrationEmailWatermark": "Email",
"RememberHelper": "Thời lượng của phiên mặc định là 20 phút. Hãy chọn tùy chọn này để đặt thành 1 năm. Để đặt giá trị của riêng bạn, hãy đi đến Cài đặt.",
"ResendCode": "Gửi lại mã"
"ResendCode": "Gửi lại mã",
"UserIsAlreadyRegistered": "Người dùng <1>{{email}} </1> đã được đăng ký trong DocSpace này, hãy nhập mật khẩu của bạn hoặc quay lại để tiếp tục với một email khác."
}

View File

@ -19,5 +19,6 @@
"RegisterTitle": "注册请求",
"RegistrationEmailWatermark": "邮箱",
"RememberHelper": "默认会话寿命为20分钟。勾选此选项以将其设为1年。如需自行设置其值请前往设置。",
"ResendCode": "重新发送代码"
"ResendCode": "重新发送代码",
"UserIsAlreadyRegistered": "用户 <1>{{email}}</1> 已注册协作空间,请输入密码,或返回使用另一电子邮件继续。"
}

View File

@ -24,119 +24,65 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { permanentRedirect, redirect } from "next/navigation";
import { cookies, headers } from "next/headers";
import { cookies } from "next/headers";
import { Toast } from "@docspace/shared/components/toast";
import { getBaseUrl } from "@docspace/shared/utils/next-ssr-helper";
import { TenantStatus, ThemeKeys } from "@docspace/shared/enums";
import { SYSTEM_THEME_KEY } from "@docspace/shared/constants";
import { ThemeKeys, WhiteLabelLogoType } from "@docspace/shared/enums";
import { getBgPattern, getLogoUrl } from "@docspace/shared/utils/common";
import { Scrollbar } from "@docspace/shared/components/scrollbar";
import { ColorTheme, ThemeId } from "@docspace/shared/components/color-theme";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import { Providers } from "@/providers";
import StyledComponentsRegistry from "@/utils/registry";
import {
checkIsAuthenticated,
getColorTheme,
getSettings,
} from "@/utils/actions";
import SimpleNav from "@/components/SimpleNav";
import { LoginContent, LoginFormWrapper } from "@/components/Login";
import GreetingContainer from "@/components/GreetingContainer";
import { getColorTheme, getSettings } from "@/utils/actions";
import "../../styles/globals.scss";
export default async function RootLayout({
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const baseUrl = getBaseUrl();
const timers = { isAuth: 0, otherOperations: 0 };
const cookieStore = cookies();
const systemTheme = cookieStore.get(SYSTEM_THEME_KEY);
let redirectUrl = "";
const api_host = process.env.API_HOST?.trim();
const startOtherOperationsDate = new Date();
const [settings, colorTheme] = await Promise.all([
getSettings(),
getColorTheme(),
]);
timers.otherOperations =
new Date().getTime() - startOtherOperationsDate.getTime();
const cookieStore = cookies();
if (settings === "access-restricted") redirectUrl = `/${settings}`;
const systemTheme = cookieStore.get(SYSTEM_THEME_KEY)?.value as ThemeKeys;
if (settings === "portal-not-found") {
const config = await (
await fetch(`${baseUrl}/static/scripts/config.json`)
).json();
const hdrs = headers();
const host = hdrs.get("host");
const bgPattern = getBgPattern(colorTheme?.selected);
const url = new URL(
config.wrongPortalNameUrl ??
"https://www.onlyoffice.com/wrongportalname.aspx",
);
const objectSettings = typeof settings === "string" ? undefined : settings;
url.searchParams.append("url", host ?? "");
const isRegisterContainerVisible = objectSettings?.enabledJoin;
redirectUrl = url.toString();
}
const isDark = systemTheme === ThemeKeys.DarkStr;
if (typeof settings !== "string" && settings?.wizardToken) {
redirectUrl = `wizard`;
}
if (
typeof settings !== "string" &&
settings?.tenantStatus === TenantStatus.PortalRestore
) {
redirectUrl = `preparation-portal`;
}
if (
typeof settings !== "string" &&
settings?.tenantStatus === TenantStatus.PortalDeactivate
) {
redirectUrl = `unavailable`;
}
const logoUrl = getLogoUrl(WhiteLabelLogoType.LoginPage, isDark);
return (
<html lang="en" translate="no">
<head>
<link rel="icon" type="image/x-icon" href="/logo.ashx?logotype=3" />
<link rel="mask-icon" href="/logo.ashx?logotype=3" />
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
/>
<meta name="google" content="notranslate" />
</head>
<body>
<StyledComponentsRegistry>
<Providers
value={{
settings: typeof settings !== "string" ? settings : undefined,
colorTheme,
systemTheme: systemTheme?.value as ThemeKeys,
}}
timers={timers}
api_host={api_host}
redirectURL={redirectUrl}
>
<SimpleNav systemTheme={systemTheme?.value as ThemeKeys} />
<Toast isSSR />
{children}
</Providers>
</StyledComponentsRegistry>
</body>
</html>
<div style={{ width: "100%", height: "100%" }}>
<SimpleNav systemTheme={systemTheme} />
<LoginFormWrapper id="login-page" bgPattern={bgPattern}>
<div className="bg-cover" />
<Scrollbar id="customScrollBar">
<LoginContent>
<ColorTheme
themeId={ThemeId.LinkForgotPassword}
isRegisterContainerVisible={isRegisterContainerVisible}
>
<GreetingContainer
logoUrl={logoUrl}
greetingSettings={objectSettings?.greetingSettings}
/>
<FormWrapper id="login-form">{children}</FormWrapper>
</ColorTheme>
</LoginContent>
</Scrollbar>
</LoginFormWrapper>
</div>
);
}

View File

@ -24,94 +24,38 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use server";
import { redirect } from "next/navigation";
import { cookies, headers } from "next/headers";
import { getBaseUrl } from "@docspace/shared/utils/next-ssr-helper";
import { getBgPattern } from "@docspace/shared/utils/common";
import { SYSTEM_THEME_KEY } from "@docspace/shared/constants";
import { ThemeKeys } from "@docspace/shared/enums";
import { getSettings } from "@/utils/actions";
import Login from "@/components/Login";
import { LoginFormWrapper } from "@/components/Login/Login.styled";
import {
getSettings,
getThirdPartyProviders,
getCapabilities,
getSSO,
checkIsAuthenticated,
getColorTheme,
} from "@/utils/actions";
import LoginForm from "@/components/LoginForm";
import ThirdParty from "@/components/ThirdParty";
import RecoverAccess from "@/components/RecoverAccess";
import Register from "@/components/Register";
async function Page({
searchParams,
}: {
searchParams: { [key: string]: string };
}) {
const timers = { isAuth: 0, otherOperations: 0 };
const startOtherOperationsDate = new Date();
const [settings, thirdParty, capabilities, ssoSettings, colorTheme] =
await Promise.all([
getSettings(),
getThirdPartyProviders(),
getCapabilities(),
getSSO(),
getColorTheme(),
]);
timers.otherOperations =
new Date().getTime() - startOtherOperationsDate.getTime();
if (settings === "access-restricted") redirect(`${getBaseUrl()}/${settings}`);
if (settings === "portal-not-found") {
const config = await (
await fetch(`${getBaseUrl()}/static/scripts/config.json`)
).json();
const hdrs = headers();
const host = hdrs.get("host");
const url = new URL(
config.wrongPortalNameUrl ??
"https://www.onlyoffice.com/wrongportalname.aspx",
);
url.searchParams.append("url", host ?? "");
redirect(url.toString());
}
const ssoUrl = capabilities ? capabilities.ssoUrl : "";
const hideAuthPage = ssoSettings ? ssoSettings.hideAuthPage : false;
if (ssoUrl && hideAuthPage && searchParams.skipssoredirect !== "true") {
redirect(ssoUrl);
}
const bgPattern = getBgPattern(colorTheme?.selected);
const cookieStore = cookies();
const systemTheme = cookieStore.get(SYSTEM_THEME_KEY);
async function Page() {
const settings = await getSettings();
return (
<LoginFormWrapper id="login-page" bgPattern={bgPattern}>
<div className="bg-cover" />
<Login
searchParams={searchParams}
capabilities={capabilities}
settings={settings}
thirdPartyProvider={thirdParty}
ssoSettings={ssoSettings}
isAuthenticated={false}
systemTheme={systemTheme?.value as ThemeKeys}
timers={timers}
/>
</LoginFormWrapper>
<Login>
{settings && typeof settings !== "string" && (
<>
<LoginForm
hashSettings={settings?.passwordHash}
cookieSettingsEnabled={settings?.cookieSettingsEnabled}
/>
<ThirdParty />
{settings.enableAdmMess && <RecoverAccess />}
{settings.enabledJoin && (
<Register
id="login_register"
enabledJoin
trustedDomains={settings.trustedDomains}
trustedDomainsType={settings.trustedDomainsType}
isAuthenticated={false}
/>
)}
</>
)}
</Login>
);
}

View File

@ -51,7 +51,7 @@ export default function GlobalError({ error }: { error: Error }) {
const { i18n } = useI18N({ settings });
const { currentDeviceType } = useDeviceType();
const { theme } = useTheme({});
const { theme } = useTheme({ i18n });
const firebaseHelper = useMemo(() => {
return new FirebaseHelper(settings?.firebase ?? ({} as TFirebaseSettings));
}, [settings?.firebase]);

View File

@ -0,0 +1,137 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { cookies, headers } from "next/headers";
import { Toast } from "@docspace/shared/components/toast";
import { getBaseUrl } from "@docspace/shared/utils/next-ssr-helper";
import { TenantStatus, ThemeKeys } from "@docspace/shared/enums";
import { LANGUAGE, SYSTEM_THEME_KEY } from "@docspace/shared/constants";
import StyledComponentsRegistry from "@/utils/registry";
import { Providers } from "@/providers";
import { getColorTheme, getSettings } from "@/utils/actions";
import "../styles/globals.scss";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const baseUrl = getBaseUrl();
const cookieStore = cookies();
const systemTheme = cookieStore.get(SYSTEM_THEME_KEY);
const cookieLng = cookieStore.get(LANGUAGE);
let redirectUrl = "";
const timers = { otherOperations: 0 };
const startOtherOperationsDate = new Date();
const [settings, colorTheme] = await Promise.all([
getSettings(),
getColorTheme(),
]);
timers.otherOperations =
new Date().getTime() - startOtherOperationsDate.getTime();
if (settings === "access-restricted") redirectUrl = `/${settings}`;
if (settings === "portal-not-found") {
const config = await (
await fetch(`${baseUrl}/static/scripts/config.json`)
).json();
const hdrs = headers();
const host = hdrs.get("host");
const url = new URL(
config.wrongPortalNameUrl ??
"https://www.onlyoffice.com/wrongportalname.aspx",
);
url.searchParams.append("url", host ?? "");
redirectUrl = url.toString();
}
if (typeof settings !== "string" && settings?.wizardToken) {
redirectUrl = `wizard`;
}
if (
typeof settings !== "string" &&
settings?.tenantStatus === TenantStatus.PortalRestore
) {
redirectUrl = `preparation-portal`;
}
if (
typeof settings !== "string" &&
settings?.tenantStatus === TenantStatus.PortalDeactivate
) {
redirectUrl = `unavailable`;
}
if (cookieLng && settings && typeof settings !== "string") {
settings.culture = cookieLng.value;
}
return (
<html lang="en" translate="no">
<head>
<link rel="icon" type="image/x-icon" href="/logo.ashx?logotype=3" />
<link rel="mask-icon" href="/logo.ashx?logotype=3" />
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
/>
<meta name="google" content="notranslate" />
</head>
<body>
<StyledComponentsRegistry>
<Providers
value={{
settings: typeof settings === "string" ? undefined : settings,
colorTheme,
systemTheme: systemTheme?.value as ThemeKeys,
}}
redirectURL={redirectUrl}
timers={timers}
>
<Toast isSSR />
{children}
</Providers>
</StyledComponentsRegistry>
</body>
</html>
);
}

View File

@ -29,4 +29,3 @@ import NotFoundError from "@/components/NotFoundError";
export default function NotFound() {
return <NotFoundError />;
}

View File

@ -25,23 +25,50 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
"use client";
import React, { useLayoutEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useSearchParams } from "next/navigation";
import { Text } from "@docspace/shared/components/text";
import { GreetingContainersProps } from "@/types";
import { DEFAULT_PORTAL_TEXT, DEFAULT_ROOM_TEXT } from "@/utils/constants";
import { getInvitationLinkData } from "@/utils";
const GreetingContainer = ({
roomName,
firstName,
lastName,
greetingSettings,
logoUrl,
type,
greetingSettings,
}: GreetingContainersProps) => {
const { t } = useTranslation();
const { t } = useTranslation(["Login"]);
const searchParams = useSearchParams();
const [invitationLinkData, setInvitationLinkData] = useState({
email: "",
roomName: "",
firstName: "",
lastName: "",
type: "",
});
useLayoutEffect(() => {
if (!searchParams) return;
const encodeString = searchParams.get("loginData");
if (!encodeString) return;
const queryParams = getInvitationLinkData(encodeString);
if (!queryParams) return;
setInvitationLinkData(queryParams);
window.history.replaceState({}, document.title, window.location.pathname);
}, [searchParams]);
const { type, roomName, firstName, lastName } = invitationLinkData;
return (
<>

View File

@ -24,9 +24,29 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { notFound } from "next/navigation";
"use client";
export default function NotFoundCatchAll() {
notFound();
}
import React, { createContext, useState } from "react";
export const LoginValueContext = createContext({
isLoading: false,
isModalOpen: false,
});
export const LoginDispatchContext = createContext({
setIsLoading: (value: boolean) => {},
setIsModalOpen: (value: boolean) => {},
});
export const LoginContext = ({ children }: { children: React.ReactNode }) => {
const [isLoading, setIsLoading] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<LoginDispatchContext.Provider value={{ setIsLoading, setIsModalOpen }}>
<LoginValueContext.Provider value={{ isLoading, isModalOpen }}>
{children}
</LoginValueContext.Provider>
</LoginDispatchContext.Provider>
);
};

View File

@ -26,264 +26,20 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
import { ThemeKeys, WhiteLabelLogoType } from "@docspace/shared/enums";
import { PROVIDERS_DATA } from "@docspace/shared/constants";
import {
getBgPattern,
getLoginLink,
getLogoUrl,
getOAuthToken,
} from "@docspace/shared/utils/common";
import RecoverAccessModalDialog from "@docspace/shared/components/recover-access-modal-dialog/RecoverAccessModalDialog";
import { Scrollbar } from "@docspace/shared/components/scrollbar";
import { ColorTheme, ThemeId } from "@docspace/shared/components/color-theme";
import { FormWrapper } from "@docspace/shared/components/form-wrapper";
import { Link, LinkType } from "@docspace/shared/components/link";
import { SocialButtonsGroup } from "@docspace/shared/components/social-buttons-group";
import { Text } from "@docspace/shared/components/text";
import SSOIcon from "PUBLIC_DIR/images/sso.react.svg?url";
import { LoginProps } from "@/types";
import useRecoverDialog from "@/hooks/useRecoverDialog";
import GreetingContainer from "../GreetingContainer";
import Register from "../Register";
import LoginForm from "../LoginForm";
import { LoginContext } from "./Login.context";
import { LoginContent, LoginFormWrapper } from "./Login.styled";
import { LoginValueContext, LoginDispatchContext } from "./Login.context";
const Login = ({
searchParams,
settings,
capabilities,
thirdPartyProvider,
isAuthenticated,
timers,
systemTheme,
}: LoginProps) => {
const [isLoading, setIsLoading] = useState(false);
export {
LoginContent,
LoginFormWrapper,
LoginValueContext,
LoginDispatchContext,
};
const [invitationLinkData, setInvitationLinkData] = useState({
email: "",
roomName: "",
firstName: "",
lastName: "",
type: "",
});
console.log("api res", settings, capabilities, thirdPartyProvider);
const { t } = useTranslation(["Login", "Common"]);
const {
recoverDialogVisible,
recoverDialogEmailPlaceholder,
recoverDialogTextBody,
openRecoverDialog,
closeRecoverDialog,
} = useRecoverDialog({});
useEffect(() => {
console.log("Login page API requests timings:", { ...timers });
}, [timers]);
useEffect(() => {
if (searchParams) {
if (!searchParams.loginData) return;
const fromBinaryStr = (encodeString: string) => {
const decodeStr = atob(encodeString);
const decoder = new TextDecoder();
const charCodeArray = Uint8Array.from(
{ length: decodeStr.length },
(element, index) => decodeStr.charCodeAt(index),
);
return decoder.decode(charCodeArray);
};
const encodeString = searchParams.loginData;
const decodeString = fromBinaryStr(encodeString);
const queryParams = JSON.parse(decodeString);
setInvitationLinkData(queryParams);
window.history.replaceState({}, document.title, window.location.pathname);
}
}, [searchParams]);
const ssoExists = () => {
if (capabilities?.ssoUrl) return true;
else return false;
};
const oauthDataExists = () => {
if (!capabilities?.oauthEnabled) return false;
let existProviders = 0;
if (thirdPartyProvider && thirdPartyProvider.length > 0)
thirdPartyProvider?.map((item) => {
if (!(item.provider in PROVIDERS_DATA)) return;
existProviders++;
});
return !!existProviders;
};
const onSocialButtonClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement | HTMLElement>) => {
const target = e.target as HTMLElement;
let targetElement = target;
if (
!(targetElement instanceof HTMLButtonElement) &&
target.parentElement
) {
targetElement = target.parentElement;
}
const providerName = targetElement.dataset.providername;
let url = targetElement.dataset.url || "";
try {
//Lifehack for Twitter
if (providerName == "twitter") {
url += "authCallback";
}
const tokenGetterWin =
window["AscDesktopEditor"] !== undefined
? (window.location.href = url)
: window.open(
url,
"login",
"width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no,popup=yes",
);
getOAuthToken(tokenGetterWin).then((code) => {
const token = window.btoa(
JSON.stringify({
auth: providerName,
mode: "popup",
callback: "authCallback",
}),
);
if (tokenGetterWin && typeof tokenGetterWin !== "string")
tokenGetterWin.location.href = getLoginLink(token, code);
});
} catch (err) {
console.log(err);
}
},
[],
);
const isDark = systemTheme === ThemeKeys.DarkStr;
const logoUrl = getLogoUrl(WhiteLabelLogoType.LoginPage, isDark);
const ssoProps = ssoExists()
? {
ssoUrl: capabilities?.ssoUrl,
ssoLabel: capabilities?.ssoLabel,
ssoSVG: SSOIcon as string,
}
: {};
const isRegisterContainerVisible = settings?.enabledJoin;
console.log("settings", settings);
return (
<>
<Scrollbar id="customScrollBar">
<LoginContent>
<ColorTheme
themeId={ThemeId.LinkForgotPassword}
isRegisterContainerVisible={isRegisterContainerVisible}
>
<GreetingContainer
roomName={invitationLinkData.roomName}
firstName={invitationLinkData.firstName}
lastName={invitationLinkData.lastName}
logoUrl={logoUrl}
greetingSettings={settings?.greetingSettings}
type={invitationLinkData.type}
/>
<FormWrapper id="login-form">
<LoginForm
isLoading={isLoading}
setIsLoading={setIsLoading}
hashSettings={settings?.passwordHash}
isDesktop={
typeof window !== "undefined" &&
window["AscDesktopEditor"] !== undefined
}
match={searchParams}
openRecoverDialog={openRecoverDialog}
enableAdmMess={settings?.enableAdmMess ?? false}
cookieSettingsEnabled={settings?.cookieSettingsEnabled ?? false}
emailFromInvitation={invitationLinkData.email}
/>
{(oauthDataExists() || ssoExists()) && (
<>
<div className="line">
<Text className="or-label">
{t("Common:orContinueWith")}
</Text>
</div>
<SocialButtonsGroup
providers={thirdPartyProvider}
onClick={onSocialButtonClick}
t={t}
isDisabled={isLoading}
{...ssoProps}
/>
</>
)}
{settings?.enableAdmMess && (
<Link
fontWeight={600}
fontSize="13px"
type={LinkType.action}
isHovered
className="login-link recover-link"
onClick={openRecoverDialog}
>
{t("RecoverAccess")}
</Link>
)}
</FormWrapper>
</ColorTheme>
</LoginContent>
{isRegisterContainerVisible && (
<Register
id="login_register"
enabledJoin
trustedDomains={settings.trustedDomains}
trustedDomainsType={settings.trustedDomainsType}
isAuthenticated={isAuthenticated ?? false}
/>
)}
</Scrollbar>
{recoverDialogVisible && (
<RecoverAccessModalDialog
visible={recoverDialogVisible}
onClose={closeRecoverDialog}
textBody={recoverDialogTextBody}
emailPlaceholderText={recoverDialogEmailPlaceholder}
id="recover-access-modal"
/>
)}
</>
);
const Login = ({ children }: { children: React.ReactNode }) => {
return <LoginContext>{children}</LoginContext>;
};
export default Login;

View File

@ -24,77 +24,91 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useRef, useEffect, useCallback } from "react";
"use client";
import React, {
useState,
useRef,
useEffect,
useCallback,
useContext,
useLayoutEffect,
} from "react";
import { useTranslation } from "react-i18next";
import ReCAPTCHA from "react-google-recaptcha";
import { isMobileOnly } from "react-device-detect";
import { useTheme } from "styled-components";
import { useSearchParams } from "next/navigation";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { PasswordInput } from "@docspace/shared/components/password-input";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { HelpButton } from "@docspace/shared/components/help-button";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkType } from "@docspace/shared/components/link";
import { Button, ButtonSize } from "@docspace/shared/components/button";
import { createPasswordHash } from "@docspace/shared/utils/common";
import { checkIsSSR } from "@docspace/shared/utils";
import { checkPwd } from "@docspace/shared/utils/desktop";
import { login } from "@docspace/shared/utils/loginUtils";
import { toastr } from "@docspace/shared/components/toast";
import { thirdPartyLogin } from "@docspace/shared/api/user";
import { setWithCredentialsStatus } from "@docspace/shared/api/client";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
import { LoginFormProps } from "@/types";
import { getEmailFromInvitation } from "@/utils";
import EmailContainer from "./sub-components/EmailContainer";
import ForgotPasswordModalDialog from "./sub-components/ForgotPasswordModalDialog";
import PasswordContainer from "./sub-components/PasswordContainer";
import ForgotContainer from "./sub-components/ForgotContainer";
import { StyledCaptcha } from "./LoginForm.styled";
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false,
};
import { LoginDispatchContext, LoginValueContext } from "../Login";
const LoginForm = ({
isLoading,
hashSettings,
isDesktop,
match,
setIsLoading,
cookieSettingsEnabled,
recaptchaPublicKey,
emailFromInvitation,
reCaptchaPublicKey,
}: LoginFormProps) => {
const { isLoading, isModalOpen } = useContext(LoginValueContext);
const { setIsLoading } = useContext(LoginDispatchContext);
const searchParams = useSearchParams();
const theme = useTheme();
const { t, ready } = useTranslation(["Login", "Common"]);
const message = searchParams.get("message");
const confirmedEmail = searchParams.get("confirmedEmail");
const authError = searchParams.get("authError");
const loginData = searchParams.get("loginData");
const isDesktop =
typeof window !== "undefined" && window["AscDesktopEditor"] !== undefined;
const [emailFromInvitation, setEmailFromInvitation] = useState(
getEmailFromInvitation(loginData),
);
const [identifier, setIdentifier] = useState(
getEmailFromInvitation(loginData),
);
const [isEmailErrorShow, setIsEmailErrorShow] = useState(false);
const [errorText, setErrorText] = useState("");
const [identifier, setIdentifier] = useState(emailFromInvitation ?? "");
const [passwordValid, setPasswordValid] = useState(true);
const [identifierValid, setIdentifierValid] = useState(true);
const [password, setPassword] = useState("");
const [isDisabled, setIsDisabled] = useState(false);
const [isChecked, setIsChecked] = useState(false);
const [isDialogVisible, setIsDialogVisible] = useState(false);
const [isCaptcha, setIsCaptcha] = useState(false);
const [isCaptchaSuccessful, setIsCaptchaSuccess] = useState(false);
const [isCaptchaError, setIsCaptchaError] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const captchaRef = useRef<ReCAPTCHA>(null);
const { t, ready } = useTranslation(["Login", "Common"]);
const theme = useTheme();
useLayoutEffect(() => {
const email = getEmailFromInvitation(loginData);
const { message, confirmedEmail, authError } = match || {
message: "",
confirmedEmail: "",
authError: "",
};
setIdentifier(email);
setEmailFromInvitation(email);
}, [loginData]);
const authCallback = useCallback(
async (profile: string) => {
@ -157,8 +171,6 @@ const LoginForm = ({
if (confirmedEmail && ready) toastr.success(text);
if (authError && ready) toastr.error(t("Common:ProviderLoginError"));
focusInput();
window.authCallback = authCallback;
}, [message, confirmedEmail, t, ready, authError, authCallback]);
@ -173,11 +185,11 @@ const LoginForm = ({
if (!passwordValid) setPasswordValid(true);
};
const onSubmit = () => {
const onSubmit = useCallback(() => {
//errorText && setErrorText("");
let captchaToken: string | undefined | null = "";
if (recaptchaPublicKey && isCaptcha) {
if (reCaptchaPublicKey && isCaptcha) {
if (!isCaptchaSuccessful) {
setIsCaptchaError(true);
return;
@ -243,7 +255,7 @@ const LoginForm = ({
errorMessage = error;
}
if (recaptchaPublicKey && error?.response?.status === 403) {
if (reCaptchaPublicKey && error?.response?.status === 403) {
setIsCaptcha(true);
}
@ -255,9 +267,19 @@ const LoginForm = ({
setErrorText(errorMessage);
setPasswordValid(!errorMessage);
setIsLoading(false);
focusInput();
});
};
}, [
hashSettings,
identifier,
identifierValid,
isCaptcha,
isCaptchaSuccessful,
isChecked,
isDesktop,
password,
reCaptchaPublicKey,
setIsLoading,
]);
const onBlurEmail = () => {
!identifierValid && setIsEmailErrorShow(true);
@ -270,38 +292,13 @@ const LoginForm = ({
return undefined;
};
const focusInput = () => {
if (inputRef && inputRef.current) {
inputRef.current.focus();
}
};
const onChangePassword = (e: React.ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
onClearErrors();
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
onClearErrors();
!isDisabled && onSubmit();
e.preventDefault();
}
};
const onChangeCheckbox = () => setIsChecked(!isChecked);
const onClick = () => {
setIsDialogVisible(true);
setIsDisabled(true);
};
const onDialogClose = () => {
setIsDialogVisible(false);
setIsDisabled(false);
setIsLoading(false);
};
const onSuccessfullyComplete = () => {
setIsCaptchaSuccess(true);
};
@ -315,6 +312,22 @@ const LoginForm = ({
}
};
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Enter") {
if (isModalOpen) return;
onSubmit();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [isModalOpen, onSubmit]);
const passwordErrorMessage = errorMessage();
return (
@ -330,83 +343,27 @@ const LoginForm = ({
onValidateEmail={onValidateEmail}
/>
<FieldContainer
isVertical
labelVisible={false}
hasError={!passwordValid}
errorMessage={passwordErrorMessage} //TODO: Add wrong password server error
>
<PasswordInput
className="password-input"
simpleView
passwordSettings={settings}
id="login_password"
inputName="password"
placeholder={t("Common:Password")}
hasError={!passwordValid}
inputValue={password}
size={InputSize.large}
scale
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
isAutoFocussed={!!emailFromInvitation}
inputType={InputType.password}
isDisableTooltip
/>
</FieldContainer>
<PasswordContainer
isLoading={isLoading}
emailFromInvitation={emailFromInvitation}
passwordValid={passwordValid}
passwordErrorMessage={passwordErrorMessage}
password={password}
onChangePassword={onChangePassword}
/>
<div className="login-forgot-wrapper">
<div className="login-checkbox-wrapper">
<div className="remember-wrapper">
{!cookieSettingsEnabled && (
<Checkbox
id="login_remember"
className="login-checkbox"
isChecked={isChecked}
onChange={onChangeCheckbox}
label={t("Common:Remember")}
helpButton={
<HelpButton
id="login_remember-hint"
className="help-button"
offsetRight={0}
tooltipContent={
<Text fontSize="12px">{t("RememberHelper")}</Text>
}
tooltipMaxWidth={isMobileOnly ? "240px" : "340px"}
/>
}
/>
)}
</div>
<ForgotContainer
cookieSettingsEnabled={cookieSettingsEnabled}
isChecked={isChecked}
identifier={identifier}
onChangeCheckbox={onChangeCheckbox}
/>
<Link
fontSize="13px"
className="login-link"
type={LinkType.page}
isHovered={false}
onClick={onClick}
id="login_forgot-password-link"
>
{t("ForgotPassword")}
</Link>
</div>
</div>
{isDialogVisible && (
<ForgotPasswordModalDialog
isVisible={isDialogVisible}
userEmail={identifier}
onDialogClose={onDialogClose}
/>
)}
{recaptchaPublicKey && isCaptcha && (
{reCaptchaPublicKey && isCaptcha && (
<StyledCaptcha isCaptchaError={isCaptchaError}>
<div className="captcha-wrapper">
<ReCAPTCHA
sitekey={recaptchaPublicKey}
sitekey={reCaptchaPublicKey}
ref={captchaRef}
theme={theme.isBase ? "light" : "dark"}
onChange={onSuccessfullyComplete}

View File

@ -32,12 +32,12 @@ import { FieldContainer } from "@docspace/shared/components/field-container";
import { Text } from "@docspace/shared/components/text";
import { Link, LinkType } from "@docspace/shared/components/link";
import { IconButton } from "@docspace/shared/components/icon-button";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
import ArrowIcon from "PUBLIC_DIR/images/arrow.left.react.svg?url";
import { DEFAULT_EMAIL_TEXT } from "@/utils/constants";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types";
interface IEmailContainer {
emailFromInvitation?: string;
@ -45,6 +45,7 @@ interface IEmailContainer {
errorText?: string;
identifier: string;
isLoading: boolean;
onChangeLogin: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlurEmail: () => void;
onValidateEmail: (res: TValidate) => undefined;

View File

@ -0,0 +1,117 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { isMobileOnly } from "react-device-detect";
import { Checkbox } from "@docspace/shared/components/checkbox";
import { HelpButton } from "@docspace/shared/components/help-button";
import { Link, LinkType } from "@docspace/shared/components/link";
import { Text } from "@docspace/shared/components/text";
import { LoginDispatchContext } from "@/components/Login";
import ForgotPasswordModalDialog from "./ForgotPasswordModalDialog";
interface IForgotContainer {
cookieSettingsEnabled: boolean;
isChecked: boolean;
identifier: string;
onChangeCheckbox: VoidFunction;
}
const ForgotContainer = ({
cookieSettingsEnabled,
isChecked,
identifier,
onChangeCheckbox,
}: IForgotContainer) => {
const { setIsModalOpen } = useContext(LoginDispatchContext);
const { t } = useTranslation(["Login", "Common"]);
const [isDialogVisible, setIsDialogVisible] = useState(false);
const onClick = () => {
setIsDialogVisible(true);
setIsModalOpen(true);
};
const onDialogClose = () => {
setIsDialogVisible(false);
setIsModalOpen(false);
};
return (
<div className="login-forgot-wrapper">
<div className="login-checkbox-wrapper">
<div className="remember-wrapper">
{!cookieSettingsEnabled && (
<Checkbox
id="login_remember"
className="login-checkbox"
isChecked={isChecked}
onChange={onChangeCheckbox}
label={t("Common:Remember")}
helpButton={
<HelpButton
id="login_remember-hint"
className="help-button"
offsetRight={0}
tooltipContent={
<Text fontSize="12px">{t("RememberHelper")}</Text>
}
tooltipMaxWidth={isMobileOnly ? "240px" : "340px"}
/>
}
/>
)}
</div>
<Link
fontSize="13px"
className="login-link"
type={LinkType.page}
isHovered={false}
onClick={onClick}
id="login_forgot-password-link"
>
{t("ForgotPassword")}
</Link>
</div>
{isDialogVisible && (
<ForgotPasswordModalDialog
isVisible={isDialogVisible}
userEmail={identifier}
onDialogClose={onDialogClose}
/>
)}
</div>
);
};
export default ForgotContainer;

View File

@ -84,7 +84,6 @@ const ForgotPasswordModalDialog = ({
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {
//console.log("onKeyDown", e.key);
if (e.key === "Enter") {
onSendPasswordInstructions();
e.preventDefault();

View File

@ -0,0 +1,92 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { useTranslation } from "react-i18next";
import { FieldContainer } from "@docspace/shared/components/field-container";
import { PasswordInput } from "@docspace/shared/components/password-input";
import { InputSize, InputType } from "@docspace/shared/components/text-input";
interface IPasswordContainer {
passwordValid: boolean;
passwordErrorMessage: string;
password: string;
isLoading: boolean;
emailFromInvitation: string;
onChangePassword: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const settings = {
minLength: 6,
upperCase: false,
digits: false,
specSymbols: false,
};
const PasswordContainer = ({
passwordValid,
passwordErrorMessage,
password,
isLoading,
emailFromInvitation,
onChangePassword,
}: IPasswordContainer) => {
const { t } = useTranslation(["Common"]);
return (
<FieldContainer
isVertical
labelVisible={false}
hasError={!passwordValid}
errorMessage={passwordErrorMessage} //TODO: Add wrong password server error
>
<PasswordInput
className="password-input"
simpleView
passwordSettings={settings}
id="login_password"
inputName="password"
placeholder={t("Common:Password")}
hasError={!passwordValid}
inputValue={password}
size={InputSize.large}
scale
tabIndex={1}
isDisabled={isLoading}
autoComplete="current-password"
onChange={onChangePassword}
isAutoFocussed={!!emailFromInvitation}
inputType={InputType.password}
isDisableTooltip
/>
</FieldContainer>
);
};
export default PasswordContainer;

View File

@ -24,9 +24,49 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useEffect, useLayoutEffect } from "react";
"use client";
const canUseDOM = typeof window !== "undefined";
const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;
import { useTranslation } from "react-i18next";
export default useIsomorphicLayoutEffect;
import { Link, LinkType } from "@docspace/shared/components/link";
import RecoverAccessModalDialog from "@docspace/shared/components/recover-access-modal-dialog/RecoverAccessModalDialog";
import useRecoverDialog from "@/hooks/useRecoverDialog";
const RecoverAccess = () => {
const { t } = useTranslation(["Login", "Common"]);
const {
recoverDialogVisible,
recoverDialogEmailPlaceholder,
recoverDialogTextBody,
openRecoverDialog,
closeRecoverDialog,
} = useRecoverDialog({});
return (
<>
<Link
fontWeight={600}
fontSize="13px"
type={LinkType.action}
isHovered
className="login-link recover-link"
onClick={openRecoverDialog}
>
{t("RecoverAccess")}
</Link>
{recoverDialogVisible && (
<RecoverAccessModalDialog
visible={recoverDialogVisible}
onClose={closeRecoverDialog}
textBody={recoverDialogTextBody}
emailPlaceholderText={recoverDialogEmailPlaceholder}
id="recover-access-modal"
/>
)}
</>
);
};
export default RecoverAccess;

View File

@ -24,7 +24,9 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState } from "react";
"use client";
import React, { useCallback, useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { useTheme } from "styled-components";
@ -35,8 +37,9 @@ import { sendRegisterRequest } from "@docspace/shared/api/settings";
import { RegisterProps } from "@/types";
import RegisterModalDialog from "./sub-components/RegisterModalDialog";
import { LoginDispatchContext } from "../Login";
import RegisterModalDialog from "./sub-components/RegisterModalDialog";
import { StyledRegister } from "./Register.styled";
const Register = (props: RegisterProps) => {
@ -48,6 +51,9 @@ const Register = (props: RegisterProps) => {
id,
} = props;
const { setIsModalOpen } = useContext(LoginDispatchContext);
const [visible, setVisible] = useState(false);
const [loading, setLoading] = useState(false);
@ -62,13 +68,15 @@ const Register = (props: RegisterProps) => {
const onRegisterClick = () => {
setVisible(true);
setIsModalOpen(true);
};
const onRegisterModalClose = () => {
const onRegisterModalClose = useCallback(() => {
setVisible(false);
setEmail("");
setEmailErr(false);
};
setIsModalOpen(false);
}, [setIsModalOpen]);
const onChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e) {
@ -105,7 +113,7 @@ const Register = (props: RegisterProps) => {
onRegisterModalClose();
}
}
}, [email, emailErr]);
}, [email, emailErr, onRegisterModalClose]);
const onKeyDown = React.useCallback(
(e: KeyboardEvent) => {

View File

@ -35,7 +35,7 @@ import { getLogoUrl } from "@docspace/shared/utils/common";
import { Base, Dark } from "@docspace/shared/themes";
import { ThemeKeys, WhiteLabelLogoType } from "@docspace/shared/enums";
const StyledSimpleNav = styled.div<{ isError: boolean }>`
const StyledSimpleNav = styled.div`
display: none;
height: 48px;
align-items: center;
@ -49,7 +49,7 @@ const StyledSimpleNav = styled.div<{ isError: boolean }>`
}
@media ${mobile} {
display: ${(props) => (props.isError ? "none" : "flex")};
display: flex;
}
`;
@ -63,17 +63,8 @@ const SimpleNav = ({ systemTheme }: SimpleNavProps) => {
const isDark = systemTheme === ThemeKeys.DarkStr;
const logoUrl = getLogoUrl(WhiteLabelLogoType.LightSmall, isDark);
const isError = false;
typeof window !== "undefined"
? window?.location?.pathname === "/login/error"
: false;
return (
<StyledSimpleNav
id="login-header"
isError={isError}
theme={isDark ? Dark : Base}
>
<StyledSimpleNav id="login-header" theme={isDark ? Dark : Base}>
<img src={logoUrl} alt="logo-url" />
</StyledSimpleNav>
);

View File

@ -0,0 +1,199 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import React, { useCallback, useEffect, useContext } from "react";
import { useTranslation } from "react-i18next";
import { useSearchParams } from "next/navigation";
import styled from "styled-components";
import { SocialButtonsGroup } from "@docspace/shared/components/social-buttons-group";
import { Text } from "@docspace/shared/components/text";
import { PROVIDERS_DATA } from "@docspace/shared/constants";
import { getOAuthToken, getLoginLink } from "@docspace/shared/utils/common";
import {
TCapabilities,
TGetSsoSettings,
TThirdPartyProvider,
} from "@docspace/shared/api/settings/types";
import { Nullable } from "@docspace/shared/types";
import SSOIcon from "PUBLIC_DIR/images/sso.react.svg?url";
import {
getCapabilities,
getSSO,
getThirdPartyProviders,
} from "@/utils/actions";
import { LoginDispatchContext, LoginValueContext } from "./Login";
const StyledThirdParty = styled.div<{ isVisible: boolean }>`
width: 100%;
height: auto;
`;
const ThirdParty = () => {
const { isLoading } = useContext(LoginValueContext);
const { setIsModalOpen } = useContext(LoginDispatchContext);
const searchParams = useSearchParams();
const { t } = useTranslation(["Login", "Common"]);
const [capabilities, setCapabilities] =
React.useState<Nullable<TCapabilities>>(null);
const [thirdPartyProvider, setThirdPartyProvider] =
React.useState<Nullable<TThirdPartyProvider[]>>(null);
const [ssoSettings, setSsoSettings] =
React.useState<Nullable<TGetSsoSettings>>(null);
const getData = useCallback(async () => {
const [thirdParty, capabilities, ssoSettings] = await Promise.all([
getThirdPartyProviders(),
getCapabilities(),
getSSO(),
]);
if (thirdParty) setThirdPartyProvider(thirdParty);
if (capabilities) setCapabilities(capabilities);
if (ssoSettings) setSsoSettings(ssoSettings);
}, []);
useEffect(() => {
getData();
}, [getData]);
useEffect(() => {
const ssoUrl = capabilities ? capabilities.ssoUrl : "";
const hideAuthPage = ssoSettings ? ssoSettings.hideAuthPage : false;
if (
ssoUrl &&
hideAuthPage &&
searchParams.get("skipssoredirect") !== "true"
) {
window.location.replace(ssoUrl);
}
}, [capabilities, searchParams, ssoSettings]);
const ssoExists = () => {
if (capabilities?.ssoUrl) return true;
else return false;
};
const oauthDataExists = () => {
if (!capabilities?.oauthEnabled) return false;
let existProviders = 0;
if (thirdPartyProvider && thirdPartyProvider.length > 0)
thirdPartyProvider?.map((item) => {
if (!(item.provider in PROVIDERS_DATA)) return;
existProviders++;
});
return !!existProviders;
};
const onSocialButtonClick = useCallback(
(e: React.MouseEvent<Element, MouseEvent>) => {
const target = e.target as HTMLElement;
let targetElement = target;
if (
!(targetElement instanceof HTMLButtonElement) &&
target.parentElement
) {
targetElement = target.parentElement;
}
const providerName = targetElement.dataset.providername;
let url = targetElement.dataset.url || "";
try {
//Lifehack for Twitter
if (providerName == "twitter") {
url += "authCallback";
}
const tokenGetterWin =
window["AscDesktopEditor"] !== undefined
? (window.location.href = url)
: window.open(
url,
"login",
"width=800,height=500,status=no,toolbar=no,menubar=no,resizable=yes,scrollbars=no,popup=yes",
);
getOAuthToken(tokenGetterWin).then((code) => {
const token = window.btoa(
JSON.stringify({
auth: providerName,
mode: "popup",
callback: "authCallback",
}),
);
if (tokenGetterWin && typeof tokenGetterWin !== "string")
tokenGetterWin.location.href = getLoginLink(token, code);
});
} catch (err) {
console.log(err);
}
},
[],
);
const ssoProps = ssoExists()
? {
ssoUrl: capabilities?.ssoUrl,
ssoLabel: capabilities?.ssoLabel,
ssoSVG: SSOIcon as string,
}
: {};
const isVisible = oauthDataExists() || ssoExists();
return (
isVisible && (
<StyledThirdParty isVisible={isVisible}>
<div className="line">
<Text className="or-label">{t("Common:orContinueWith")}</Text>
</div>
<SocialButtonsGroup
providers={thirdPartyProvider ?? undefined}
onClick={onSocialButtonClick}
onMoreAuthToggle={setIsModalOpen}
t={t}
isDisabled={isLoading}
{...ssoProps}
/>
</StyledThirdParty>
)
);
};
export default ThirdParty;

View File

@ -39,38 +39,12 @@ interface UseI18NProps {
}
const useI18N = ({ settings }: UseI18NProps) => {
const [i18n, setI18N] = React.useState<i18n>(
getI18NInstance(settings?.culture ?? "en") ?? ({} as i18n),
);
const isInit = React.useRef(false);
React.useEffect(() => {
if (!settings?.timezone) return;
window.timezone = settings.timezone;
}, [settings?.timezone]);
React.useEffect(() => {
isInit.current = true;
let currentLanguage: string = settings?.culture ?? "en";
const cookieLang = getCookie(LANGUAGE);
if (cookieLang) {
currentLanguage = cookieLang;
} else {
setCookie(LANGUAGE, currentLanguage);
}
currentLanguage = getLanguage(currentLanguage);
const instance = getI18NInstance(currentLanguage);
if (instance) setI18N(instance);
}, [settings?.culture]);
return { i18n };
return { i18n: getI18NInstance(settings?.culture ?? "en") };
};
export default useI18N;

View File

@ -24,26 +24,31 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState } from "react";
import { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { LoginDispatchContext } from "@/components/Login";
const useRecoverDialog = ({}) => {
const [recoverDialogVisible, setRecoverDialogVisible] = useState(false);
const { setIsModalOpen } = useContext(LoginDispatchContext);
const { t } = useTranslation(["Login"]);
const openRecoverDialog = () => {
setRecoverDialogVisible(true);
setIsModalOpen(true);
};
const closeRecoverDialog = () => {
setRecoverDialogVisible(false);
setIsModalOpen(false);
};
const recoverDialogEmailPlaceholder = t(
"Login:RecoverContactEmailPlaceholder",
);
const recoverDialogTextBody = t("Login:RecoverTextBody");
return {

View File

@ -1,3 +1,4 @@
import { i18n } from "i18next";
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
@ -30,26 +31,40 @@ import { Base, Dark, TColorScheme, TTheme } from "@docspace/shared/themes";
import { getEditorTheme, getSystemTheme } from "@docspace/shared/utils";
import { ThemeKeys } from "@docspace/shared/enums";
import { getAppearanceTheme } from "@docspace/shared/api/settings";
import { TGetColorTheme, TSettings } from "@docspace/shared/api/settings/types";
import { TGetColorTheme } from "@docspace/shared/api/settings/types";
import { setCookie } from "@docspace/shared/utils/cookie";
import { SYSTEM_THEME_KEY } from "@docspace/shared/constants";
import useI18N from "./useI18N";
export interface UseThemeProps {
colorTheme?: TGetColorTheme;
settings?: TSettings;
systemTheme?: ThemeKeys;
i18n: i18n;
}
const useTheme = ({ colorTheme, settings }: UseThemeProps) => {
const { i18n } = useI18N({ settings });
const useTheme = ({ colorTheme, systemTheme, i18n }: UseThemeProps) => {
const [currentColorTheme, setCurrentColorTheme] =
React.useState<TColorScheme>({} as TColorScheme);
React.useState<TColorScheme>(() => {
if (!colorTheme) return {} as TColorScheme;
const [theme, setTheme] = React.useState<TTheme>({
...Base,
currentColorScheme: currentColorTheme,
return (
colorTheme.themes.find((theme) => theme.id === colorTheme.selected) ??
({} as TColorScheme)
);
});
const [theme, setTheme] = React.useState<TTheme>(() => {
const currColorTheme = colorTheme
? colorTheme.themes.find((theme) => theme.id === colorTheme.selected) ??
({} as TColorScheme)
: ({} as TColorScheme);
if (systemTheme === ThemeKeys.DarkStr) {
return { ...Dark, currentColorScheme: currColorTheme };
}
return {
...Base,
currentColorScheme: currColorTheme,
};
});
const isRequestRunning = React.useRef(false);
@ -111,7 +126,7 @@ const useTheme = ({ colorTheme, settings }: UseThemeProps) => {
React.useEffect(() => {
getUserTheme();
}, [currentColorTheme, getUserTheme]);
}, [getUserTheme]);
React.useEffect(() => {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

View File

@ -49,5 +49,5 @@ export function middleware(request: NextRequest) {
// See "Matching Paths" below to learn more
export const config = {
matcher: ["/health", "/"],
matcher: ["/health", "/", "/not-found"],
};

View File

@ -48,7 +48,6 @@ export const Providers = ({
children,
value,
timers,
api_host,
redirectURL,
}: {
children: React.ReactNode;
@ -60,13 +59,12 @@ export const Providers = ({
);
React.useEffect(() => {
console.log("Layout API requests timings:", { ...timers });
console.log("API_HOST: ", api_host);
}, [api_host, timers]);
if (redirectURL) window.location.replace(redirectURL);
}, [redirectURL]);
React.useEffect(() => {
if (redirectURL) window.location.replace("/");
}, [redirectURL]);
console.log("Timers:", { ...timers });
}, [timers]);
const { currentDeviceType } = useDeviceType();
@ -74,20 +72,14 @@ export const Providers = ({
settings: value.settings,
});
const { theme } = useTheme({
const { theme, currentColorTheme } = useTheme({
colorTheme: value.colorTheme,
settings: value.settings,
systemTheme: value.systemTheme,
i18n,
});
const currentTheme =
typeof window !== "undefined" || !value.systemTheme
? theme
: value.systemTheme === ThemeKeys.BaseStr
? Base
: Dark;
return (
<ThemeProvider theme={currentTheme}>
<ThemeProvider theme={theme} currentColorScheme={currentColorTheme}>
<I18nextProvider i18n={i18n}>
<ErrorBoundary
user={{} as TUser}

View File

@ -42,12 +42,8 @@ export type TDataContext = {
};
export type GreetingContainersProps = {
roomName?: string;
firstName?: string;
lastName?: string;
greetingSettings?: string;
logoUrl?: string;
type: string;
};
export type LoginProps = {
@ -86,15 +82,8 @@ export type RegisterModalDialogProps = {
};
export type LoginFormProps = {
isLoading: boolean;
setIsLoading: (isLoading: boolean) => void;
hashSettings?: TPasswordHash;
isDesktop: boolean;
match: { [key: string]: string };
openRecoverDialog: () => void;
enableAdmMess: boolean;
recaptchaPublicKey?: string;
emailFromInvitation?: string;
reCaptchaPublicKey?: string;
cookieSettingsEnabled: boolean;
};

View File

@ -143,36 +143,3 @@ export async function getSSO() {
return sso.response as TGetSsoSettings;
}
export const getData = async () => {
const [settings, ...rest] = await Promise.all([
getSettings(),
getVersionBuild(),
getColorTheme(),
]);
if (
settings &&
settings !== "access-restricted" &&
settings !== "portal-not-found" &&
settings.tenantStatus !== TenantStatus.PortalRestore
) {
const response = await Promise.all([
getThirdPartyProviders(),
getCapabilities(),
getSSO(),
]);
return [settings, ...rest, ...response];
}
return [settings, ...rest];
};
export const updateCookie = (name: string, value: string, options: object) => {
"use server";
const cookieStore = cookies();
cookieStore.set(name, value, options);
};

View File

@ -25,7 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { thirdPartyLogin } from "@docspace/shared/utils/loginUtils";
import { TTranslation } from "@docspace/shared/types";
import { Nullable, TTranslation } from "@docspace/shared/types";
import { MessageKey } from "./enums";
@ -103,3 +103,38 @@ export const getMessageKeyTranslate = (t: TTranslation, message: string) => {
return t("Common:Error");
}
};
export const getInvitationLinkData = (encodeString: string) => {
const fromBinaryStr = (encodeString: string) => {
const decodeStr = atob(encodeString);
const decoder = new TextDecoder();
const charCodeArray = Uint8Array.from(
{ length: decodeStr.length },
(element, index) => decodeStr.charCodeAt(index),
);
return decoder.decode(charCodeArray);
};
const decodeString = fromBinaryStr(encodeString);
const queryParams = JSON.parse(decodeString) as {
email: string;
roomName: string;
firstName: string;
lastName: string;
type: string;
};
return queryParams;
};
export const getEmailFromInvitation = (encodeString: Nullable<string>) => {
if (!encodeString) return "";
const queryParams = getInvitationLinkData(encodeString);
if (!queryParams || !queryParams.email) return "";
return queryParams.email;
};

View File

@ -53,4 +53,3 @@ export default function StyledComponentsRegistry({
</StyleSheetManager>
);
}

View File

@ -31,7 +31,7 @@ const ArticleBody = ({ children }: { children: React.ReactNode }) => {
return (
<Scrollbar
className="article-body__scrollbar"
scrollclass="article-scroller"
scrollClass="article-scroller"
>
{children}
</Scrollbar>

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import React, { PropsWithChildren, forwardRef, useContext } from "react";
import { ThemeContext } from "styled-components";

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import styled from "styled-components";
import { tablet, mobile } from "../../../utils";
import { Base } from "../../../themes";

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import React from "react";
import styled from "styled-components";
import { tablet, mobile } from "../../utils";

View File

@ -363,7 +363,7 @@ const MainButtonMobile = (props: MainButtonMobileProps) => {
{isMobile ? (
<Scrollbar
style={{ position: "absolute" }}
scrollclass="section-scroll"
scrollClass="section-scroll"
ref={dropDownRef}
>
{children}

View File

@ -303,6 +303,7 @@ const MediaViewer = (props: MediaViewerProps): JSX.Element | undefined => {
break;
case KeyboardEventKeys.Escape:
event.stopPropagation();
if (!deleteDialogVisible) onClose?.();
break;

View File

@ -55,6 +55,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>`
padding: 4px;
border-radius: 8px !important;
background: transparent !important;
z-index: 201;
@media ${desktop} {
&:hover {
@ -111,8 +112,8 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>`
touch-action: none;
background-color: ${(props) =>
props.color ? props.color : props.theme.scrollbar.bgColor} !important;
z-index: 201;
position: relative;
cursor: default !important;
:hover {
background-color: ${(props) =>
@ -127,7 +128,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>`
}
}
.thumb-vertical {
& > .track > .thumb-vertical {
width: ${({ $fixedSize }) => ($fixedSize ? "8px" : "4px")} !important;
transition: width linear 0.1s;
@ -142,7 +143,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>`
}
}
.thumb-horizontal {
& > .track > .thumb-horizontal {
height: ${({ $fixedSize }) => ($fixedSize ? "8px" : "4px")} !important;
transition: height linear 0.1s;
@ -156,6 +157,51 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>`
height: 4px !important;
}
}
// fix when iframe breaks dragging scroll
&:has(> .track > .dragging) {
iframe {
pointer-events: none;
}
}
// ------- Auto hide styles -------
&.auto-hide {
// tracks hidden by default
.track {
opacity: 0;
transition: opacity 0.35s;
}
// tracks always shown if hovered or thumb dragged
.track:is(:hover, :has(> .dragging)) {
opacity: 1;
}
}
// tracks shown if scroll element was not auto hidden, hovered
// and there is no another nesting hovered scroll element, dragging thumb or backdrop
&.auto-hide.scroll-visible:hover:not(
:has(
&:hover.trackYVisible,
&:hover.trackXVisible,
.thumb.dragging,
.backdrop-active
)
) {
> .track {
opacity: 1;
}
}
// no hover logic for touch devices
@media (hover: none) {
&.auto-hide.scroll-visible:not(:has(.backdrop-active)) {
.track {
opacity: 1;
}
}
}
`;
StyledScrollbar.defaultProps = {

View File

@ -24,10 +24,13 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useEffect, useRef, useState } from "react";
import { useTheme } from "styled-components";
"use client";
import { classNames } from "../../utils";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useTheme } from "styled-components";
import throttle from "lodash/throttle";
import { classNames, isTouchDevice } from "../../utils";
import StyledScrollbar from "./Scrollbar.styled";
import { ScrollbarProps } from "./Scrollbar.types";
@ -36,68 +39,22 @@ import { Scrollbar } from "./custom-scrollbar";
const ScrollbarComponent = React.forwardRef<Scrollbar, ScrollbarProps>(
(props, ref) => {
const {
id,
onScroll,
autoHide = false,
hideTrackTimer = 500,
scrollclass,
autoHide = true,
scrollClass,
fixedSize = false,
className,
...rest
} = props;
const defaultTheme = useTheme();
const interfaceDirection = defaultTheme?.interfaceDirection;
const [isScrolling, setIsScrolling] = useState(false);
const [isMouseOver, setIsMouseOver] = useState(false);
const timerId = useRef<null | ReturnType<typeof setTimeout>>();
const [scrollVisible, setScrollVisible] = useState(false);
const timerId = useRef<null | ReturnType<typeof setTimeout>>(null);
const isRtl = interfaceDirection === "rtl";
const showTrack = () => {
if (timerId.current) clearTimeout(timerId.current);
setIsScrolling(true);
};
const hideTrack = () => {
timerId.current = setTimeout(() => {
setIsScrolling(false);
}, hideTrackTimer);
};
const onScrollStart = () => showTrack();
const onScrollStop = () => {
if (isMouseOver) return;
hideTrack();
};
const onMouseEnter = () => {
showTrack();
setIsMouseOver(true);
};
const onMouseLeave = () => {
hideTrack();
setIsMouseOver(false);
};
const scrollAutoHideHandlers = autoHide
? { onScrollStart, onScrollStop }
: {};
const tracksAutoHideHandlers = autoHide
? { onMouseEnter, onMouseLeave }
: {};
const tracksAutoHideStyles = autoHide
? {
opacity: !isScrolling ? 0 : 1,
transition: "opacity 0.4s ease-in-out",
}
: {};
// onScroll handler placed here on Scroller element to get native event instead of parameters that library put
const renderScroller = React.useCallback(
(libProps: { elementRef?: React.LegacyRef<HTMLDivElement> }) => {
@ -107,13 +64,31 @@ const ScrollbarComponent = React.forwardRef<Scrollbar, ScrollbarProps>(
<div
{...restLibProps}
key="scroll-renderer-div"
className={classNames("scroller", scrollclass || "") || "scroller"}
className={classNames("scroller", scrollClass || "") || "scroller"}
ref={elementRef}
onScroll={onScroll}
/>
);
},
[onScroll, scrollclass],
[onScroll, scrollClass],
);
const showTracks = useMemo(
() =>
throttle(
() => {
setScrollVisible(true);
if (timerId.current) {
clearTimeout(timerId.current);
}
timerId.current = setTimeout(() => setScrollVisible(false), 3000);
},
500,
{ trailing: false },
),
[],
);
useEffect(() => {
@ -122,32 +97,36 @@ const ScrollbarComponent = React.forwardRef<Scrollbar, ScrollbarProps>(
};
}, []);
const autoHideContainerProps = autoHide
? {
onScroll: showTracks,
className: classNames(className, {
"auto-hide": autoHide,
"scroll-visible": autoHide && scrollVisible,
}),
}
: {};
const autoHideContentProps =
autoHide && !isTouchDevice ? { onMouseMove: showTracks } : {};
return (
<StyledScrollbar
{...rest}
id={id}
data-testid="scrollbar"
disableTracksWidthCompensation
$fixedSize={fixedSize}
rtl={isRtl}
{...scrollAutoHideHandlers}
onScrollStart={onScrollStart}
className={className}
wrapperProps={{ className: "scroll-wrapper" }}
scrollerProps={{ renderer: renderScroller }}
contentProps={{ className: "scroll-body" }}
contentProps={{ className: "scroll-body", ...autoHideContentProps }}
thumbYProps={{ className: "thumb thumb-vertical" }}
thumbXProps={{ className: "thumb thumb-horizontal" }}
trackYProps={{
className: "track track-vertical",
style: { ...tracksAutoHideStyles },
...tracksAutoHideHandlers,
}}
trackXProps={{
className: "track track-horizontal",
style: { ...tracksAutoHideStyles },
...tracksAutoHideHandlers,
}}
trackYProps={{ className: "track track-vertical" }}
trackXProps={{ className: "track track-horizontal" }}
ref={ref}
{...autoHideContainerProps}
/>
);
},

View File

@ -29,27 +29,24 @@ import { ScrollbarType } from "./Scrollbar.enums";
export interface ScrollbarProps {
/** Accepts class */
className?: string;
/** This class will be placed on scroller element */
scrollClass?: string;
/** Accepts id */
id?: string;
/** Accepts css style */
style?: React.CSSProperties;
/** Enable tracks auto hiding. */
autoHide?: boolean;
/** Track auto hiding delay in ms. */
hideTrackTimer?: number;
/** Fix scrollbar size. */
fixedSize?: boolean;
/** Disable vertical scrolling. */
noScrollY?: boolean;
/** Disable horizontal scrolling. */
noScrollX?: boolean;
/** Calculating height of content depending on number of lines */
isFullHeight?: boolean;
/** Calculated height of content depending on number of lines in pixels */
fullHeight?: number;
/** Wrap children in context that contains scrollbar instance */
createContext?: boolean;
onScroll?: React.UIEventHandler<HTMLDivElement>;
scrollclass?: string;
children?: React.ReactNode;
}

View File

@ -27,6 +27,9 @@
/* eslint-disable no-bitwise */
/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/sort-comp */
"use client";
import { cnb } from "cnbuilder";
import * as React from "react";
import { DraggableData } from "react-draggable";

View File

@ -182,7 +182,9 @@ class ScrollbarThumb extends React.Component<ScrollbarThumbProps, unknown> {
return;
}
ev.preventDefault();
if (ev.cancelable) {
ev.preventDefault();
}
ev.stopPropagation();
if (!isUndef(ev.offsetX)) {

View File

@ -24,10 +24,18 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
"use client";
import { ScrollbarType } from "./Scrollbar.enums";
import { ScrollbarComponent as Scrollbar } from "./Scrollbar";
import { ScrollbarContext } from "./custom-scrollbar";
import { CustomScrollbarsVirtualList } from "./sub-components";
import type { ScrollbarProps } from "./Scrollbar.types";
export { ScrollbarType };
export { Scrollbar };
export { CustomScrollbarsVirtualList } from "./sub-components/index";
export type { ScrollbarProps } from "./Scrollbar.types";
export {
Scrollbar,
ScrollbarProps,
ScrollbarType,
CustomScrollbarsVirtualList,
ScrollbarContext,
};

View File

@ -24,6 +24,8 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { DeviceType } from "../../enums";
export const SECTION_HEADER_NAME = "SectionHeader";
export const SECTION_FILTER_NAME = "SectionFilter";
export const SECTION_BODY_NAME = "SectionBody";
@ -33,3 +35,9 @@ export const SECTION_INFO_PANEL_BODY_NAME = "InfoPanelBody";
export const SECTION_INFO_PANEL_HEADER_NAME = "InfoPanelHeader";
export const SECTION_WARNING_NAME = "SectionWarning";
export const SECTION_SUBMENU_NAME = "SectionSubmenu";
export const SECTION_HEADER_HEIGHT: Readonly<Record<DeviceType, string>> = {
[DeviceType.desktop]: "69px",
[DeviceType.tablet]: "61px",
[DeviceType.mobile]: "53px",
};

View File

@ -43,6 +43,7 @@ import { TViewAs } from "../../types";
import { Scrollbar } from "../scrollbar";
import DragAndDrop from "../drag-and-drop/DragAndDrop";
import { SectionContainerProps } from "./Section.types";
import { SECTION_HEADER_HEIGHT } from "./Section.constants";
const StyledScrollbar = styled(Scrollbar)<{ $isScrollLocked?: boolean }>`
${({ $isScrollLocked }) =>
@ -507,14 +508,7 @@ const sizeBetweenIcons = "8px";
const StyledSectionContainer = styled.section<SectionContainerProps>`
position: relative;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding: 0 20px 0 0;
`
: css`
padding: 0 0 0 20px;
`}
${(props) => !props.withBodyScroll && "padding-inline-start: 20px;"}
flex-grow: 1;
display: flex;
flex-direction: column;
@ -526,40 +520,51 @@ const StyledSectionContainer = styled.section<SectionContainerProps>`
@media ${tablet} {
width: 100%;
max-width: 100vw !important;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding: 0 16px 0 0;
`
: css`
padding: 0 0 0 16px;
`}
${(props) => !props.withBodyScroll && "padding-inline-start: 16px;"}
${tabletProps};
}
@media ${mobile} {
width: 100vw !important;
max-width: 100vw !important;
padding-inline-start: 16px;
}
.section-scroll > .scroll-body {
display: flex;
flex-direction: column;
padding-inline-start: 20px !important;
@media ${tablet} {
padding-inline-start: 16px !important;
}
}
.section-sticky-container {
position: sticky;
top: 0;
background: ${(props) => props.theme.section.header.backgroundColor};
z-index: 201;
padding-inline: 20px;
margin-inline: -20px -17px;
@media ${tablet} {
padding-inline: 16px;
margin-inline: -16px;
}
}
.progress-bar_container {
position: absolute;
position: fixed;
bottom: 0;
display: grid;
grid-gap: 24px;
margin-bottom: 24px;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: 24px;
left: 0;
`
: css`
margin-right: 24px;
right: 0;
`}
margin-inline-end: 24px;
inset-inline-end: ${(props) =>
props.isInfoPanelVisible ? INFO_PANEL_WIDTH : 0}px;
.layout-progress-bar_wrapper {
position: static;
@ -618,14 +623,6 @@ const StyledSectionContainer = styled.section<SectionContainerProps>`
StyledSectionContainer.defaultProps = { theme: Base };
const StyledSectionFilter = styled.div`
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: 20px;
`
: css`
margin-right: 20px;
`}
@media ${tablet} {
${(props) =>
props.theme.interfaceDirection === "rtl"
@ -652,18 +649,18 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>`
position: relative;
display: flex;
height: 69px;
min-height: 69px;
height: ${SECTION_HEADER_HEIGHT.desktop};
min-height: ${SECTION_HEADER_HEIGHT.desktop};
@media ${tablet} {
height: 61px;
min-height: 61px;
height: ${SECTION_HEADER_HEIGHT.tablet};
min-height: ${SECTION_HEADER_HEIGHT.tablet};
${({ isFormGallery }) =>
isFormGallery &&
css`
height: 69px;
min-height: 69px;
height: ${SECTION_HEADER_HEIGHT.desktop};
min-height: ${SECTION_HEADER_HEIGHT.desktop};
`}
.header-container {
@ -673,19 +670,10 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>`
}
@media ${mobile} {
height: 53px;
min-height: 53px;
height: ${SECTION_HEADER_HEIGHT.mobile};
min-height: ${SECTION_HEADER_HEIGHT.mobile};
}
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding-left: 20px;
`
: css`
padding-right: 20px;
`}
box-sizing: border-box;
${NoUserSelect}
@ -700,28 +688,8 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>`
display: flex;
}
@media ${tablet} {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
padding-left: 16px;
margin-left: 0px;
`
: css`
padding-right: 16px;
margin-right: 0px;
`}
}
@media ${mobile} {
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-left: 0px;
`
: css`
margin-right: 0px;
`}
margin-inline-end: 0;
}
`;
@ -754,13 +722,13 @@ StyledSectionPaging.defaultProps = { theme: Base };
const StyledSectionSubmenu = styled.div`
background: ${(props) => props.theme.section.header.backgroundColor};
width: calc(100% - 20px);
width: 100%;
z-index: 1;
@media ${tablet} {
width: calc(100% + 32px);
position: sticky;
top: 61px;
top: ${SECTION_HEADER_HEIGHT.tablet};
margin: 0 -16px;
& > div {
padding: 0 16px;
@ -769,7 +737,7 @@ const StyledSectionSubmenu = styled.div`
@media ${mobile} {
position: sticky;
top: 53px;
top: ${SECTION_HEADER_HEIGHT.mobile};
}
`;

View File

@ -73,8 +73,11 @@ export interface SectionBodyProps {
export interface SectionContainerProps {
showTwoProgress?: boolean;
isSectionHeaderAvailable: boolean;
isInfoPanelVisible?: boolean;
viewAs?: TViewAs;
children: React.ReactNode;
withBodyScroll: boolean;
currentDeviceType?: DeviceType;
}
export interface SectionFilterProps {

View File

@ -200,31 +200,37 @@ const Section = (props: SectionProps) => {
viewAs={viewAs}
ref={containerRef}
isSectionHeaderAvailable={isSectionHeaderAvailable}
isInfoPanelVisible={isInfoPanelVisible}
showTwoProgress={showTwoProgress}
withBodyScroll={withBodyScroll}
currentDeviceType={currentDeviceType}
>
{isSectionHeaderAvailable &&
currentDeviceType === DeviceType.desktop && (
<SubSectionHeader
className="section-header_header"
isFormGallery={isFormGallery}
>
{sectionHeaderContent}
</SubSectionHeader>
)}
{currentDeviceType !== DeviceType.mobile && (
<div className="section-sticky-container">
{isSectionHeaderAvailable && (
<SubSectionHeader
className="section-header_header"
isFormGallery={isFormGallery}
>
{sectionHeaderContent}
</SubSectionHeader>
)}
{isSectionSubmenuAvailable &&
currentDeviceType === DeviceType.desktop && (
<SubSectionSubmenu>{sectionSubmenuContent}</SubSectionSubmenu>
)}
{isSectionFilterAvailable &&
currentDeviceType === DeviceType.desktop && (
<SubSectionFilter
className="section-header_filter"
viewAs={viewAs}
>
{sectionFilterContent}
</SubSectionFilter>
)}
{isSectionSubmenuAvailable && (
<SubSectionSubmenu>{sectionSubmenuContent}</SubSectionSubmenu>
)}
{isSectionFilterAvailable &&
currentDeviceType === DeviceType.desktop && (
<SubSectionFilter
className="section-header_filter"
viewAs={viewAs}
>
{sectionFilterContent}
</SubSectionFilter>
)}
</div>
)}
{isSectionBodyAvailable && (
<SubSectionBody
@ -239,7 +245,7 @@ const Section = (props: SectionProps) => {
getContextModel={getContextModel}
>
{isSectionHeaderAvailable &&
currentDeviceType !== DeviceType.desktop && (
currentDeviceType === DeviceType.mobile && (
<SubSectionHeader
className="section-body_header"
isFormGallery={isFormGallery}
@ -251,7 +257,7 @@ const Section = (props: SectionProps) => {
<SubSectionWarning>{sectionWarningContent}</SubSectionWarning>
)}
{isSectionSubmenuAvailable &&
currentDeviceType !== DeviceType.desktop && (
currentDeviceType === DeviceType.mobile && (
<SubSectionSubmenu>{sectionSubmenuContent}</SubSectionSubmenu>
)}
{isSectionFilterAvailable &&

View File

@ -47,7 +47,8 @@ const SubInfoPanelBody = ({
ref={scrollRef}
$isScrollLocked={scrollLocked}
noScrollY={scrollLocked}
scrollclass="section-scroll info-panel-scroll"
scrollClass="section-scroll info-panel-scroll"
createContext
>
{children}
</StyledScrollbar>

View File

@ -28,11 +28,8 @@ import React from "react";
// import { inject, observer } from "mobx-react";
import { Scrollbar } from "@docspace/shared/components/scrollbar";
import { ContextMenu } from "@docspace/shared/components/context-menu";
import { DeviceType } from "@docspace/shared/enums";
import {
StyledDropZoneBody,
StyledSpacer,
@ -52,7 +49,6 @@ const SectionBody = React.memo(
isDesktop,
settingsStudio = false,
currentDeviceType,
getContextModel,
}: SectionBodyProps) => {
const focusRef = React.useRef<HTMLDivElement | null>(null);
@ -98,7 +94,7 @@ const SectionBody = React.memo(
React.useEffect(() => {
if (!autoFocus) return;
if (focusRef.current) focusRef.current.focus();
if (focusRef.current) focusRef.current.focus({ preventScroll: true });
}, [autoFocus]);
const focusProps = autoFocus
@ -128,27 +124,12 @@ const SectionBody = React.memo(
className="section-body"
>
{withScroll ? (
currentDeviceType !== DeviceType.mobile ? (
<Scrollbar
id="sectionScroll"
scrollclass="section-scroll"
fixedSize
>
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer />
</div>
</div>
</Scrollbar>
) : (
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer />
</div>
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer />
</div>
)
</div>
) : (
<div className="section-wrapper">
{children}
@ -168,27 +149,12 @@ const SectionBody = React.memo(
className="section-body"
>
{withScroll ? (
currentDeviceType !== DeviceType.mobile ? (
<Scrollbar
id="sectionScroll"
scrollclass="section-scroll"
fixedSize
>
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer className="settings-mobile" />
</div>
</div>
</Scrollbar>
) : (
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer className="settings-mobile" />
</div>
<div className="section-wrapper">
<div className="section-wrapper-content" {...focusProps}>
{children}
<StyledSpacer className="settings-mobile" />
</div>
)
</div>
) : (
<div className="section-wrapper">{children}</div>
)}

View File

@ -25,14 +25,33 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React from "react";
import { Scrollbar } from "../../scrollbar";
import { DeviceType } from "../../../enums";
import { StyledSectionContainer } from "../Section.styled";
import { SectionContainerProps } from "../Section.types";
const SectionContainer = React.forwardRef<
HTMLDivElement,
SectionContainerProps
>((props, forwardRef) => {
return <StyledSectionContainer ref={forwardRef} id="section" {...props} />;
>(({ withBodyScroll, children, currentDeviceType, ...props }, forwardRef) => {
return (
<StyledSectionContainer
ref={forwardRef}
id="section"
withBodyScroll={withBodyScroll}
{...props}
>
{withBodyScroll && currentDeviceType !== DeviceType.mobile ? (
<Scrollbar id="sectionScroll" scrollClass="section-scroll" fixedSize>
{children}
</Scrollbar>
) : (
children
)}
</StyledSectionContainer>
);
});
SectionContainer.displayName = "SectionContainer";

View File

@ -49,6 +49,7 @@ export const SocialButtonsGroup = memo(
ssoSVG,
t,
isDisabled,
onMoreAuthToggle,
}: SocialButtonProps) => {
const [moreAuthVisible, setMoreAuthVisible] = useState(false);
@ -59,10 +60,12 @@ export const SocialButtonsGroup = memo(
const moreAuthClose = () => {
setMoreAuthVisible(false);
onMoreAuthToggle?.(false);
};
const moreAuthOpen = () => {
setMoreAuthVisible(true);
onMoreAuthToggle?.(true);
};
const elements = showingProviders.map((item) => {
const provider = item.provider;

View File

@ -43,6 +43,7 @@ export interface SocialButtonProps {
t: TTranslation;
/** Sets a callback function that is triggered when the button is clicked */
onClick: (e: React.MouseEvent<Element, MouseEvent>) => void | Promise<void>;
onMoreAuthToggle?: (value: boolean) => void;
/** Sets the button to present a disabled state */
isDisabled: boolean;
}

View File

@ -31,7 +31,7 @@ import { Checkbox } from "../../checkbox";
import { Text } from "../../text";
import { IconButton } from "../../icon-button";
import { globalColors } from "../../../themes";
import { globalColors } from "../../../themes/globalColors";
import { StyledTableHeaderCell } from "../Table.styled";
import { TableHeaderCellProps } from "../Table.types";

View File

@ -25,7 +25,8 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import CatalogFolderReactSvgUrl from "PUBLIC_DIR/images/catalog.folder.react.svg?url";
import CatalogUserReactSvgUrl from "PUBLIC_DIR/images/catalog.user.react.svg?url";
// import CatalogUserReactSvgUrl from "PUBLIC_DIR/images/catalog.user.react.svg?url";
import CatalogDocumentsReactSvgUrl from "PUBLIC_DIR/images/catalog.documents.react.svg?url";
import CatalogRoomsReactSvgUrl from "PUBLIC_DIR/images/catalog.rooms.react.svg?url";
import CatalogArchiveReactSvgUrl from "PUBLIC_DIR/images/catalog.archive.react.svg?url";
import CatalogSharedReactSvgUrl from "PUBLIC_DIR/images/catalog.shared.react.svg?url";
@ -89,7 +90,7 @@ const defaultIcon: Record<SizeType, string> = {
const icons: Record<SizeType, Partial<Record<PageUnionType, string>>> = {
16: {
[FolderType.USER]: CatalogUserReactSvgUrl,
[FolderType.USER]: CatalogDocumentsReactSvgUrl,
[FolderType.Rooms]: CatalogRoomsReactSvgUrl,
[FolderType.Archive]: CatalogArchiveReactSvgUrl,
[FolderType.SHARE]: CatalogSharedReactSvgUrl,