Merge branch 'develop' into feature/templates

This commit is contained in:
Nikita Gopienko 2024-05-24 14:14:11 +03:00
commit 344dd6a914
129 changed files with 1991 additions and 1181 deletions

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

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

View File

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

View File

@ -136,7 +136,7 @@
"ImportedUsers": "{{selectedUsers}}/{{importedUsers}} օգտվողները հաջողությամբ ներմուծվեցին:",
"ImportFromGoogle": "Ներմուծում Google Workspace-ից",
"ImportFromNextcloud": "Ներմուծում Nextcloud-ից",
"ImportFromOnlyoffice": "Ներմուծում Onlyoffice Workspace-ից",
"ImportFromOnlyoffice": "Ներմուծում ONLYOFFICE Workspace-ից",
"ImportProcessingDescription": "Տվյալների տեղափոխումն ընթացքի մեջ է: Խնդրում ենք սպասել.",
"ImportSectionDescription": "Ընտրել բաժիններ ներմուծման համար: Դրանք կհայտնվեն DocSpace-ի համապատասխան բաժիններում։",
"IncludedInBusiness": "Ներառված է բիզնես պլանում",

View File

@ -55,5 +55,5 @@
"WebhookRemoved": "Webhook-ը հեռացվել է",
"Webhooks": "Վեբկեռիկներ",
"WebhooksGuide": "Webhooks ուղեցույց",
"WebhooksInfo": "Օգտագործեք վեբ-կեռիկներ՝ ձեր կողմից օգտագործվող ցանկացած հավելվածի կամ կայքի վրա հատուկ գործողություններ կատարելու համար՝ հիմնված ONLYOFFICE Docspace-ի տարբեր իրադարձությունների վրա:\nԱյստեղ դուք կարող եք ստեղծել և կառավարել ձեր բոլոր վեբկեռիկները, կարգավորել դրանք և թերթել յուրաքանչյուր վեբ-կեռիկի պատմությունը՝ դրանց կատարողականությունը ստուգելու համար:"
"WebhooksInfo": "Օգտագործեք վեբ-կեռիկներ՝ ձեր կողմից օգտագործվող ցանկացած հավելվածի կամ կայքի վրա հատուկ գործողություններ կատարելու համար՝ հիմնված ONLYOFFICE DocSpace-ի տարբեր իրադարձությունների վրա:\nԱյստեղ դուք կարող եք ստեղծել և կառավարել ձեր բոլոր վեբկեռիկները, կարգավորել դրանք և թերթել յուրաքանչյուր վեբ-կեռիկի պատմությունը՝ դրանց կատարողականությունը ստուգելու համար:"
}

View File

@ -63,7 +63,7 @@
"EmptyFormSubFolderHeaderText": "이 폴더에는 아직 파일이 없습니다",
"EmptyFormSubFolderProgressDescriptionText": "여기에서는 진행 중인 양식(사용자가 작성을 시작했지만 완성하지 않은 양식)을 찾을 수 있습니다.",
"EmptyRecycleBin": "휴지통 비우기",
"EmptyRootRoomHeader": "Docspace에 오신 것을 환영합니다",
"EmptyRootRoomHeader": "DocSpace에 오신 것을 환영합니다",
"EmptyScreenFolder": "아직 여기에 문서가 없습니다",
"EnableLink": "링크 활성화",
"EnableNotifications": "알림 활성화",

View File

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

View File

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

View File

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

View File

