diff --git a/.github/workflows/update-version.yml b/.github/workflows/update-version.yml new file mode 100644 index 0000000000..ea45f99adc --- /dev/null +++ b/.github/workflows/update-version.yml @@ -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 }} + diff --git a/packages/client/src/components/Layout/ScrollToTop.js b/packages/client/src/components/Layout/ScrollToTop.js index 0e4fb2a29b..7df1d47ab4 100644 --- a/packages/client/src/components/Layout/ScrollToTop.js +++ b/packages/client/src/components/Layout/ScrollToTop.js @@ -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)); diff --git a/packages/client/src/components/SimulatePassword/index.js b/packages/client/src/components/SimulatePassword/index.js index 9cf412d477..93e6072739 100644 --- a/packages/client/src/components/SimulatePassword/index.js +++ b/packages/client/src/components/SimulatePassword/index.js @@ -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; diff --git a/packages/client/src/components/panels/InvitePanel/index.js b/packages/client/src/components/panels/InvitePanel/index.js index 3f5b3754cd..0f5d0e125d 100644 --- a/packages/client/src/components/panels/InvitePanel/index.js +++ b/packages/client/src/components/panels/InvitePanel/index.js @@ -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); } diff --git a/packages/client/src/pages/FormGallery/Filter/CategoryFilter/MobileView/index.js b/packages/client/src/pages/FormGallery/Filter/CategoryFilter/MobileView/index.js index 60ec43fae8..00bd53e0b4 100644 --- a/packages/client/src/pages/FormGallery/Filter/CategoryFilter/MobileView/index.js +++ b/packages/client/src/pages/FormGallery/Filter/CategoryFilter/MobileView/index.js @@ -124,7 +124,7 @@ const CategoryFilterMobile = ({ > - {showSearchBlock && } - {!isNoItem && ( 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` diff --git a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/Rooms/index.js b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/Rooms/index.js index f075eaae8b..9629a6a462 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/Rooms/index.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/sub-components/ItemTitle/Rooms/index.js @@ -42,6 +42,7 @@ import { RoomsType, ShareAccessRights, } from "@docspace/shared/enums"; +import Search from "../../Search"; const RoomsItemHeader = ({ t, @@ -56,6 +57,7 @@ const RoomsItemHeader = ({ setBufferSelection, isArchive, hasLinks, + showSearchBlock, setCalendarDay, openHistory, setShowSearchBlock, @@ -105,6 +107,8 @@ const RoomsItemHeader = ({ return ( + {isRoomMembersPanel && showSearchBlock && } +
{ if ( !infoPanelSelection || @@ -96,6 +103,11 @@ const Members = ({ updateInfoPanelMembers(); }, [infoPanelSelection, searchValue]); + useEffect(() => { + if (searchResultIsLoading) return; + scrollContext?.parentScrollbar?.scrollToTop(); + }, [searchResultIsLoading]); + const loadNextPage = async () => { await fetchMoreMembers(t, withoutTitlesAndLinks); }; @@ -236,10 +248,12 @@ const Members = ({ return ( <> {showPublicRoomBar && ( - + + + )} {publicRoomItems} {membersList.map((user, index) => { @@ -258,7 +273,7 @@ const Members = ({ 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 ( -
- -
- ); + .members-list-item { + left: unset !important; + inset-inline-start: 0; + width: calc(100% - 20px) !important; } - return ( -
- {item} -
- ); -}, 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 ( +
+ +
+ ); + } + + return ( +
+ {item} +
+ ); + }; 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 ( - - - - - - {({ onItemsRendered, ref }) => { - const listWidth = isMobileView - ? "calc(100% + 16px)" - : "calc(100% + 20px)"; // for scroll - + + + )} + + + {({ onRowsRendered, registerChild }) => { return ( - - {Item} - + + {({ height, isScrolling, scrollTop }) => { + if (height === undefined) { + height = scrollElement.getBoundingClientRect().height; + } + + const width = scrollElement.getBoundingClientRect().width; + + return ( + + ); + }} + ); }} diff --git a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/Styled.js b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/Styled.js index a904c46e2f..a1389e0156 100644 --- a/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/Styled.js +++ b/packages/client/src/pages/Home/InfoPanel/Body/views/Members/sub-components/Styled.js @@ -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 }; diff --git a/packages/client/src/pages/Home/MediaViewer/index.js b/packages/client/src/pages/Home/MediaViewer/index.js index 514270d30f..8fc583324c 100644 --- a/packages/client/src/pages/Home/MediaViewer/index.js +++ b/packages/client/src/pages/Home/MediaViewer/index.js @@ -252,6 +252,7 @@ const FilesMediaViewer = (props) => { state: { ...location.state, fromMediaViewer: true, + disableScrollToTop: true, }, }); }, diff --git a/packages/client/src/pages/PortalSettings/categories/common/index.js b/packages/client/src/pages/PortalSettings/categories/common/index.js index e98c3ea4d1..8f2aff41d1 100644 --- a/packages/client/src/pages/PortalSettings/categories/common/index.js +++ b/packages/client/src/pages/PortalSettings/categories/common/index.js @@ -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]} /> ); }; diff --git a/packages/client/src/pages/PortalSettings/categories/data-management/index.js b/packages/client/src/pages/PortalSettings/categories/data-management/index.js index 4f0d9b14e4..312b2a2a68 100644 --- a/packages/client/src/pages/PortalSettings/categories/data-management/index.js +++ b/packages/client/src/pages/PortalSettings/categories/data-management/index.js @@ -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]} /> ); }; diff --git a/packages/client/src/pages/PortalSettings/categories/developer-tools/index.js b/packages/client/src/pages/PortalSettings/categories/developer-tools/index.js index 1f83d7ae47..25b7413384 100644 --- a/packages/client/src/pages/PortalSettings/categories/developer-tools/index.js +++ b/packages/client/src/pages/PortalSettings/categories/developer-tools/index.js @@ -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]} /> ); diff --git a/packages/client/src/pages/PortalSettings/categories/integration/index.js b/packages/client/src/pages/PortalSettings/categories/integration/index.js index f0d3af8586..fc60333cab 100644 --- a/packages/client/src/pages/PortalSettings/categories/integration/index.js +++ b/packages/client/src/pages/PortalSettings/categories/integration/index.js @@ -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]} /> ); }; diff --git a/packages/client/src/pages/PortalSettings/categories/security/index.js b/packages/client/src/pages/PortalSettings/categories/security/index.js index 8af5f06bc9..77adadef9c 100644 --- a/packages/client/src/pages/PortalSettings/categories/security/index.js +++ b/packages/client/src/pages/PortalSettings/categories/security/index.js @@ -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]} /> ); }; diff --git a/packages/client/src/pages/Profile/Section/Body/index.js b/packages/client/src/pages/Profile/Section/Body/index.js index 610c10ac5b..a0f86140db 100644 --- a/packages/client/src/pages/Profile/Section/Body/index.js +++ b/packages/client/src/pages/Profile/Section/Body/index.js @@ -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 ( - ); diff --git a/packages/client/src/store/FilesActionsStore.js b/packages/client/src/store/FilesActionsStore.js index 28a7a93d65..0025e1bb7f 100644 --- a/packages/client/src/store/FilesActionsStore.js +++ b/packages/client/src/store/FilesActionsStore.js @@ -2379,7 +2379,7 @@ class FilesActionStore { const url = getUrl(id); - window.DocSpace.navigate(url); + window.DocSpace.navigate(url, { state: { disableScrollToTop: true } }); return; } diff --git a/packages/client/src/store/MediaViewerDataStore.js b/packages/client/src/store/MediaViewerDataStore.js index 63c0cd7710..1aee25e0b5 100644 --- a/packages/client/src/store/MediaViewerDataStore.js +++ b/packages/client/src/store/MediaViewerDataStore.js @@ -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 = () => { diff --git a/packages/login/public/locales/ar-SA/Login.json b/packages/login/public/locales/ar-SA/Login.json index d51dc8075e..85b40b701e 100644 --- a/packages/login/public/locales/ar-SA/Login.json +++ b/packages/login/public/locales/ar-SA/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "طلب التسجيل", "RegistrationEmailWatermark": "بريد إلكتروني", "RememberHelper": "العمر الافتراضي للجلسة هو 20 دقيقة. حدد هذا الخيار لتعيينه على عام واحد. لتعيين القيمة الخاصة بك ، انتقل إلى الإعدادات.", - "ResendCode": "أعد إرسال الرمز" + "ResendCode": "أعد إرسال الرمز", + "UserIsAlreadyRegistered": "المستخدم <1>{{email}} مسجل بالفعل في DocSpace، أدخل كلمة المرور الخاصة بك أو ارجع للمتابعة باستخدام بريد إلكتروني آخر." } diff --git a/packages/login/public/locales/az/Login.json b/packages/login/public/locales/az/Login.json index df4f971f15..30351efe0d 100644 --- a/packages/login/public/locales/az/Login.json +++ b/packages/login/public/locales/az/Login.json @@ -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}} 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." } diff --git a/packages/login/public/locales/bg/Login.json b/packages/login/public/locales/bg/Login.json index 5d67259e1a..29cd4ebe2c 100644 --- a/packages/login/public/locales/bg/Login.json +++ b/packages/login/public/locales/bg/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "Заявка за регистрация", "RegistrationEmailWatermark": "Имейл", "RememberHelper": "Продължителността на сесията по подразбиране е 20 минути. Проверете тази опция, за да я настроите за 1 година. За да зададете собствена стойност, отидете в Настройки.", - "ResendCode": "Код за препращане" + "ResendCode": "Код за препращане", + "UserIsAlreadyRegistered": "Потребителят <1>{{email}} вече е регистриран в този DocSpace, въведете паролата си или се върнете, за да продължите с друг имейл." } diff --git a/packages/login/public/locales/cs/Login.json b/packages/login/public/locales/cs/Login.json index b814f29dad..fe4275e9fb 100644 --- a/packages/login/public/locales/cs/Login.json +++ b/packages/login/public/locales/cs/Login.json @@ -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}} je již v tomto DocSpace zaregistrován, zadejte své heslo nebo se vraťte zpět a pokračujte jiným e-mailem." } diff --git a/packages/login/public/locales/de/Login.json b/packages/login/public/locales/de/Login.json index 5dd3afe5ac..51f0096567 100644 --- a/packages/login/public/locales/de/Login.json +++ b/packages/login/public/locales/de/Login.json @@ -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}} ist bereits in diesem DocSpace registriert. Geben Sie Ihr Passwort ein oder gehen Sie zurück, um mit einer anderen E-Mail fortzufahren." } diff --git a/packages/login/public/locales/el-GR/Login.json b/packages/login/public/locales/el-GR/Login.json index 68e9b10283..dccafa6437 100644 --- a/packages/login/public/locales/el-GR/Login.json +++ b/packages/login/public/locales/el-GR/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "Αίτημα εγγραφής", "RegistrationEmailWatermark": "Email", "RememberHelper": "Η προεπιλεγμένη διάρκεια περιόδου λειτουργίας είναι 20 λεπτά. Ενεργοποιήστε αυτή την επιλογή για να την ορίσετε σε 1 έτος. Για να ορίσετε τη δική σας τιμή, μεταβείτε στις Ρυθμίσεις.", - "ResendCode": "Επαναποστολή κωδικού" + "ResendCode": "Επαναποστολή κωδικού", + "UserIsAlreadyRegistered": "Ο χρήστης <1>{{email}} είναι ήδη εγγεγραμμένος σε αυτό το DocSpace. Πληκτρολογήστε τον κωδικό πρόσβασής σας ή επιστρέψτε για να συνεχίσετε με άλλο email." } diff --git a/packages/login/public/locales/es/Login.json b/packages/login/public/locales/es/Login.json index 9356f2c34a..9143981726 100644 --- a/packages/login/public/locales/es/Login.json +++ b/packages/login/public/locales/es/Login.json @@ -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}} ya está registrado en este DocSpace, introduzca su contraseña o regrese para continuar con otro correo electrónico." } diff --git a/packages/login/public/locales/fi/Login.json b/packages/login/public/locales/fi/Login.json index 98c1ad70db..ec5edec260 100644 --- a/packages/login/public/locales/fi/Login.json +++ b/packages/login/public/locales/fi/Login.json @@ -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}} on jo rekisteröity tähän DocSpaceen, syötä salasanasi tai mene takaisin jatkaaksesi toisella sähköpostilla." } diff --git a/packages/login/public/locales/fr/Login.json b/packages/login/public/locales/fr/Login.json index 154d5c7618..3c730d19ac 100644 --- a/packages/login/public/locales/fr/Login.json +++ b/packages/login/public/locales/fr/Login.json @@ -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}} est déjà enregistré dans ce DocSpace, saisissez votre mot de passe ou revenez en arrière pour continuer avec un autre e-mail." } diff --git a/packages/login/public/locales/hy-AM/Login.json b/packages/login/public/locales/hy-AM/Login.json index 6fd5c3e231..d5c0fc274c 100644 --- a/packages/login/public/locales/hy-AM/Login.json +++ b/packages/login/public/locales/hy-AM/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "Գրանցման հայցում", "RegistrationEmailWatermark": "Էլ․փոստ", "RememberHelper": "Նախնական աշխատաշրջանի աշխատաժամը 20 րոպե է: Նշեք այս տարբերակը՝ այն 1 տարի սահմանելու համար: Ձեր սեփական արժեքը սահմանելու համար անցեք Կարգավորումներ:", - "ResendCode": "Կրկին ուղարկել կոդը" + "ResendCode": "Կրկին ուղարկել կոդը", + "UserIsAlreadyRegistered": "Օգտվող <1>{{email}}-ն արդեն գրանցված է այս DocSpace-ում, մուտքագրեք ձեր գաղտնաբառը կամ վերադարձեք՝ շարունակելու մեկ այլ էլ։" } diff --git a/packages/login/public/locales/it/Login.json b/packages/login/public/locales/it/Login.json index 99694faebe..efa4904c0c 100644 --- a/packages/login/public/locales/it/Login.json +++ b/packages/login/public/locales/it/Login.json @@ -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}} è già registrato in questo DocSpace, inserisci la tua password o torna indietro per continuare con un'altra email." } diff --git a/packages/login/public/locales/ja-JP/Login.json b/packages/login/public/locales/ja-JP/Login.json index ef5bda5a7f..31e3317100 100644 --- a/packages/login/public/locales/ja-JP/Login.json +++ b/packages/login/public/locales/ja-JP/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "登録申請", "RegistrationEmailWatermark": "メール", "RememberHelper": "デフォルトのセッションライフタイムは20分です。このオプションをチェックすると、1年間に設定されます。独自の値を設定するには、「設定」で設定します。", - "ResendCode": "コードの再送信" + "ResendCode": "コードの再送信", + "UserIsAlreadyRegistered": "ユーザー<1>{{email}}はすでにこのDocSpaceに登録されています。パスワードを入力するか、前に戻って別のメールアドレスでログインしてください。" } diff --git a/packages/login/public/locales/ko-KR/Login.json b/packages/login/public/locales/ko-KR/Login.json index 016504e135..e8222138c4 100644 --- a/packages/login/public/locales/ko-KR/Login.json +++ b/packages/login/public/locales/ko-KR/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "가입 요청", "RegistrationEmailWatermark": "이메일", "RememberHelper": "기본 세션 기간은 20분입니다. 1년으로 설정하려면 이 옵션을 확인하세요. 원하시는 값으로 설정하려면 설정으로 이동하세요.", - "ResendCode": "코드 재전송" + "ResendCode": "코드 재전송", + "UserIsAlreadyRegistered": "<1>{{email}} 사용자는 이미 이 DocSpace에 등록되어 있습니다. 비밀번호를 입력하거나 돌아가서 다른 이메일로 계속 진행하세요." } diff --git a/packages/login/public/locales/lv/Login.json b/packages/login/public/locales/lv/Login.json index d06a25c4f2..b64f206dcb 100644 --- a/packages/login/public/locales/lv/Login.json +++ b/packages/login/public/locales/lv/Login.json @@ -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}} jau ir reģistrēts šajā DocSpace, ievadiet savu paroli vai dodieties atpakaļ, lai turpinātu ar citu e-pasta adresi." } diff --git a/packages/login/public/locales/nl/Login.json b/packages/login/public/locales/nl/Login.json index ac778504b6..ee990465d1 100644 --- a/packages/login/public/locales/nl/Login.json +++ b/packages/login/public/locales/nl/Login.json @@ -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}} is al geregistreerd in deze DocSpace, voer uw wachtwoord in of ga terug om verder te gaan met een andere e-mail." } diff --git a/packages/login/public/locales/pl/Login.json b/packages/login/public/locales/pl/Login.json index 0521873e00..6940ea6ea5 100644 --- a/packages/login/public/locales/pl/Login.json +++ b/packages/login/public/locales/pl/Login.json @@ -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}} jest już zarejestrowany w tym DocSpace, wpisz swoje hasło lub wróć, aby kontynuować z innym adresem e-mail." } diff --git a/packages/login/public/locales/pt-BR/Login.json b/packages/login/public/locales/pt-BR/Login.json index 0ef6c6cc6e..e775ebde4c 100644 --- a/packages/login/public/locales/pt-BR/Login.json +++ b/packages/login/public/locales/pt-BR/Login.json @@ -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}} já está cadastrado neste DocSpace, digite sua senha ou volte para continuar com outro e-mail." } diff --git a/packages/login/public/locales/pt/Login.json b/packages/login/public/locales/pt/Login.json index f32b2c0b0d..1e417273f2 100644 --- a/packages/login/public/locales/pt/Login.json +++ b/packages/login/public/locales/pt/Login.json @@ -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}} já está cadastrado neste DocSpace, digite sua senha ou volte para continuar com outro e-mail." } diff --git a/packages/login/public/locales/ro/Login.json b/packages/login/public/locales/ro/Login.json index 16baaeb5bc..88a9a872d8 100644 --- a/packages/login/public/locales/ro/Login.json +++ b/packages/login/public/locales/ro/Login.json @@ -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}} este deja înregistrat în acest spațiu DocSpace, introduceţi parola dvs sau reveniți pentru a continua cu un alt e-mail." } diff --git a/packages/login/public/locales/ru/Login.json b/packages/login/public/locales/ru/Login.json index 900006a391..24cc48f2c9 100644 --- a/packages/login/public/locales/ru/Login.json +++ b/packages/login/public/locales/ru/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "Запрос на регистрацию", "RegistrationEmailWatermark": "Регистрационный email", "RememberHelper": "Время существования сессии по умолчанию составляет 20 минут. Отметьте эту опцию, чтобы установить значение 1 год. Чтобы задать собственное значение, перейдите в настройки.", - "ResendCode": "Отправить код повторно" + "ResendCode": "Отправить код повторно", + "UserIsAlreadyRegistered": "Пользователь <1>{{email}} уже зарегистрирован в этом DocSpace. Введите свой пароль или вернитесь назад, чтобы продолжить с другим адресом электронной почты." } diff --git a/packages/login/public/locales/sk/Login.json b/packages/login/public/locales/sk/Login.json index 4f9648d90d..d43db738aa 100644 --- a/packages/login/public/locales/sk/Login.json +++ b/packages/login/public/locales/sk/Login.json @@ -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}} je už zaregistrovaný v tomto priestore DocSpace, zadajte svoje heslo alebo sa vráťte späť a pokračujte s iným e-mailom." } diff --git a/packages/login/public/locales/sl/Login.json b/packages/login/public/locales/sl/Login.json index 813abb835f..95df7967e6 100644 --- a/packages/login/public/locales/sl/Login.json +++ b/packages/login/public/locales/sl/Login.json @@ -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}} je že registriran v tem prostoru DocSpace. Vnesite svoje geslo ali se vrnite nazaj in nadaljujte z drugim e-mail naslovom." } diff --git a/packages/login/public/locales/sr-Latn-RS/Errors.json b/packages/login/public/locales/sr-Latn-RS/Errors.json index 0967ef424b..7a2edd19b0 100644 --- a/packages/login/public/locales/sr-Latn-RS/Errors.json +++ b/packages/login/public/locales/sr-Latn-RS/Errors.json @@ -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" +} diff --git a/packages/login/public/locales/sr-Latn-RS/Login.json b/packages/login/public/locales/sr-Latn-RS/Login.json index 0967ef424b..e24b08d15d 100644 --- a/packages/login/public/locales/sr-Latn-RS/Login.json +++ b/packages/login/public/locales/sr-Latn-RS/Login.json @@ -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", + "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}} je već registrovan u ovom DocSpace-u, unesite svoju lozinku ili se vratite da biste nastavili sa drugim email-om." +} diff --git a/packages/login/public/locales/tr/Login.json b/packages/login/public/locales/tr/Login.json index 6970f6fd36..f2a07fd3e1 100644 --- a/packages/login/public/locales/tr/Login.json +++ b/packages/login/public/locales/tr/Login.json @@ -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}} bu DocSpace'e zaten kayıtlı, şifrenizi girin veya başka bir e-posta ile devam etmek için geri dönün." } diff --git a/packages/login/public/locales/uk-UA/Login.json b/packages/login/public/locales/uk-UA/Login.json index d38115de61..ec01c4de18 100644 --- a/packages/login/public/locales/uk-UA/Login.json +++ b/packages/login/public/locales/uk-UA/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "Запит на реєстрацію", "RegistrationEmailWatermark": "Електронна пошта", "RememberHelper": "Термін дії сеансу за замовчуванням складає 20 хвилин. Оберіть цей параметр, щоб задати для нього значення 1 рік. Щоб задати власне значення, перейдіть до налаштувань.", - "ResendCode": "Надіслати код повторно" + "ResendCode": "Надіслати код повторно", + "UserIsAlreadyRegistered": "Користувач <1>{{email}} вже зареєстрований у цьому просторі DocSpace. Введіть свій пароль або поверніться й продовжте з іншою адресою електронної пошти." } diff --git a/packages/login/public/locales/vi/Login.json b/packages/login/public/locales/vi/Login.json index bc611f88ff..a99f579955 100644 --- a/packages/login/public/locales/vi/Login.json +++ b/packages/login/public/locales/vi/Login.json @@ -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}} đã đượ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." } diff --git a/packages/login/public/locales/zh-CN/Login.json b/packages/login/public/locales/zh-CN/Login.json index 2fe9e964b1..23c42713b4 100644 --- a/packages/login/public/locales/zh-CN/Login.json +++ b/packages/login/public/locales/zh-CN/Login.json @@ -19,5 +19,6 @@ "RegisterTitle": "注册请求", "RegistrationEmailWatermark": "邮箱", "RememberHelper": "默认会话寿命为20分钟。勾选此选项以将其设为1年。如需自行设置其值,请前往设置。", - "ResendCode": "重新发送代码" + "ResendCode": "重新发送代码", + "UserIsAlreadyRegistered": "用户 <1>{{email}} 已注册协作空间,请输入密码,或返回使用另一电子邮件继续。" } diff --git a/packages/login/src/app/(root)/layout.tsx b/packages/login/src/app/(root)/layout.tsx index 2a339f9889..34cc4cbb0f 100644 --- a/packages/login/src/app/(root)/layout.tsx +++ b/packages/login/src/app/(root)/layout.tsx @@ -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 ( - - - - - - - - - - - - - - {children} - - - - +
+ + + +
+ + + + + {children} + + + + +
); } diff --git a/packages/login/src/app/(root)/page.tsx b/packages/login/src/app/(root)/page.tsx index 9797dde823..5fd06d7525 100644 --- a/packages/login/src/app/(root)/page.tsx +++ b/packages/login/src/app/(root)/page.tsx @@ -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 ( - -
- - + + {settings && typeof settings !== "string" && ( + <> + + + {settings.enableAdmMess && } + {settings.enabledJoin && ( + + )} + + )} + ); } diff --git a/packages/login/src/app/(root)/error/page.tsx b/packages/login/src/app/error/page.tsx similarity index 100% rename from packages/login/src/app/(root)/error/page.tsx rename to packages/login/src/app/error/page.tsx diff --git a/packages/login/src/app/global-error.tsx b/packages/login/src/app/global-error.tsx index 33ced39788..28b186d7c5 100644 --- a/packages/login/src/app/global-error.tsx +++ b/packages/login/src/app/global-error.tsx @@ -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]); diff --git a/packages/login/src/app/layout.tsx b/packages/login/src/app/layout.tsx new file mode 100644 index 0000000000..a30a1f8aa8 --- /dev/null +++ b/packages/login/src/app/layout.tsx @@ -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 ( + + + + + + + + + + + + + {children} + + + + + ); +} diff --git a/packages/login/src/app/(root)/not-found.tsx b/packages/login/src/app/not-found.tsx similarity index 99% rename from packages/login/src/app/(root)/not-found.tsx rename to packages/login/src/app/not-found.tsx index 1672e8a61a..1383d3000d 100644 --- a/packages/login/src/app/(root)/not-found.tsx +++ b/packages/login/src/app/not-found.tsx @@ -29,4 +29,3 @@ import NotFoundError from "@/components/NotFoundError"; export default function NotFound() { return ; } - diff --git a/packages/login/src/components/GreetingContainer.tsx b/packages/login/src/components/GreetingContainer.tsx index 344eda2689..c23ef010ee 100644 --- a/packages/login/src/components/GreetingContainer.tsx +++ b/packages/login/src/components/GreetingContainer.tsx @@ -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 ( <> diff --git a/packages/login/src/app/(root)/[...not-found]/page.tsx b/packages/login/src/components/Login/Login.context.tsx similarity index 66% rename from packages/login/src/app/(root)/[...not-found]/page.tsx rename to packages/login/src/components/Login/Login.context.tsx index cc83b29b02..7e478ab0d0 100644 --- a/packages/login/src/app/(root)/[...not-found]/page.tsx +++ b/packages/login/src/components/Login/Login.context.tsx @@ -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 ( + + + {children} + + + ); +}; diff --git a/packages/login/src/components/Login/index.tsx b/packages/login/src/components/Login/index.tsx index 4be3cc7dfe..0207f94fd6 100644 --- a/packages/login/src/components/Login/index.tsx +++ b/packages/login/src/components/Login/index.tsx @@ -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) => { - 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 ( - <> - - - - - - - {(oauthDataExists() || ssoExists()) && ( - <> -
- - {t("Common:orContinueWith")} - -
- - - )} - {settings?.enableAdmMess && ( - - {t("RecoverAccess")} - - )} -
-
-
- {isRegisterContainerVisible && ( - - )} -
- {recoverDialogVisible && ( - - )} - - ); +const Login = ({ children }: { children: React.ReactNode }) => { + return {children}; }; export default Login; diff --git a/packages/login/src/components/LoginForm/index.tsx b/packages/login/src/components/LoginForm/index.tsx index 561a7382cd..99ef3bc0fa 100644 --- a/packages/login/src/components/LoginForm/index.tsx +++ b/packages/login/src/components/LoginForm/index.tsx @@ -24,77 +24,91 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode -import React, { useState, useRef, useEffect, useCallback } from "react"; +"use client"; + +import React, { + useState, + useRef, + useEffect, + useCallback, + useContext, + useLayoutEffect, +} from "react"; import { useTranslation } from "react-i18next"; import ReCAPTCHA from "react-google-recaptcha"; -import { isMobileOnly } from "react-device-detect"; import { useTheme } from "styled-components"; +import { useSearchParams } from "next/navigation"; -import { FieldContainer } from "@docspace/shared/components/field-container"; -import { PasswordInput } from "@docspace/shared/components/password-input"; -import { Checkbox } from "@docspace/shared/components/checkbox"; -import { HelpButton } from "@docspace/shared/components/help-button"; import { Text } from "@docspace/shared/components/text"; -import { Link, LinkType } from "@docspace/shared/components/link"; import { Button, ButtonSize } from "@docspace/shared/components/button"; import { createPasswordHash } from "@docspace/shared/utils/common"; -import { checkIsSSR } from "@docspace/shared/utils"; import { checkPwd } from "@docspace/shared/utils/desktop"; import { login } from "@docspace/shared/utils/loginUtils"; import { toastr } from "@docspace/shared/components/toast"; import { thirdPartyLogin } from "@docspace/shared/api/user"; import { setWithCredentialsStatus } from "@docspace/shared/api/client"; -import { InputSize, InputType } from "@docspace/shared/components/text-input"; import { TValidate } from "@docspace/shared/components/email-input/EmailInput.types"; import { LoginFormProps } from "@/types"; +import { getEmailFromInvitation } from "@/utils"; import EmailContainer from "./sub-components/EmailContainer"; -import ForgotPasswordModalDialog from "./sub-components/ForgotPasswordModalDialog"; +import PasswordContainer from "./sub-components/PasswordContainer"; +import ForgotContainer from "./sub-components/ForgotContainer"; import { StyledCaptcha } from "./LoginForm.styled"; - -const settings = { - minLength: 6, - upperCase: false, - digits: false, - specSymbols: false, -}; +import { LoginDispatchContext, LoginValueContext } from "../Login"; const LoginForm = ({ - isLoading, hashSettings, - isDesktop, - match, - setIsLoading, cookieSettingsEnabled, - recaptchaPublicKey, - emailFromInvitation, + reCaptchaPublicKey, }: LoginFormProps) => { + const { isLoading, isModalOpen } = useContext(LoginValueContext); + const { setIsLoading } = useContext(LoginDispatchContext); + + const searchParams = useSearchParams(); + + const theme = useTheme(); + + const { t, ready } = useTranslation(["Login", "Common"]); + + const message = searchParams.get("message"); + const confirmedEmail = searchParams.get("confirmedEmail"); + const authError = searchParams.get("authError"); + const loginData = searchParams.get("loginData"); + + const isDesktop = + typeof window !== "undefined" && window["AscDesktopEditor"] !== undefined; + + const [emailFromInvitation, setEmailFromInvitation] = useState( + getEmailFromInvitation(loginData), + ); + const [identifier, setIdentifier] = useState( + getEmailFromInvitation(loginData), + ); + const [isEmailErrorShow, setIsEmailErrorShow] = useState(false); const [errorText, setErrorText] = useState(""); - const [identifier, setIdentifier] = useState(emailFromInvitation ?? ""); + const [passwordValid, setPasswordValid] = useState(true); const [identifierValid, setIdentifierValid] = useState(true); const [password, setPassword] = useState(""); - const [isDisabled, setIsDisabled] = useState(false); + const [isChecked, setIsChecked] = useState(false); - const [isDialogVisible, setIsDialogVisible] = useState(false); + const [isCaptcha, setIsCaptcha] = useState(false); const [isCaptchaSuccessful, setIsCaptchaSuccess] = useState(false); const [isCaptchaError, setIsCaptchaError] = useState(false); - const inputRef = useRef(null); const captchaRef = useRef(null); - const { t, ready } = useTranslation(["Login", "Common"]); - const theme = useTheme(); + useLayoutEffect(() => { + const email = getEmailFromInvitation(loginData); - const { message, confirmedEmail, authError } = match || { - message: "", - confirmedEmail: "", - authError: "", - }; + setIdentifier(email); + setEmailFromInvitation(email); + }, [loginData]); const authCallback = useCallback( async (profile: string) => { @@ -157,8 +171,6 @@ const LoginForm = ({ if (confirmedEmail && ready) toastr.success(text); if (authError && ready) toastr.error(t("Common:ProviderLoginError")); - focusInput(); - window.authCallback = authCallback; }, [message, confirmedEmail, t, ready, authError, authCallback]); @@ -173,11 +185,11 @@ const LoginForm = ({ if (!passwordValid) setPasswordValid(true); }; - const onSubmit = () => { + const onSubmit = useCallback(() => { //errorText && setErrorText(""); let captchaToken: string | undefined | null = ""; - if (recaptchaPublicKey && isCaptcha) { + if (reCaptchaPublicKey && isCaptcha) { if (!isCaptchaSuccessful) { setIsCaptchaError(true); return; @@ -243,7 +255,7 @@ const LoginForm = ({ errorMessage = error; } - if (recaptchaPublicKey && error?.response?.status === 403) { + if (reCaptchaPublicKey && error?.response?.status === 403) { setIsCaptcha(true); } @@ -255,9 +267,19 @@ const LoginForm = ({ setErrorText(errorMessage); setPasswordValid(!errorMessage); setIsLoading(false); - focusInput(); }); - }; + }, [ + hashSettings, + identifier, + identifierValid, + isCaptcha, + isCaptchaSuccessful, + isChecked, + isDesktop, + password, + reCaptchaPublicKey, + setIsLoading, + ]); const onBlurEmail = () => { !identifierValid && setIsEmailErrorShow(true); @@ -270,38 +292,13 @@ const LoginForm = ({ return undefined; }; - const focusInput = () => { - if (inputRef && inputRef.current) { - inputRef.current.focus(); - } - }; - const onChangePassword = (e: React.ChangeEvent) => { setPassword(e.target.value); onClearErrors(); }; - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter") { - onClearErrors(); - !isDisabled && onSubmit(); - e.preventDefault(); - } - }; - const onChangeCheckbox = () => setIsChecked(!isChecked); - const onClick = () => { - setIsDialogVisible(true); - setIsDisabled(true); - }; - - const onDialogClose = () => { - setIsDialogVisible(false); - setIsDisabled(false); - setIsLoading(false); - }; - const onSuccessfullyComplete = () => { setIsCaptchaSuccess(true); }; @@ -315,6 +312,22 @@ const LoginForm = ({ } }; + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + if (isModalOpen) return; + + onSubmit(); + } + }; + + window.addEventListener("keydown", onKeyDown); + + return () => { + window.removeEventListener("keydown", onKeyDown); + }; + }, [isModalOpen, onSubmit]); + const passwordErrorMessage = errorMessage(); return ( @@ -330,83 +343,27 @@ const LoginForm = ({ onValidateEmail={onValidateEmail} /> - - - + -
-
-
- {!cookieSettingsEnabled && ( - {t("RememberHelper")} - } - tooltipMaxWidth={isMobileOnly ? "240px" : "340px"} - /> - } - /> - )} -
+ - - {t("ForgotPassword")} - -
-
- - {isDialogVisible && ( - - )} - {recaptchaPublicKey && isCaptcha && ( + {reCaptchaPublicKey && isCaptcha && (
) => void; onBlurEmail: () => void; onValidateEmail: (res: TValidate) => undefined; diff --git a/packages/login/src/components/LoginForm/sub-components/ForgotContainer.tsx b/packages/login/src/components/LoginForm/sub-components/ForgotContainer.tsx new file mode 100644 index 0000000000..d7ff81ad0f --- /dev/null +++ b/packages/login/src/components/LoginForm/sub-components/ForgotContainer.tsx @@ -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 ( +
+
+
+ {!cookieSettingsEnabled && ( + {t("RememberHelper")} + } + tooltipMaxWidth={isMobileOnly ? "240px" : "340px"} + /> + } + /> + )} +
+ + + {t("ForgotPassword")} + +
+ + {isDialogVisible && ( + + )} +
+ ); +}; + +export default ForgotContainer; diff --git a/packages/login/src/components/LoginForm/sub-components/ForgotPasswordModalDialog.tsx b/packages/login/src/components/LoginForm/sub-components/ForgotPasswordModalDialog.tsx index 9b600da6cf..34a268a3d9 100644 --- a/packages/login/src/components/LoginForm/sub-components/ForgotPasswordModalDialog.tsx +++ b/packages/login/src/components/LoginForm/sub-components/ForgotPasswordModalDialog.tsx @@ -84,7 +84,6 @@ const ForgotPasswordModalDialog = ({ const onKeyDown = React.useCallback( (e: KeyboardEvent) => { - //console.log("onKeyDown", e.key); if (e.key === "Enter") { onSendPasswordInstructions(); e.preventDefault(); diff --git a/packages/login/src/components/LoginForm/sub-components/PasswordContainer.tsx b/packages/login/src/components/LoginForm/sub-components/PasswordContainer.tsx new file mode 100644 index 0000000000..e0143b5124 --- /dev/null +++ b/packages/login/src/components/LoginForm/sub-components/PasswordContainer.tsx @@ -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) => 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 ( + + + + ); +}; + +export default PasswordContainer; diff --git a/packages/login/src/hooks/useIsomorphicLayoutEffect.ts b/packages/login/src/components/RecoverAccess.tsx similarity index 56% rename from packages/login/src/hooks/useIsomorphicLayoutEffect.ts rename to packages/login/src/components/RecoverAccess.tsx index 8db077ebea..a217af11a7 100644 --- a/packages/login/src/hooks/useIsomorphicLayoutEffect.ts +++ b/packages/login/src/components/RecoverAccess.tsx @@ -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 ( + <> + + {t("RecoverAccess")} + + {recoverDialogVisible && ( + + )} + + ); +}; + +export default RecoverAccess; diff --git a/packages/login/src/components/Register/index.tsx b/packages/login/src/components/Register/index.tsx index 7252e29406..2241262622 100644 --- a/packages/login/src/components/Register/index.tsx +++ b/packages/login/src/components/Register/index.tsx @@ -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) => { if (e) { @@ -105,7 +113,7 @@ const Register = (props: RegisterProps) => { onRegisterModalClose(); } } - }, [email, emailErr]); + }, [email, emailErr, onRegisterModalClose]); const onKeyDown = React.useCallback( (e: KeyboardEvent) => { diff --git a/packages/login/src/components/SimpleNav.tsx b/packages/login/src/components/SimpleNav.tsx index 40b7136946..f343b6bdef 100644 --- a/packages/login/src/components/SimpleNav.tsx +++ b/packages/login/src/components/SimpleNav.tsx @@ -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 ( - + logo-url ); diff --git a/packages/login/src/components/ThirdParty.tsx b/packages/login/src/components/ThirdParty.tsx new file mode 100644 index 0000000000..c0381795c5 --- /dev/null +++ b/packages/login/src/components/ThirdParty.tsx @@ -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>(null); + const [thirdPartyProvider, setThirdPartyProvider] = + React.useState>(null); + const [ssoSettings, setSsoSettings] = + React.useState>(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) => { + 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 && ( + +
+ {t("Common:orContinueWith")} +
+ +
+ ) + ); +}; + +export default ThirdParty; diff --git a/packages/login/src/hooks/useI18N.ts b/packages/login/src/hooks/useI18N.ts index 55c48657bd..c9136c9284 100644 --- a/packages/login/src/hooks/useI18N.ts +++ b/packages/login/src/hooks/useI18N.ts @@ -39,38 +39,12 @@ interface UseI18NProps { } const useI18N = ({ settings }: UseI18NProps) => { - const [i18n, setI18N] = React.useState( - 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; diff --git a/packages/login/src/hooks/useRecoverDialog.ts b/packages/login/src/hooks/useRecoverDialog.ts index d687592f28..453200d5df 100644 --- a/packages/login/src/hooks/useRecoverDialog.ts +++ b/packages/login/src/hooks/useRecoverDialog.ts @@ -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 { diff --git a/packages/login/src/hooks/useTheme.ts b/packages/login/src/hooks/useTheme.ts index a41fb9d1c2..e92f9f21ad 100644 --- a/packages/login/src/hooks/useTheme.ts +++ b/packages/login/src/hooks/useTheme.ts @@ -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({} as TColorScheme); + React.useState(() => { + if (!colorTheme) return {} as TColorScheme; - const [theme, setTheme] = React.useState({ - ...Base, - currentColorScheme: currentColorTheme, + return ( + colorTheme.themes.find((theme) => theme.id === colorTheme.selected) ?? + ({} as TColorScheme) + ); + }); + + const [theme, setTheme] = React.useState(() => { + 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)"); diff --git a/packages/login/src/middleware.ts b/packages/login/src/middleware.ts index 357020289e..6ee0fb52d3 100644 --- a/packages/login/src/middleware.ts +++ b/packages/login/src/middleware.ts @@ -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"], }; diff --git a/packages/login/src/providers/index.tsx b/packages/login/src/providers/index.tsx index 65c0738fc3..a2a98f71ab 100644 --- a/packages/login/src/providers/index.tsx +++ b/packages/login/src/providers/index.tsx @@ -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 ( - + void; hashSettings?: TPasswordHash; - isDesktop: boolean; - match: { [key: string]: string }; - openRecoverDialog: () => void; - enableAdmMess: boolean; - recaptchaPublicKey?: string; - emailFromInvitation?: string; + reCaptchaPublicKey?: string; cookieSettingsEnabled: boolean; }; diff --git a/packages/login/src/utils/actions.ts b/packages/login/src/utils/actions.ts index 4edc5c616b..83e5ea0b73 100644 --- a/packages/login/src/utils/actions.ts +++ b/packages/login/src/utils/actions.ts @@ -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); -}; diff --git a/packages/login/src/utils/index.ts b/packages/login/src/utils/index.ts index aecaf81c23..7b91cfa1dd 100644 --- a/packages/login/src/utils/index.ts +++ b/packages/login/src/utils/index.ts @@ -25,7 +25,7 @@ // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode import { thirdPartyLogin } from "@docspace/shared/utils/loginUtils"; -import { TTranslation } from "@docspace/shared/types"; +import { Nullable, TTranslation } from "@docspace/shared/types"; import { MessageKey } from "./enums"; @@ -103,3 +103,38 @@ export const getMessageKeyTranslate = (t: TTranslation, message: string) => { return t("Common:Error"); } }; + +export const getInvitationLinkData = (encodeString: string) => { + const fromBinaryStr = (encodeString: string) => { + const decodeStr = atob(encodeString); + + const decoder = new TextDecoder(); + const charCodeArray = Uint8Array.from( + { length: decodeStr.length }, + (element, index) => decodeStr.charCodeAt(index), + ); + + return decoder.decode(charCodeArray); + }; + + const decodeString = fromBinaryStr(encodeString); + const queryParams = JSON.parse(decodeString) as { + email: string; + roomName: string; + firstName: string; + lastName: string; + type: string; + }; + + return queryParams; +}; + +export const getEmailFromInvitation = (encodeString: Nullable) => { + if (!encodeString) return ""; + + const queryParams = getInvitationLinkData(encodeString); + + if (!queryParams || !queryParams.email) return ""; + + return queryParams.email; +}; diff --git a/packages/login/src/utils/registry.tsx b/packages/login/src/utils/registry.tsx index b873383028..fe2635b849 100644 --- a/packages/login/src/utils/registry.tsx +++ b/packages/login/src/utils/registry.tsx @@ -53,4 +53,3 @@ export default function StyledComponentsRegistry({ ); } - diff --git a/packages/shared/components/article/sub-components/Body.tsx b/packages/shared/components/article/sub-components/Body.tsx index 3ccf4e2fb8..04792cb8c8 100644 --- a/packages/shared/components/article/sub-components/Body.tsx +++ b/packages/shared/components/article/sub-components/Body.tsx @@ -31,7 +31,7 @@ const ArticleBody = ({ children }: { children: React.ReactNode }) => { return ( {children} diff --git a/packages/shared/components/color-theme/ColorTheme.tsx b/packages/shared/components/color-theme/ColorTheme.tsx index 34ab056125..3d7068bee6 100644 --- a/packages/shared/components/color-theme/ColorTheme.tsx +++ b/packages/shared/components/color-theme/ColorTheme.tsx @@ -24,6 +24,8 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +"use client"; + import React, { PropsWithChildren, forwardRef, useContext } from "react"; import { ThemeContext } from "styled-components"; diff --git a/packages/shared/components/color-theme/sub-components/LoginContainer.ts b/packages/shared/components/color-theme/sub-components/LoginContainer.ts index 960946ab5a..849f5c6321 100644 --- a/packages/shared/components/color-theme/sub-components/LoginContainer.ts +++ b/packages/shared/components/color-theme/sub-components/LoginContainer.ts @@ -24,6 +24,8 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +"use client"; + import styled from "styled-components"; import { tablet, mobile } from "../../../utils"; import { Base } from "../../../themes"; diff --git a/packages/shared/components/form-wrapper/index.tsx b/packages/shared/components/form-wrapper/index.tsx index 512d5aea9c..3012055b6d 100644 --- a/packages/shared/components/form-wrapper/index.tsx +++ b/packages/shared/components/form-wrapper/index.tsx @@ -24,6 +24,8 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +"use client"; + import React from "react"; import styled from "styled-components"; import { tablet, mobile } from "../../utils"; diff --git a/packages/shared/components/main-button-mobile/MainButtonMobile.tsx b/packages/shared/components/main-button-mobile/MainButtonMobile.tsx index 06d7b23a78..b319057c29 100644 --- a/packages/shared/components/main-button-mobile/MainButtonMobile.tsx +++ b/packages/shared/components/main-button-mobile/MainButtonMobile.tsx @@ -363,7 +363,7 @@ const MainButtonMobile = (props: MainButtonMobileProps) => { {isMobile ? ( {children} diff --git a/packages/shared/components/media-viewer/MediaViewer.tsx b/packages/shared/components/media-viewer/MediaViewer.tsx index e6d2b0ea77..ed3ad9fb16 100644 --- a/packages/shared/components/media-viewer/MediaViewer.tsx +++ b/packages/shared/components/media-viewer/MediaViewer.tsx @@ -303,6 +303,7 @@ const MediaViewer = (props: MediaViewerProps): JSX.Element | undefined => { break; case KeyboardEventKeys.Escape: + event.stopPropagation(); if (!deleteDialogVisible) onClose?.(); break; diff --git a/packages/shared/components/scrollbar/Scrollbar.styled.ts b/packages/shared/components/scrollbar/Scrollbar.styled.ts index 8a1f7692d3..d746bd7aa2 100644 --- a/packages/shared/components/scrollbar/Scrollbar.styled.ts +++ b/packages/shared/components/scrollbar/Scrollbar.styled.ts @@ -55,6 +55,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>` padding: 4px; border-radius: 8px !important; background: transparent !important; + z-index: 201; @media ${desktop} { &:hover { @@ -111,8 +112,8 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>` touch-action: none; background-color: ${(props) => props.color ? props.color : props.theme.scrollbar.bgColor} !important; - z-index: 201; position: relative; + cursor: default !important; :hover { background-color: ${(props) => @@ -127,7 +128,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>` } } - .thumb-vertical { + & > .track > .thumb-vertical { width: ${({ $fixedSize }) => ($fixedSize ? "8px" : "4px")} !important; transition: width linear 0.1s; @@ -142,7 +143,7 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>` } } - .thumb-horizontal { + & > .track > .thumb-horizontal { height: ${({ $fixedSize }) => ($fixedSize ? "8px" : "4px")} !important; transition: height linear 0.1s; @@ -156,6 +157,51 @@ const StyledScrollbar = styled(Scrollbar)<{ $fixedSize?: boolean }>` height: 4px !important; } } + + // fix when iframe breaks dragging scroll + &:has(> .track > .dragging) { + iframe { + pointer-events: none; + } + } + + // ------- Auto hide styles ------- + + &.auto-hide { + // tracks hidden by default + .track { + opacity: 0; + transition: opacity 0.35s; + } + + // tracks always shown if hovered or thumb dragged + .track:is(:hover, :has(> .dragging)) { + opacity: 1; + } + } + + // tracks shown if scroll element was not auto hidden, hovered + // and there is no another nesting hovered scroll element, dragging thumb or backdrop + &.auto-hide.scroll-visible:hover:not( + :has( + &:hover.trackYVisible, + &:hover.trackXVisible, + .thumb.dragging, + .backdrop-active + ) + ) { + > .track { + opacity: 1; + } + } + // no hover logic for touch devices + @media (hover: none) { + &.auto-hide.scroll-visible:not(:has(.backdrop-active)) { + .track { + opacity: 1; + } + } + } `; StyledScrollbar.defaultProps = { diff --git a/packages/shared/components/scrollbar/Scrollbar.tsx b/packages/shared/components/scrollbar/Scrollbar.tsx index e215b0197c..35fddc73b5 100644 --- a/packages/shared/components/scrollbar/Scrollbar.tsx +++ b/packages/shared/components/scrollbar/Scrollbar.tsx @@ -24,10 +24,13 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode -import React, { useEffect, useRef, useState } from "react"; -import { useTheme } from "styled-components"; +"use client"; -import { classNames } from "../../utils"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { useTheme } from "styled-components"; +import throttle from "lodash/throttle"; + +import { classNames, isTouchDevice } from "../../utils"; import StyledScrollbar from "./Scrollbar.styled"; import { ScrollbarProps } from "./Scrollbar.types"; @@ -36,68 +39,22 @@ import { Scrollbar } from "./custom-scrollbar"; const ScrollbarComponent = React.forwardRef( (props, ref) => { const { - id, onScroll, - autoHide = false, - hideTrackTimer = 500, - scrollclass, + autoHide = true, + scrollClass, fixedSize = false, + className, ...rest } = props; const defaultTheme = useTheme(); const interfaceDirection = defaultTheme?.interfaceDirection; - const [isScrolling, setIsScrolling] = useState(false); - const [isMouseOver, setIsMouseOver] = useState(false); - const timerId = useRef>(); + const [scrollVisible, setScrollVisible] = useState(false); + const timerId = useRef>(null); const isRtl = interfaceDirection === "rtl"; - const showTrack = () => { - if (timerId.current) clearTimeout(timerId.current); - - setIsScrolling(true); - }; - - const hideTrack = () => { - timerId.current = setTimeout(() => { - setIsScrolling(false); - }, hideTrackTimer); - }; - - const onScrollStart = () => showTrack(); - - const onScrollStop = () => { - if (isMouseOver) return; - hideTrack(); - }; - - const onMouseEnter = () => { - showTrack(); - - setIsMouseOver(true); - }; - - const onMouseLeave = () => { - hideTrack(); - - setIsMouseOver(false); - }; - - const scrollAutoHideHandlers = autoHide - ? { onScrollStart, onScrollStop } - : {}; - const tracksAutoHideHandlers = autoHide - ? { onMouseEnter, onMouseLeave } - : {}; - const tracksAutoHideStyles = autoHide - ? { - opacity: !isScrolling ? 0 : 1, - transition: "opacity 0.4s ease-in-out", - } - : {}; - // onScroll handler placed here on Scroller element to get native event instead of parameters that library put const renderScroller = React.useCallback( (libProps: { elementRef?: React.LegacyRef }) => { @@ -107,13 +64,31 @@ const ScrollbarComponent = React.forwardRef(
); }, - [onScroll, scrollclass], + [onScroll, scrollClass], + ); + + const showTracks = useMemo( + () => + throttle( + () => { + setScrollVisible(true); + + if (timerId.current) { + clearTimeout(timerId.current); + } + + timerId.current = setTimeout(() => setScrollVisible(false), 3000); + }, + 500, + { trailing: false }, + ), + [], ); useEffect(() => { @@ -122,32 +97,36 @@ const ScrollbarComponent = React.forwardRef( }; }, []); + const autoHideContainerProps = autoHide + ? { + onScroll: showTracks, + className: classNames(className, { + "auto-hide": autoHide, + "scroll-visible": autoHide && scrollVisible, + }), + } + : {}; + + const autoHideContentProps = + autoHide && !isTouchDevice ? { onMouseMove: showTracks } : {}; + return ( ); }, diff --git a/packages/shared/components/scrollbar/Scrollbar.types.ts b/packages/shared/components/scrollbar/Scrollbar.types.ts index 3f321b2f5d..76c073b35c 100644 --- a/packages/shared/components/scrollbar/Scrollbar.types.ts +++ b/packages/shared/components/scrollbar/Scrollbar.types.ts @@ -29,27 +29,24 @@ import { ScrollbarType } from "./Scrollbar.enums"; export interface ScrollbarProps { /** Accepts class */ className?: string; + /** This class will be placed on scroller element */ + scrollClass?: string; /** Accepts id */ id?: string; /** Accepts css style */ style?: React.CSSProperties; /** Enable tracks auto hiding. */ autoHide?: boolean; - /** Track auto hiding delay in ms. */ - hideTrackTimer?: number; /** Fix scrollbar size. */ fixedSize?: boolean; /** Disable vertical scrolling. */ noScrollY?: boolean; /** Disable horizontal scrolling. */ noScrollX?: boolean; - /** Calculating height of content depending on number of lines */ - isFullHeight?: boolean; - /** Calculated height of content depending on number of lines in pixels */ - fullHeight?: number; + /** Wrap children in context that contains scrollbar instance */ + createContext?: boolean; onScroll?: React.UIEventHandler; - scrollclass?: string; children?: React.ReactNode; } diff --git a/packages/shared/components/scrollbar/custom-scrollbar/Scrollbar.tsx b/packages/shared/components/scrollbar/custom-scrollbar/Scrollbar.tsx index 66153e0195..ba942a2cfd 100644 --- a/packages/shared/components/scrollbar/custom-scrollbar/Scrollbar.tsx +++ b/packages/shared/components/scrollbar/custom-scrollbar/Scrollbar.tsx @@ -27,6 +27,9 @@ /* eslint-disable no-bitwise */ /* eslint-disable react/destructuring-assignment */ /* eslint-disable react/sort-comp */ + +"use client"; + import { cnb } from "cnbuilder"; import * as React from "react"; import { DraggableData } from "react-draggable"; diff --git a/packages/shared/components/scrollbar/custom-scrollbar/ScrollbarThumb.tsx b/packages/shared/components/scrollbar/custom-scrollbar/ScrollbarThumb.tsx index e36543ba1d..58f062222d 100644 --- a/packages/shared/components/scrollbar/custom-scrollbar/ScrollbarThumb.tsx +++ b/packages/shared/components/scrollbar/custom-scrollbar/ScrollbarThumb.tsx @@ -182,7 +182,9 @@ class ScrollbarThumb extends React.Component { return; } - ev.preventDefault(); + if (ev.cancelable) { + ev.preventDefault(); + } ev.stopPropagation(); if (!isUndef(ev.offsetX)) { diff --git a/packages/shared/components/scrollbar/index.tsx b/packages/shared/components/scrollbar/index.tsx index afe7217057..04c4eb9250 100644 --- a/packages/shared/components/scrollbar/index.tsx +++ b/packages/shared/components/scrollbar/index.tsx @@ -24,10 +24,18 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +"use client"; + import { ScrollbarType } from "./Scrollbar.enums"; import { ScrollbarComponent as Scrollbar } from "./Scrollbar"; +import { ScrollbarContext } from "./custom-scrollbar"; +import { CustomScrollbarsVirtualList } from "./sub-components"; +import type { ScrollbarProps } from "./Scrollbar.types"; -export { ScrollbarType }; -export { Scrollbar }; -export { CustomScrollbarsVirtualList } from "./sub-components/index"; -export type { ScrollbarProps } from "./Scrollbar.types"; +export { + Scrollbar, + ScrollbarProps, + ScrollbarType, + CustomScrollbarsVirtualList, + ScrollbarContext, +}; diff --git a/packages/shared/components/section/Section.constants.ts b/packages/shared/components/section/Section.constants.ts index f5f7785e08..ac7005bc59 100644 --- a/packages/shared/components/section/Section.constants.ts +++ b/packages/shared/components/section/Section.constants.ts @@ -24,6 +24,8 @@ // content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode +import { DeviceType } from "../../enums"; + export const SECTION_HEADER_NAME = "SectionHeader"; export const SECTION_FILTER_NAME = "SectionFilter"; export const SECTION_BODY_NAME = "SectionBody"; @@ -33,3 +35,9 @@ export const SECTION_INFO_PANEL_BODY_NAME = "InfoPanelBody"; export const SECTION_INFO_PANEL_HEADER_NAME = "InfoPanelHeader"; export const SECTION_WARNING_NAME = "SectionWarning"; export const SECTION_SUBMENU_NAME = "SectionSubmenu"; + +export const SECTION_HEADER_HEIGHT: Readonly> = { + [DeviceType.desktop]: "69px", + [DeviceType.tablet]: "61px", + [DeviceType.mobile]: "53px", +}; diff --git a/packages/shared/components/section/Section.styled.ts b/packages/shared/components/section/Section.styled.ts index 4113460c5e..d5f9e6033c 100644 --- a/packages/shared/components/section/Section.styled.ts +++ b/packages/shared/components/section/Section.styled.ts @@ -43,6 +43,7 @@ import { TViewAs } from "../../types"; import { Scrollbar } from "../scrollbar"; import DragAndDrop from "../drag-and-drop/DragAndDrop"; import { SectionContainerProps } from "./Section.types"; +import { SECTION_HEADER_HEIGHT } from "./Section.constants"; const StyledScrollbar = styled(Scrollbar)<{ $isScrollLocked?: boolean }>` ${({ $isScrollLocked }) => @@ -507,14 +508,7 @@ const sizeBetweenIcons = "8px"; const StyledSectionContainer = styled.section` position: relative; - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - padding: 0 20px 0 0; - ` - : css` - padding: 0 0 0 20px; - `} + ${(props) => !props.withBodyScroll && "padding-inline-start: 20px;"} flex-grow: 1; display: flex; flex-direction: column; @@ -526,40 +520,51 @@ const StyledSectionContainer = styled.section` @media ${tablet} { width: 100%; max-width: 100vw !important; - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - padding: 0 16px 0 0; - ` - : css` - padding: 0 0 0 16px; - `} + + ${(props) => !props.withBodyScroll && "padding-inline-start: 16px;"} ${tabletProps}; } @media ${mobile} { width: 100vw !important; max-width: 100vw !important; + padding-inline-start: 16px; + } + + .section-scroll > .scroll-body { + display: flex; + flex-direction: column; + padding-inline-start: 20px !important; + + @media ${tablet} { + padding-inline-start: 16px !important; + } + } + + .section-sticky-container { + position: sticky; + top: 0; + background: ${(props) => props.theme.section.header.backgroundColor}; + z-index: 201; + padding-inline: 20px; + margin-inline: -20px -17px; + + @media ${tablet} { + padding-inline: 16px; + margin-inline: -16px; + } } .progress-bar_container { - position: absolute; + position: fixed; bottom: 0; display: grid; grid-gap: 24px; margin-bottom: 24px; - - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - margin-left: 24px; - left: 0; - ` - : css` - margin-right: 24px; - right: 0; - `} + margin-inline-end: 24px; + inset-inline-end: ${(props) => + props.isInfoPanelVisible ? INFO_PANEL_WIDTH : 0}px; .layout-progress-bar_wrapper { position: static; @@ -618,14 +623,6 @@ const StyledSectionContainer = styled.section` StyledSectionContainer.defaultProps = { theme: Base }; const StyledSectionFilter = styled.div` - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - margin-left: 20px; - ` - : css` - margin-right: 20px; - `} @media ${tablet} { ${(props) => props.theme.interfaceDirection === "rtl" @@ -652,18 +649,18 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>` position: relative; display: flex; - height: 69px; - min-height: 69px; + height: ${SECTION_HEADER_HEIGHT.desktop}; + min-height: ${SECTION_HEADER_HEIGHT.desktop}; @media ${tablet} { - height: 61px; - min-height: 61px; + height: ${SECTION_HEADER_HEIGHT.tablet}; + min-height: ${SECTION_HEADER_HEIGHT.tablet}; ${({ isFormGallery }) => isFormGallery && css` - height: 69px; - min-height: 69px; + height: ${SECTION_HEADER_HEIGHT.desktop}; + min-height: ${SECTION_HEADER_HEIGHT.desktop}; `} .header-container { @@ -673,19 +670,10 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>` } @media ${mobile} { - height: 53px; - min-height: 53px; + height: ${SECTION_HEADER_HEIGHT.mobile}; + min-height: ${SECTION_HEADER_HEIGHT.mobile}; } - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - padding-left: 20px; - ` - : css` - padding-right: 20px; - `} - box-sizing: border-box; ${NoUserSelect} @@ -700,28 +688,8 @@ const StyledSectionHeader = styled.div<{ isFormGallery?: boolean }>` display: flex; } - @media ${tablet} { - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - padding-left: 16px; - margin-left: 0px; - ` - : css` - padding-right: 16px; - margin-right: 0px; - `} - } - @media ${mobile} { - ${(props) => - props.theme.interfaceDirection === "rtl" - ? css` - margin-left: 0px; - ` - : css` - margin-right: 0px; - `} + margin-inline-end: 0; } `; @@ -754,13 +722,13 @@ StyledSectionPaging.defaultProps = { theme: Base }; const StyledSectionSubmenu = styled.div` background: ${(props) => props.theme.section.header.backgroundColor}; - width: calc(100% - 20px); + width: 100%; z-index: 1; @media ${tablet} { width: calc(100% + 32px); position: sticky; - top: 61px; + top: ${SECTION_HEADER_HEIGHT.tablet}; margin: 0 -16px; & > div { padding: 0 16px; @@ -769,7 +737,7 @@ const StyledSectionSubmenu = styled.div` @media ${mobile} { position: sticky; - top: 53px; + top: ${SECTION_HEADER_HEIGHT.mobile}; } `; diff --git a/packages/shared/components/section/Section.types.ts b/packages/shared/components/section/Section.types.ts index 85e53b8e9c..061b602129 100644 --- a/packages/shared/components/section/Section.types.ts +++ b/packages/shared/components/section/Section.types.ts @@ -73,8 +73,11 @@ export interface SectionBodyProps { export interface SectionContainerProps { showTwoProgress?: boolean; isSectionHeaderAvailable: boolean; + isInfoPanelVisible?: boolean; viewAs?: TViewAs; children: React.ReactNode; + withBodyScroll: boolean; + currentDeviceType?: DeviceType; } export interface SectionFilterProps { diff --git a/packages/shared/components/section/index.tsx b/packages/shared/components/section/index.tsx index 340a02219c..3bd59c294d 100644 --- a/packages/shared/components/section/index.tsx +++ b/packages/shared/components/section/index.tsx @@ -200,31 +200,37 @@ const Section = (props: SectionProps) => { viewAs={viewAs} ref={containerRef} isSectionHeaderAvailable={isSectionHeaderAvailable} + isInfoPanelVisible={isInfoPanelVisible} showTwoProgress={showTwoProgress} + withBodyScroll={withBodyScroll} + currentDeviceType={currentDeviceType} > - {isSectionHeaderAvailable && - currentDeviceType === DeviceType.desktop && ( - - {sectionHeaderContent} - - )} + {currentDeviceType !== DeviceType.mobile && ( +
+ {isSectionHeaderAvailable && ( + + {sectionHeaderContent} + + )} - {isSectionSubmenuAvailable && - currentDeviceType === DeviceType.desktop && ( - {sectionSubmenuContent} - )} - {isSectionFilterAvailable && - currentDeviceType === DeviceType.desktop && ( - - {sectionFilterContent} - - )} + {isSectionSubmenuAvailable && ( + {sectionSubmenuContent} + )} + + {isSectionFilterAvailable && + currentDeviceType === DeviceType.desktop && ( + + {sectionFilterContent} + + )} +
+ )} {isSectionBodyAvailable && ( { getContextModel={getContextModel} > {isSectionHeaderAvailable && - currentDeviceType !== DeviceType.desktop && ( + currentDeviceType === DeviceType.mobile && ( { {sectionWarningContent} )} {isSectionSubmenuAvailable && - currentDeviceType !== DeviceType.desktop && ( + currentDeviceType === DeviceType.mobile && ( {sectionSubmenuContent} )} {isSectionFilterAvailable && diff --git a/packages/shared/components/section/sub-components/InfoPanelBody.tsx b/packages/shared/components/section/sub-components/InfoPanelBody.tsx index 38df8bf624..c004207fba 100644 --- a/packages/shared/components/section/sub-components/InfoPanelBody.tsx +++ b/packages/shared/components/section/sub-components/InfoPanelBody.tsx @@ -47,7 +47,8 @@ const SubInfoPanelBody = ({ ref={scrollRef} $isScrollLocked={scrollLocked} noScrollY={scrollLocked} - scrollclass="section-scroll info-panel-scroll" + scrollClass="section-scroll info-panel-scroll" + createContext > {children}
diff --git a/packages/shared/components/section/sub-components/SectionBody.tsx b/packages/shared/components/section/sub-components/SectionBody.tsx index 4d1f6d2873..19bd8b3320 100644 --- a/packages/shared/components/section/sub-components/SectionBody.tsx +++ b/packages/shared/components/section/sub-components/SectionBody.tsx @@ -28,11 +28,8 @@ import React from "react"; // import { inject, observer } from "mobx-react"; -import { Scrollbar } from "@docspace/shared/components/scrollbar"; import { ContextMenu } from "@docspace/shared/components/context-menu"; -import { DeviceType } from "@docspace/shared/enums"; - import { StyledDropZoneBody, StyledSpacer, @@ -52,7 +49,6 @@ const SectionBody = React.memo( isDesktop, settingsStudio = false, - currentDeviceType, getContextModel, }: SectionBodyProps) => { const focusRef = React.useRef(null); @@ -98,7 +94,7 @@ const SectionBody = React.memo( React.useEffect(() => { if (!autoFocus) return; - if (focusRef.current) focusRef.current.focus(); + if (focusRef.current) focusRef.current.focus({ preventScroll: true }); }, [autoFocus]); const focusProps = autoFocus @@ -128,27 +124,12 @@ const SectionBody = React.memo( className="section-body" > {withScroll ? ( - currentDeviceType !== DeviceType.mobile ? ( - -
-
- {children} - -
-
-
- ) : ( -
-
- {children} - -
+
+
+ {children} +
- ) +
) : (
{children} @@ -168,27 +149,12 @@ const SectionBody = React.memo( className="section-body" > {withScroll ? ( - currentDeviceType !== DeviceType.mobile ? ( - -
-
- {children} - -
-
-
- ) : ( -
-
- {children} - -
+
+
+ {children} +
- ) +
) : (
{children}
)} diff --git a/packages/shared/components/section/sub-components/SectionContainer.tsx b/packages/shared/components/section/sub-components/SectionContainer.tsx index e9e508ef7e..5ae991d680 100644 --- a/packages/shared/components/section/sub-components/SectionContainer.tsx +++ b/packages/shared/components/section/sub-components/SectionContainer.tsx @@ -25,14 +25,33 @@ // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode import React from "react"; + +import { Scrollbar } from "../../scrollbar"; +import { DeviceType } from "../../../enums"; + import { StyledSectionContainer } from "../Section.styled"; import { SectionContainerProps } from "../Section.types"; const SectionContainer = React.forwardRef< HTMLDivElement, SectionContainerProps ->((props, forwardRef) => { - return ; +>(({ withBodyScroll, children, currentDeviceType, ...props }, forwardRef) => { + return ( + + {withBodyScroll && currentDeviceType !== DeviceType.mobile ? ( + + {children} + + ) : ( + children + )} + + ); }); SectionContainer.displayName = "SectionContainer"; diff --git a/packages/shared/components/social-buttons-group/SocialButtonsGroup.tsx b/packages/shared/components/social-buttons-group/SocialButtonsGroup.tsx index 8ea410632e..9b85add402 100644 --- a/packages/shared/components/social-buttons-group/SocialButtonsGroup.tsx +++ b/packages/shared/components/social-buttons-group/SocialButtonsGroup.tsx @@ -49,6 +49,7 @@ export const SocialButtonsGroup = memo( ssoSVG, t, isDisabled, + onMoreAuthToggle, }: SocialButtonProps) => { const [moreAuthVisible, setMoreAuthVisible] = useState(false); @@ -59,10 +60,12 @@ export const SocialButtonsGroup = memo( const moreAuthClose = () => { setMoreAuthVisible(false); + onMoreAuthToggle?.(false); }; const moreAuthOpen = () => { setMoreAuthVisible(true); + onMoreAuthToggle?.(true); }; const elements = showingProviders.map((item) => { const provider = item.provider; diff --git a/packages/shared/components/social-buttons-group/SocialButtonsGroup.types.ts b/packages/shared/components/social-buttons-group/SocialButtonsGroup.types.ts index b0c2c8256c..e1ba3777e3 100644 --- a/packages/shared/components/social-buttons-group/SocialButtonsGroup.types.ts +++ b/packages/shared/components/social-buttons-group/SocialButtonsGroup.types.ts @@ -43,6 +43,7 @@ export interface SocialButtonProps { t: TTranslation; /** Sets a callback function that is triggered when the button is clicked */ onClick: (e: React.MouseEvent) => void | Promise; + onMoreAuthToggle?: (value: boolean) => void; /** Sets the button to present a disabled state */ isDisabled: boolean; } diff --git a/packages/shared/components/table/sub-components/TableHeaderCell.tsx b/packages/shared/components/table/sub-components/TableHeaderCell.tsx index 985149da65..a22a18ed3c 100644 --- a/packages/shared/components/table/sub-components/TableHeaderCell.tsx +++ b/packages/shared/components/table/sub-components/TableHeaderCell.tsx @@ -31,7 +31,7 @@ import { Checkbox } from "../../checkbox"; import { Text } from "../../text"; import { IconButton } from "../../icon-button"; -import { globalColors } from "../../../themes"; +import { globalColors } from "../../../themes/globalColors"; import { StyledTableHeaderCell } from "../Table.styled"; import { TableHeaderCellProps } from "../Table.types"; diff --git a/packages/shared/utils/catalogIconHelper.ts b/packages/shared/utils/catalogIconHelper.ts index e327f37c69..200938cc44 100644 --- a/packages/shared/utils/catalogIconHelper.ts +++ b/packages/shared/utils/catalogIconHelper.ts @@ -25,7 +25,8 @@ // International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode import CatalogFolderReactSvgUrl from "PUBLIC_DIR/images/catalog.folder.react.svg?url"; -import CatalogUserReactSvgUrl from "PUBLIC_DIR/images/catalog.user.react.svg?url"; +// import CatalogUserReactSvgUrl from "PUBLIC_DIR/images/catalog.user.react.svg?url"; +import CatalogDocumentsReactSvgUrl from "PUBLIC_DIR/images/catalog.documents.react.svg?url"; import CatalogRoomsReactSvgUrl from "PUBLIC_DIR/images/catalog.rooms.react.svg?url"; import CatalogArchiveReactSvgUrl from "PUBLIC_DIR/images/catalog.archive.react.svg?url"; import CatalogSharedReactSvgUrl from "PUBLIC_DIR/images/catalog.shared.react.svg?url"; @@ -89,7 +90,7 @@ const defaultIcon: Record = { const icons: Record>> = { 16: { - [FolderType.USER]: CatalogUserReactSvgUrl, + [FolderType.USER]: CatalogDocumentsReactSvgUrl, [FolderType.Rooms]: CatalogRoomsReactSvgUrl, [FolderType.Archive]: CatalogArchiveReactSvgUrl, [FolderType.SHARE]: CatalogSharedReactSvgUrl,