Merge branch 'develop' into feature/security-active-sessions
This commit is contained in:
commit
a3a93171e1
30
.github/workflows/update-version.yml
vendored
Normal file
30
.github/workflows/update-version.yml
vendored
Normal 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 }}
|
||||
|
@ -50,7 +50,7 @@
|
||||
"BackupListWarningText": "Jos poistat luettelosta kohteita, myös vastaavat tiedostot poistetaan. Tätä toimintoa ei voi kumota. Jos haluat poistaa kaikki tiedostot, käytä linkkiä:",
|
||||
"BlockingTime": "Estoaika (sek)",
|
||||
"Branding": "Brändäys",
|
||||
"BrandingSectionDescription": "Tarkenna yrityksesi tiedot, lisää linkit Docspace-käyttöliittymässä näkyvät ulkoiset resurssit ja sähköpostiosoitteet.",
|
||||
"BrandingSectionDescription": "Tarkenna yrityksesi tiedot, lisää linkit DocSpace-käyttöliittymässä näkyvät ulkoiset resurssit ja sähköpostiosoitteet.",
|
||||
"BrandingSubtitle": "Käytä tätä vaihtoehtoa tarjotaksesi käyttäjille brändikokemusta.",
|
||||
"BreakpointMobileWarningText": "Tämä osio ei ole käytettävissä mobiiliversiossa",
|
||||
"BreakpointMobileWarningTextPrompt": "Ole hyvä ja käytä työpöytää tai tablettia käyttääksesi <1>{{sectionName}}</1>.",
|
||||
@ -136,7 +136,7 @@
|
||||
"ImportedUsers": "{{selectedUsers}}/{{importedUsers}} käyttäjät tuotiin onnistuneesti.",
|
||||
"ImportFromGoogle": "Tuo Google Workspacesta",
|
||||
"ImportFromNextcloud": "Tuo Nextcloudista",
|
||||
"ImportFromOnlyoffice": "Tuo Onlyoffice Workspacesta",
|
||||
"ImportFromOnlyoffice": "Tuo ONLYOFFICE Workspacesta",
|
||||
"ImportProcessingDescription": "Tietojen siirto on käynnissä. Ole hyvä ja odota.",
|
||||
"ImportSectionDescription": "Valitse osiot tuontia varten. Ne ilmestyvät vastaaviin DocSpacen osioihin.",
|
||||
"IncludedInBusiness": "Sisältyy Business-tilaukseen",
|
||||
|
@ -136,7 +136,7 @@
|
||||
"ImportedUsers": "{{selectedUsers}}/{{importedUsers}} օգտվողները հաջողությամբ ներմուծվեցին:",
|
||||
"ImportFromGoogle": "Ներմուծում Google Workspace-ից",
|
||||
"ImportFromNextcloud": "Ներմուծում Nextcloud-ից",
|
||||
"ImportFromOnlyoffice": "Ներմուծում Onlyoffice Workspace-ից",
|
||||
"ImportFromOnlyoffice": "Ներմուծում ONLYOFFICE Workspace-ից",
|
||||
"ImportProcessingDescription": "Տվյալների տեղափոխումն ընթացքի մեջ է: Խնդրում ենք սպասել.",
|
||||
"ImportSectionDescription": "Ընտրել բաժիններ ներմուծման համար: Դրանք կհայտնվեն DocSpace-ի համապատասխան բաժիններում։",
|
||||
"IncludedInBusiness": "Ներառված է բիզնես պլանում",
|
||||
|
@ -55,5 +55,5 @@
|
||||
"WebhookRemoved": "Webhook-ը հեռացվել է",
|
||||
"Webhooks": "Վեբկեռիկներ",
|
||||
"WebhooksGuide": "Webhooks ուղեցույց",
|
||||
"WebhooksInfo": "Օգտագործեք վեբ-կեռիկներ՝ ձեր կողմից օգտագործվող ցանկացած հավելվածի կամ կայքի վրա հատուկ գործողություններ կատարելու համար՝ հիմնված ONLYOFFICE Docspace-ի տարբեր իրադարձությունների վրա:\nԱյստեղ դուք կարող եք ստեղծել և կառավարել ձեր բոլոր վեբկեռիկները, կարգավորել դրանք և թերթել յուրաքանչյուր վեբ-կեռիկի պատմությունը՝ դրանց կատարողականությունը ստուգելու համար:"
|
||||
"WebhooksInfo": "Օգտագործեք վեբ-կեռիկներ՝ ձեր կողմից օգտագործվող ցանկացած հավելվածի կամ կայքի վրա հատուկ գործողություններ կատարելու համար՝ հիմնված ONLYOFFICE DocSpace-ի տարբեր իրադարձությունների վրա:\nԱյստեղ դուք կարող եք ստեղծել և կառավարել ձեր բոլոր վեբկեռիկները, կարգավորել դրանք և թերթել յուրաքանչյուր վեբ-կեռիկի պատմությունը՝ դրանց կատարողականությունը ստուգելու համար:"
|
||||
}
|
||||
|
@ -63,7 +63,7 @@
|
||||
"EmptyFormSubFolderHeaderText": "이 폴더에는 아직 파일이 없습니다",
|
||||
"EmptyFormSubFolderProgressDescriptionText": "여기에서는 진행 중인 양식(사용자가 작성을 시작했지만 완성하지 않은 양식)을 찾을 수 있습니다.",
|
||||
"EmptyRecycleBin": "휴지통 비우기",
|
||||
"EmptyRootRoomHeader": "Docspace에 오신 것을 환영합니다",
|
||||
"EmptyRootRoomHeader": "DocSpace에 오신 것을 환영합니다",
|
||||
"EmptyScreenFolder": "아직 여기에 문서가 없습니다",
|
||||
"EnableLink": "링크 활성화",
|
||||
"EnableNotifications": "알림 활성화",
|
||||
|
@ -55,5 +55,5 @@
|
||||
"WebhookRemoved": "Webhook removido",
|
||||
"Webhooks": "Webhooks",
|
||||
"WebhooksGuide": "Guia de webhooks",
|
||||
"WebhooksInfo": "Use webhooks para executar ações personalizadas em qualquer aplicativo ou site que você esteja usando com base em vários eventos no ONLYOFFICE Docspace.\nAqui, você pode criar e gerenciar todos os seus webhooks, configurá-los e navegar no histórico de cada webhook para auditar seu desempenho."
|
||||
"WebhooksInfo": "Use webhooks para executar ações personalizadas em qualquer aplicativo ou site que você esteja usando com base em vários eventos no ONLYOFFICE DocSpace.\nAqui, você pode criar e gerenciar todos os seus webhooks, configurá-los e navegar no histórico de cada webhook para auditar seu desempenho."
|
||||
}
|
||||
|
@ -63,7 +63,7 @@
|
||||
"EmptyFormSubFolderHeaderText": "Ainda não há arquivos nesta pasta",
|
||||
"EmptyFormSubFolderProgressDescriptionText": "Aqui você encontrará formulários em andamento, ou seja, formulários que os usuários começaram a preencher, mas não concluíram.",
|
||||
"EmptyRecycleBin": "Esvaziar lixo",
|
||||
"EmptyRootRoomHeader": "Bem-vindo ao Docspace!",
|
||||
"EmptyRootRoomHeader": "Bem-vindo ao DocSpace!",
|
||||
"EmptyScreenFolder": "Ainda não há documentos aqui",
|
||||
"EnableLink": "Ativar link",
|
||||
"EnableNotifications": "Permitir notificações",
|
||||
|
@ -55,5 +55,5 @@
|
||||
"WebhookRemoved": "Eliminare Webhook",
|
||||
"Webhooks": "Webhook-uri",
|
||||
"WebhooksGuide": "Ghid Webhooks",
|
||||
"WebhooksInfo": "Utilizați webhook-uri pentru a efectua acțiuni personalizate pe partea aplicației sau paginii web pe care utilizați pentru diferite evenimente din spațiu ONLYOFFICE Docspace.\nAici puteți să creați și să gestionați toate webhook-urile, să le configurați și să parcurgeți istoricul fiecărui webhook pentru a verifica performanța acestora."
|
||||
"WebhooksInfo": "Utilizați webhook-uri pentru a efectua acțiuni personalizate pe partea aplicației sau paginii web pe care utilizați pentru diferite evenimente din spațiu ONLYOFFICE DocSpace.\nAici puteți să creați și să gestionați toate webhook-urile, să le configurați și să parcurgeți istoricul fiecărui webhook pentru a verifica performanța acestora."
|
||||
}
|
||||
|
@ -55,5 +55,5 @@
|
||||
"WebhookRemoved": "Вебхук удален",
|
||||
"Webhooks": "Вебхуки",
|
||||
"WebhooksGuide": "Руководство по вебхукам",
|
||||
"WebhooksInfo": "Используйте вебхуки для выполнения пользовательских действий на стороне любого используемого вами приложения или веб-сайта на основе различных событий в ONLYOFFICE Docspace.\nЗдесь вы можете создавать все ваши вебхуки и управлять ими, настраивать их и загружать историю каждого вебхука для проверки их эффективности."
|
||||
"WebhooksInfo": "Используйте вебхуки для выполнения пользовательских действий на стороне любого используемого вами приложения или веб-сайта на основе различных событий в ONLYOFFICE DocSpace.\nЗдесь вы можете создавать все ваши вебхуки и управлять ими, настраивать их и загружать историю каждого вебхука для проверки их эффективности."
|
||||
}
|
||||
|
@ -136,7 +136,7 @@
|
||||
"ImportedUsers": "{{selectedUsers}}/{{importedUsers}} korisnici su uvezeni uspešno.",
|
||||
"ImportFromGoogle": "Uvezi sa Google Workspace-a",
|
||||
"ImportFromNextcloud": "Uvezi sa Nextcloud-a",
|
||||
"ImportFromOnlyoffice": "Uvezi sa Onlyoffice Workspace-a",
|
||||
"ImportFromOnlyoffice": "Uvezi sa ONLYOFFICE Workspace-a",
|
||||
"ImportProcessingDescription": "Migracija podataka je u toku. Molimo sačekajte.",
|
||||
"ImportSectionDescription": "Izaberite sekcije za uvoz. One će se pojaviti u odgovarajućim sekcijama DocSpace-a.",
|
||||
"IncludedInBusiness": "Uključeno u Poslovni plan",
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
|
||||
|
@ -116,7 +116,11 @@ const ChangeStorageQuotaDialog = (props) => {
|
||||
|
||||
return (
|
||||
<StyledModalDialog visible={isVisible} onClose={onCloseClick}>
|
||||
<ModalDialog.Header>{t("Common:DisableStorageQuota")}</ModalDialog.Header>
|
||||
<ModalDialog.Header>
|
||||
{isDisableQuota
|
||||
? t("Common:DisableStorageQuota")
|
||||
: t("Common:ManageStorageQuota")}
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
<Text noSelect>
|
||||
{isDisableQuota
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ const CategoryFilterMobile = ({
|
||||
>
|
||||
<Scrollbar
|
||||
style={{ position: "absolute" }}
|
||||
scrollclass="section-scroll"
|
||||
scrollClass="section-scroll"
|
||||
ref={scrollRef}
|
||||
>
|
||||
<DropDownItem
|
||||
|
@ -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}
|
||||
|
@ -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`
|
||||
|
@ -38,6 +38,7 @@ import { RoomIcon } from "@docspace/shared/components/room-icon";
|
||||
import RoomsContextBtn from "./context-btn";
|
||||
import { FolderType, RoomsType } from "@docspace/shared/enums";
|
||||
import { getDefaultAccessUser } from "@docspace/shared/utils/getDefaultAccessUser";
|
||||
import Search from "../../Search";
|
||||
|
||||
const RoomsItemHeader = ({
|
||||
t,
|
||||
@ -52,6 +53,7 @@ const RoomsItemHeader = ({
|
||||
setBufferSelection,
|
||||
isArchive,
|
||||
hasLinks,
|
||||
showSearchBlock,
|
||||
setShowSearchBlock,
|
||||
roomType,
|
||||
}) => {
|
||||
@ -99,6 +101,8 @@ const RoomsItemHeader = ({
|
||||
|
||||
return (
|
||||
<StyledTitle ref={itemTitleRef}>
|
||||
{isRoomMembersPanel && showSearchBlock && <Search />}
|
||||
|
||||
<div className="item-icon">
|
||||
<RoomIcon
|
||||
color={selection.logo?.color}
|
||||
@ -160,6 +164,7 @@ export default inject(
|
||||
infoPanelSelection,
|
||||
roomsView,
|
||||
setIsMobileHidden,
|
||||
showSearchBlock,
|
||||
setShowSearchBlock,
|
||||
} = infoPanelStore;
|
||||
const { externalLinks } = publicRoomStore;
|
||||
@ -176,6 +181,7 @@ export default inject(
|
||||
roomsView,
|
||||
infoPanelSelection,
|
||||
setIsMobileHidden,
|
||||
showSearchBlock,
|
||||
setShowSearchBlock,
|
||||
|
||||
isGracePeriod: currentTariffStatusStore.isGracePeriod,
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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 };
|
||||
|
@ -252,6 +252,7 @@ const FilesMediaViewer = (props) => {
|
||||
state: {
|
||||
...location.state,
|
||||
fromMediaViewer: true,
|
||||
disableScrollToTop: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ const ImportStep = ({
|
||||
setImportOptions({ [name]: checked });
|
||||
};
|
||||
|
||||
const serviceName = "Onlyoffice Workspace";
|
||||
const serviceName = "ONLYOFFICE Workspace";
|
||||
const users =
|
||||
t("Settings:Employees")[0].toUpperCase() + t("Settings:Employees").slice(1);
|
||||
|
||||
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -41,6 +41,7 @@ import AuditTrail from "./audit-trail/index.js";
|
||||
import Sessions from "./sessions/index";
|
||||
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;
|
||||
@ -116,13 +117,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]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -2379,7 +2379,7 @@ class FilesActionStore {
|
||||
|
||||
const url = getUrl(id);
|
||||
|
||||
window.DocSpace.navigate(url);
|
||||
window.DocSpace.navigate(url, { state: { disableScrollToTop: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -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 = () => {
|
||||
|
@ -48,7 +48,7 @@ const Root = dynamic(() => import("@/components/Root"), {
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Onlyoffice DocEditor page",
|
||||
title: "ONLYOFFICE DocEditor page",
|
||||
|
||||
description: "",
|
||||
};
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "طلب التسجيل",
|
||||
"RegistrationEmailWatermark": "بريد إلكتروني",
|
||||
"RememberHelper": "العمر الافتراضي للجلسة هو 20 دقيقة. حدد هذا الخيار لتعيينه على عام واحد. لتعيين القيمة الخاصة بك ، انتقل إلى الإعدادات.",
|
||||
"ResendCode": "أعد إرسال الرمز"
|
||||
"ResendCode": "أعد إرسال الرمز",
|
||||
"UserIsAlreadyRegistered": "المستخدم <1>{{email}}</1> مسجل بالفعل في DocSpace، أدخل كلمة المرور الخاصة بك أو ارجع للمتابعة باستخدام بريد إلكتروني آخر."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "Заявка за регистрация",
|
||||
"RegistrationEmailWatermark": "Имейл",
|
||||
"RememberHelper": "Продължителността на сесията по подразбиране е 20 минути. Проверете тази опция, за да я настроите за 1 година. За да зададете собствена стойност, отидете в Настройки.",
|
||||
"ResendCode": "Код за препращане"
|
||||
"ResendCode": "Код за препращане",
|
||||
"UserIsAlreadyRegistered": "Потребителят <1>{{email}}</1> вече е регистриран в този DocSpace, въведете паролата си или се върнете, за да продължите с друг имейл."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "Αίτημα εγγραφής",
|
||||
"RegistrationEmailWatermark": "Email",
|
||||
"RememberHelper": "Η προεπιλεγμένη διάρκεια περιόδου λειτουργίας είναι 20 λεπτά. Ενεργοποιήστε αυτή την επιλογή για να την ορίσετε σε 1 έτος. Για να ορίσετε τη δική σας τιμή, μεταβείτε στις Ρυθμίσεις.",
|
||||
"ResendCode": "Επαναποστολή κωδικού"
|
||||
"ResendCode": "Επαναποστολή κωδικού",
|
||||
"UserIsAlreadyRegistered": "Ο χρήστης <1>{{email}}</1> είναι ήδη εγγεγραμμένος σε αυτό το DocSpace. Πληκτρολογήστε τον κωδικό πρόσβασής σας ή επιστρέψτε για να συνεχίσετε με άλλο email."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "Գրանցման հայցում",
|
||||
"RegistrationEmailWatermark": "Էլ․փոստ",
|
||||
"RememberHelper": "Նախնական աշխատաշրջանի աշխատաժամը 20 րոպե է: Նշեք այս տարբերակը՝ այն 1 տարի սահմանելու համար: Ձեր սեփական արժեքը սահմանելու համար անցեք Կարգավորումներ:",
|
||||
"ResendCode": "Կրկին ուղարկել կոդը"
|
||||
"ResendCode": "Կրկին ուղարկել կոդը",
|
||||
"UserIsAlreadyRegistered": "Օգտվող <1>{{email}}</1>-ն արդեն գրանցված է այս DocSpace-ում, մուտքագրեք ձեր գաղտնաբառը կամ վերադարձեք՝ շարունակելու մեկ այլ էլ։"
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "登録申請",
|
||||
"RegistrationEmailWatermark": "メール",
|
||||
"RememberHelper": "デフォルトのセッションライフタイムは20分です。このオプションをチェックすると、1年間に設定されます。独自の値を設定するには、「設定」で設定します。",
|
||||
"ResendCode": "コードの再送信"
|
||||
"ResendCode": "コードの再送信",
|
||||
"UserIsAlreadyRegistered": "ユーザー<1>{{email}}</1>はすでにこのDocSpaceに登録されています。パスワードを入力するか、前に戻って別のメールアドレスでログインしてください。"
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "가입 요청",
|
||||
"RegistrationEmailWatermark": "이메일",
|
||||
"RememberHelper": "기본 세션 기간은 20분입니다. 1년으로 설정하려면 이 옵션을 확인하세요. 원하시는 값으로 설정하려면 설정으로 이동하세요.",
|
||||
"ResendCode": "코드 재전송"
|
||||
"ResendCode": "코드 재전송",
|
||||
"UserIsAlreadyRegistered": "<1>{{email}}</1> 사용자는 이미 이 DocSpace에 등록되어 있습니다. 비밀번호를 입력하거나 돌아가서 다른 이메일로 계속 진행하세요."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "Запрос на регистрацию",
|
||||
"RegistrationEmailWatermark": "Регистрационный email",
|
||||
"RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.",
|
||||
"ResendCode": "Отправить код повторно"
|
||||
"ResendCode": "Отправить код повторно",
|
||||
"UserIsAlreadyRegistered": "Пользователь <1>{{email}}</1> уже зарегистрирован в этом DocSpace. Введите свой пароль или вернитесь назад, чтобы продолжить с другим адресом электронной почты."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "Запит на реєстрацію",
|
||||
"RegistrationEmailWatermark": "Електронна пошта",
|
||||
"RememberHelper": "Термін дії сеансу за замовчуванням складає 20 хвилин. Оберіть цей параметр, щоб задати для нього значення 1 рік. Щоб задати власне значення, перейдіть до налаштувань.",
|
||||
"ResendCode": "Надіслати код повторно"
|
||||
"ResendCode": "Надіслати код повторно",
|
||||
"UserIsAlreadyRegistered": "Користувач <1>{{email}}</1> вже зареєстрований у цьому просторі DocSpace. Введіть свій пароль або поверніться й продовжте з іншою адресою електронної пошти."
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
|
@ -19,5 +19,6 @@
|
||||
"RegisterTitle": "注册请求",
|
||||
"RegistrationEmailWatermark": "邮箱",
|
||||
"RememberHelper": "默认会话寿命为20分钟。勾选此选项以将其设为1年。如需自行设置其值,请前往设置。",
|
||||
"ResendCode": "重新发送代码"
|
||||
"ResendCode": "重新发送代码",
|
||||
"UserIsAlreadyRegistered": "用户 <1>{{email}}</1> 已注册协作空间,请输入密码,或返回使用另一电子邮件继续。"
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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]);
|
||||
|
137
packages/login/src/app/layout.tsx
Normal file
137
packages/login/src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -29,4 +29,3 @@ import NotFoundError from "@/components/NotFoundError";
|
||||
export default function NotFound() {
|
||||
return <NotFoundError />;
|
||||
}
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -24,77 +24,92 @@
|
||||
// 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 referenceUrl = searchParams.get("referenceUrl");
|
||||
|
||||
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) => {
|
||||
@ -120,7 +135,8 @@ const LoginForm = ({
|
||||
return window.location.replace(response.confirmUrl);
|
||||
}
|
||||
|
||||
const redirectPath = sessionStorage.getItem("referenceUrl");
|
||||
const redirectPath =
|
||||
referenceUrl || sessionStorage.getItem("referenceUrl");
|
||||
|
||||
if (redirectPath) {
|
||||
sessionStorage.removeItem("referenceUrl");
|
||||
@ -135,7 +151,7 @@ const LoginForm = ({
|
||||
);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
[t, referenceUrl],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -157,8 +173,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 +187,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;
|
||||
@ -221,7 +235,8 @@ const LoginForm = ({
|
||||
login(user, hash, session, captchaToken)
|
||||
.then((res: string | object) => {
|
||||
const isConfirm = typeof res === "string" && res.includes("confirm");
|
||||
const redirectPath = sessionStorage.getItem("referenceUrl");
|
||||
const redirectPath =
|
||||
referenceUrl || sessionStorage.getItem("referenceUrl");
|
||||
if (redirectPath && !isConfirm) {
|
||||
sessionStorage.removeItem("referenceUrl");
|
||||
window.location.href = redirectPath;
|
||||
@ -243,7 +258,7 @@ const LoginForm = ({
|
||||
errorMessage = error;
|
||||
}
|
||||
|
||||
if (recaptchaPublicKey && error?.response?.status === 403) {
|
||||
if (reCaptchaPublicKey && error?.response?.status === 403) {
|
||||
setIsCaptcha(true);
|
||||
}
|
||||
|
||||
@ -255,9 +270,20 @@ const LoginForm = ({
|
||||
setErrorText(errorMessage);
|
||||
setPasswordValid(!errorMessage);
|
||||
setIsLoading(false);
|
||||
focusInput();
|
||||
});
|
||||
};
|
||||
}, [
|
||||
hashSettings,
|
||||
identifier,
|
||||
identifierValid,
|
||||
isCaptcha,
|
||||
isCaptchaSuccessful,
|
||||
isChecked,
|
||||
isDesktop,
|
||||
password,
|
||||
reCaptchaPublicKey,
|
||||
setIsLoading,
|
||||
referenceUrl,
|
||||
]);
|
||||
|
||||
const onBlurEmail = () => {
|
||||
!identifierValid && setIsEmailErrorShow(true);
|
||||
@ -270,38 +296,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 +316,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 +347,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}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -84,7 +84,6 @@ const ForgotPasswordModalDialog = ({
|
||||
|
||||
const onKeyDown = React.useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
//console.log("onKeyDown", e.key);
|
||||
if (e.key === "Enter") {
|
||||
onSendPasswordInstructions();
|
||||
e.preventDefault();
|
||||
|
@ -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;
|
@ -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;
|
@ -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) => {
|
||||
|
@ -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>
|
||||
);
|
||||
|
199
packages/login/src/components/ThirdParty.tsx
Normal file
199
packages/login/src/components/ThirdParty.tsx
Normal 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;
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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)");
|
||||
|
@ -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"],
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -53,4 +53,3 @@ export default function StyledComponentsRegistry({
|
||||
</StyleSheetManager>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ const ArticleBody = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<Scrollbar
|
||||
className="article-body__scrollbar"
|
||||
scrollclass="article-scroller"
|
||||
scrollClass="article-scroller"
|
||||
>
|
||||
{children}
|
||||
</Scrollbar>
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -363,7 +363,7 @@ const MainButtonMobile = (props: MainButtonMobileProps) => {
|
||||
{isMobile ? (
|
||||
<Scrollbar
|
||||
style={{ position: "absolute" }}
|
||||
scrollclass="section-scroll"
|
||||
scrollClass="section-scroll"
|
||||
ref={dropDownRef}
|
||||
>
|
||||
{children}
|
||||
|
@ -303,6 +303,7 @@ const MediaViewer = (props: MediaViewerProps): JSX.Element | undefined => {
|
||||
break;
|
||||
|
||||
case KeyboardEventKeys.Escape:
|
||||
event.stopPropagation();
|
||||
if (!deleteDialogVisible) onClose?.();
|
||||
break;
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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)) {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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",
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user