@ -55,5 +55,5 @@
"WebhookRemoved": "Вебхук удален",
"Webhooks": "Вебхуки",
"WebhooksGuide": "Руководство по вебхукам",
"WebhooksInfo": "Используйте вебхуки для выполнения пользовательских действий на стороне любого используемого вами приложения или веб-сайта на основе различных событий в ONLYOFFICE Docspace.\nЗдесь вы можете создавать все ваши вебхуки и управлять ими, настраивать их и загружать историю каждого вебхука для проверки их эффективности."
"WebhooksInfo": "Используйте вебхуки для выполнения пользовательских действий на стороне любого используемого вами приложения или веб-сайта на основе различных событий в ONLYOFFICE DocSpace.\nЗдесь вы можете создавать все ваши вебхуки и управлять ими, настраивать их и загружать историю каждого вебхука для проверки их эффективности."
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -128,7 +128,7 @@ export const SortDropdownItem = styled(DropDownItem)`
`}
${({ isDescending }) =>
isDescending &&
!isDescending &&
css`
.sortorder-arrow {
transform: rotate(180deg);

View File

@ -366,7 +366,7 @@ const StyledTileContainer = styled.div`
display: none;
cursor: pointer;
${(props) =>
props.isDesc &&
!props.isDesc &&
css`
transform: rotate(180deg);
`}

View File

@ -32,3 +32,4 @@ export { default as useGroups } from "./useGroups";
export { default as useSettings } from "./useSettings";
export { default as usePublic } from "./usePublic";
export { default as useInsideGroup } from "./useInsideGroup";
export { default as useAccountsHotkeys } from "./useAccountsHotkeys";

View File

@ -0,0 +1,109 @@
// (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 { useEffect, useState, useCallback } from "react";
import { useHotkeys, Options } from "react-hotkeys-hook";
import throttle from "lodash/throttle";
import { checkDialogsOpen } from "@docspace/shared/utils/checkDialogsOpen";
interface AccountsHotkeysProps {
enabledHotkeys: boolean;
accountsIsIsLoading: boolean;
selectBottom: () => void;
selectUpper: () => void;
activateHotkeys: (e: KeyboardEvent) => void;
}
const useAccountsHotkeys = ({
enabledHotkeys,
accountsIsIsLoading,
selectBottom,
selectUpper,
activateHotkeys,
}: AccountsHotkeysProps) => {
const [isEnabled, setIsEnabled] = useState(true);
const hotkeysFilter = {
filter: (ev) => {
const eElement = ev.target as HTMLElement;
const eInputElement = ev.target as HTMLInputElement;
return (
eInputElement?.type === "checkbox" || eElement?.tagName !== "INPUT"
);
},
filterPreventDefault: false,
enableOnTags: ["INPUT"],
enabled: enabledHotkeys && !accountsIsIsLoading && isEnabled,
} as Options;
const onKeyDown = useCallback(
(e: KeyboardEvent) => {
const someDialogIsOpen = checkDialogsOpen();
setIsEnabled(!someDialogIsOpen);
activateHotkeys(e);
},
[activateHotkeys],
);
useEffect(() => {
const throttledKeyDownEvent = throttle(onKeyDown, 300);
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keypress", throttledKeyDownEvent);
};
}, [onKeyDown]);
useHotkeys(
"*",
(e) => {
const someDialogIsOpen = checkDialogsOpen();
if (e.shiftKey || e.ctrlKey || someDialogIsOpen) return;
switch (e.key) {
case "ArrowDown":
case "j": {
return selectBottom();
}
case "ArrowUp":
case "k": {
return selectUpper();
}
default:
break;
}
},
hotkeysFilter,
);
};
export default useAccountsHotkeys;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -84,7 +84,7 @@ const GroupsRow = ({
isActive={isActive}
className={`group-item row-wrapper ${
isChecked || isActive ? "row-selected" : ""
}`}
} ${item.id}`}
value={item.id}
>
<div className={"group-item"}>

View File

@ -90,7 +90,7 @@ const GroupsTableItem = ({
<Styled.GroupsRowWrapper
className={`group-item ${
(isChecked || isActive) && "table-row-selected"
}`}
} ${item.id}`}
value={value}
>
<Styled.GroupsRow

View File

@ -146,7 +146,7 @@ const SimpleUserRow = (props) => {
<StyledWrapper
className={`user-item row-wrapper ${
isChecked || isActive ? "row-selected" : ""
}`}
} ${item.id}`}
value={value}
checked={isChecked}
isActive={isActive}

View File

@ -469,7 +469,7 @@ const InsideGroupTableRow = (props) => {
<StyledWrapper
className={`user-item ${
isChecked || isActive ? "table-row-selected" : ""
}`}
} ${item.id}`}
value={value}
>
<StyledPeopleRow

View File

@ -146,7 +146,7 @@ const SimpleUserRow = (props) => {
<StyledWrapper
className={`user-item row-wrapper ${
isChecked || isActive ? "row-selected" : ""
}`}
} ${item.id}`}
value={value}
checked={isChecked}
isActive={isActive}

View File

@ -472,7 +472,7 @@ const PeopleTableRow = (props) => {
<StyledWrapper
className={`user-item ${
isChecked || isActive ? "table-row-selected" : ""
}`}
} ${item.id}`}
value={value}
>
<StyledPeopleRow

View File

@ -35,6 +35,7 @@ import InsideGroup from "./InsideGroup";
import { withTranslation } from "react-i18next";
import { Consumer } from "@docspace/shared/utils";
import withLoader from "SRC_DIR/HOCs/withLoader";
import { useAccountsHotkeys } from "../../Hooks";
const SectionBodyContent = (props) => {
const {
@ -47,11 +48,26 @@ const SectionBodyContent = (props) => {
setGroupsBufferSelection,
setChangeOwnerDialogVisible,
selectUser,
enabledHotkeys,
accountsIsIsLoading,
selectBottom,
selectUpper,
activateHotkeys,
setHotkeyCaretStart,
setHotkeyCaret,
} = props;
const location = useLocation();
const { groupId } = useParams();
useAccountsHotkeys({
enabledHotkeys,
accountsIsIsLoading,
selectBottom,
selectUpper,
activateHotkeys,
});
useEffect(() => {
window.addEventListener("mousedown", onMouseDown);
@ -85,6 +101,8 @@ const SectionBodyContent = (props) => {
setPeopleBufferSelection(null);
setGroupsBufferSelection(null);
window?.getSelection()?.removeAllRanges();
setHotkeyCaretStart(null);
setHotkeyCaret(null);
}
};
@ -110,7 +128,12 @@ const SectionBodyContent = (props) => {
};
export default inject(({ peopleStore }) => {
const { viewAs: accountsViewAs, filterStore } = peopleStore;
const {
viewAs: accountsViewAs,
filterStore,
enabledHotkeys,
setEnabledHotkeys,
} = peopleStore;
const { isFiltered } = filterStore;
const {
@ -125,6 +148,15 @@ export default inject(({ peopleStore }) => {
} = peopleStore.groupsStore;
const { setChangeOwnerDialogVisible } = peopleStore.dialogStore;
const { accountsIsIsLoading } = peopleStore.usersStore;
const {
selectBottom,
selectUpper,
activateHotkeys,
setHotkeyCaretStart,
setHotkeyCaret,
} = peopleStore.accountsHotkeysStore;
return {
accountsViewAs,
@ -135,6 +167,15 @@ export default inject(({ peopleStore }) => {
setGroupsBufferSelection,
setChangeOwnerDialogVisible,
selectUser,
enabledHotkeys,
accountsIsIsLoading,
selectBottom,
selectUpper,
activateHotkeys,
setEnabledHotkeys,
setHotkeyCaretStart,
setHotkeyCaret,
};
})(
withTranslation(["People", "Common", "PeopleTranslations"])(

View File

@ -146,7 +146,7 @@ const StyledTileContainer = styled.div`
display: none;
cursor: pointer;
${(props) =>
props.isDesc &&
!props.isDesc &&
css`
transform: rotate(180deg);
`}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,270 @@
// (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 { makeAutoObservable } from "mobx";
import { isMobile } from "@docspace/shared/utils";
import { checkDialogsOpen } from "@docspace/shared/utils/checkDialogsOpen";
import { TUser, TUserGroup } from "@docspace/shared/api/people/types";
type AccountsType = TUser | TUserGroup;
class AccountsHotkeysStore {
peopleStore;
hotkeyCaret: AccountsType | null = null;
hotkeyCaretStart: AccountsType | null = null;
elemOffset: number = 0;
constructor(peopleStore: any) {
this.peopleStore = peopleStore;
makeAutoObservable(this);
}
get isAccountsPage() {
const groupId = new URLSearchParams(window.location.search).get("group");
return window.location.pathname.includes("/accounts/people") || groupId;
}
get accountsList() {
return this.isAccountsPage
? this.peopleStore.usersStore.peopleList
: this.peopleStore.groupsStore.groups;
}
get accountsSelection() {
return this.isAccountsPage
? this.peopleStore.selectionStore.selection
: this.peopleStore.groupsStore.selection;
}
get caretIndex() {
const item = this.hotkeyCaret
? this.hotkeyCaret
: this.accountsSelection.length
? this.accountsSelection.length === 1
? this.accountsSelection[0]
: this.accountsSelection[this.accountsSelection.length - 1]
: null;
const caretIndex = this.accountsList.findIndex((f) => f.id === item?.id);
if (caretIndex !== -1) return caretIndex;
return null;
}
get prevFile() {
if (this.caretIndex !== -1) {
const prevCaretIndex = this.caretIndex - 1;
return this.accountsList[prevCaretIndex];
}
return null;
}
get nextFile() {
if (this.caretIndex !== -1) {
const nextCaretIndex = this.caretIndex + 1;
return this.accountsList[nextCaretIndex];
}
return null;
}
selectBottom = () => {
if (!this.hotkeyCaret && !this.accountsSelection.length)
return this.selectFirstFile();
if (this.nextFile) this.setSelectionWithCaret([this.nextFile]);
};
selectUpper = () => {
if (!this.hotkeyCaret && !this.accountsSelection.length)
return this.selectFirstFile();
if (this.prevFile) this.setSelectionWithCaret([this.prevFile]);
};
setSelection = (selection: AccountsType[]) => {
return this.isAccountsPage
? this.peopleStore.selectionStore.setSelection(selection)
: this.peopleStore.groupsStore.setSelection(selection);
};
setHotkeyCaret = (hotkeyCaret: AccountsType) => {
if (hotkeyCaret || this.hotkeyCaret) {
this.hotkeyCaret = hotkeyCaret;
}
};
setHotkeyCaretStart = (hotkeyCaretStart: AccountsType) => {
this.hotkeyCaretStart = hotkeyCaretStart;
};
getItemOffset = () => {
const className = `${this.hotkeyCaret?.id}`;
let item = document.getElementsByClassName(className);
if (this.peopleStore.viewAs === "table") {
item = item && item[0]?.getElementsByClassName("table-container_cell");
}
if (item && item[0]) {
const el = item[0] as HTMLElement;
const windowItem = el.closest(".window-item") as HTMLElement;
const offset = windowItem?.offsetTop;
const offsetTop = offset ?? el.offsetTop;
return { offsetTop, item };
}
return { offsetTop: null, item: null };
};
scrollToCaret = () => {
const { offsetTop, item } = this.getItemOffset();
const scroll = isMobile()
? document.querySelector("#customScrollBar > .scroll-wrapper > .scroller")
: document.getElementsByClassName("section-scroll")[0];
const scrollRect = scroll?.getBoundingClientRect();
if (scrollRect && scroll) {
if (item && item[0]) {
const el = item[0] as HTMLElement;
const rect = el.getBoundingClientRect();
const rectHeight =
this.peopleStore.viewAs === "table" ? rect.height * 2 : rect.height;
if (
scrollRect.top + scrollRect.height - rect.height > rect.top &&
scrollRect.top < rect.top + el.offsetHeight - rectHeight
) {
// console.log("element is visible");
} else {
scroll.scrollTo(0, offsetTop - scrollRect.height / 2);
// console.log("element is not visible");
}
} else {
scroll?.scrollTo(0, this.elemOffset - scrollRect.height / 2);
}
}
};
setCaret = (caret: AccountsType, withScroll: boolean = true) => {
this.setHotkeyCaret(caret);
if (withScroll) this.scrollToCaret();
const { offsetTop } = this.getItemOffset();
if (offsetTop) this.elemOffset = offsetTop;
};
setSelectionWithCaret = (selection: AccountsType[]) => {
this.setSelection(selection);
this.setCaret(selection[0]);
this.setHotkeyCaretStart(selection[0]);
};
selectFirstFile = () => {
if (this.accountsList.length) {
// scroll to first element
const scroll = isMobile()
? document.querySelector(
"#customScrollBar > .scroll-wrapper > .scroller",
)
: document.getElementsByClassName("section-scroll")[0];
scroll?.scrollTo(0, 0);
this.setSelection([this.accountsList[0]]);
this.setCaret(this.accountsList[0]);
this.setHotkeyCaretStart(this.accountsList[0]);
}
};
activateHotkeys = (e: KeyboardEvent) => {
const infiniteLoaderComponent = document.getElementsByClassName(
"ReactVirtualized__List",
)[0] as HTMLElement;
const isAccountsPage =
window.location.pathname.includes("/accounts") ||
window.location.pathname.includes("accounts/people");
if (!isAccountsPage) return e;
if (infiniteLoaderComponent) {
infiniteLoaderComponent.tabIndex = -1;
}
const someDialogIsOpen = checkDialogsOpen();
const elementTarget = e.target as HTMLElement;
const inputTarget = e.target as HTMLInputElement;
if (
someDialogIsOpen ||
(elementTarget?.tagName === "INPUT" &&
inputTarget?.type !== "checkbox") ||
elementTarget?.tagName === "TEXTAREA"
)
return e;
const isDefaultKeys =
["PageUp", "PageDown", "Home", "End"].indexOf(e.code) > -1;
if (
["Space", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].indexOf(
e.code,
) > -1
) {
e.preventDefault();
}
const selection = this.accountsSelection.length
? this.accountsSelection
: this.accountsList;
if (!this.hotkeyCaret) {
const scroll = document.getElementsByClassName(
"section-scroll",
) as HTMLCollectionOf<HTMLElement>;
if (scroll && scroll[0]) scroll[0].focus();
}
if (!this.hotkeyCaret && selection.length) {
this.setCaret(selection[0], !(e.ctrlKey || e.metaKey || e.shiftKey));
this.setHotkeyCaretStart(selection[0]);
}
if (!this.hotkeyCaret || isDefaultKeys) return e;
};
}
export default AccountsHotkeysStore;

View File

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

View File

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

View File

@ -40,6 +40,7 @@ import TargetUserStore from "./TargetUserStore";
import EditingFormStore from "./EditingFormStore";
import FilterStore from "./FilterStore";
import SelectionStore from "./SelectionPeopleStore";
import AccountsHotkeysStore from "./AccountsHotkeysStore";
import HeaderMenuStore from "./HeaderMenuStore";
import InviteLinksStore from "./InviteLinksStore";
@ -77,10 +78,12 @@ class PeopleStore {
profileActionsStore = null;
infoPanelStore = null;
userStore = null;
accountsHotkeysStore = null;
isInit = false;
viewAs = isDesktop() ? "table" : "row";
isLoadedProfileSectionBody = false;
enabledHotkeys = true;
constructor(
authStore,
@ -101,6 +104,7 @@ class PeopleStore {
infoPanelStore,
userStore,
);
this.accountsHotkeysStore = new AccountsHotkeysStore(this);
this.groupsStore = new GroupsStore(
authStore,
this,
@ -531,6 +535,10 @@ class PeopleStore {
else if (user.isVisitor) return "user";
else return "manager";
};
setEnabledHotkeys = (enabledHotkeys) => {
this.enabledHotkeys = enabledHotkeys;
};
}
export default PeopleStore;

View File

@ -180,6 +180,7 @@ class SelectionStore {
if (exists) return;
this.setSelection([...this.selection, user]);
this.peopleStore.accountsHotkeysStore.setHotkeyCaret(null);
this.incrementUsersRights(user);
};
@ -265,6 +266,8 @@ class SelectionStore {
list.forEach((u) => this.incrementUsersRights(u));
}
this.peopleStore.accountsHotkeysStore.setHotkeyCaret(null);
return selected;
};

View File

@ -48,7 +48,7 @@ const Root = dynamic(() => import("@/components/Root"), {
});
export const metadata: Metadata = {
title: "Onlyoffice DocEditor page",
title: "ONLYOFFICE DocEditor page",
description: "",
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,77 +24,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}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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