Merge branch 'release/v2.5.0' of github.com:ONLYOFFICE/DocSpace-client into release/v2.5.0

This commit is contained in:
Timofey Boyko 2024-04-04 08:12:00 +03:00
commit ceb24f07fe
94 changed files with 1082 additions and 543 deletions

View File

@ -14,5 +14,9 @@
"MoveToTrashTitle": "Move to Trash?",
"UnsubscribeButton": "Unsubscribe",
"UnsubscribeNote": "Are you sure you want to unsubscribe from the selected items from the list?",
"UnsubscribeTitle": "Unsubscribe confirmation"
"UnsubscribeTitle": "Unsubscribe confirmation",
"DeleteGroupTitle": "Delete group",
"DeleteAllGroupsTitle": "Delete groups",
"DeleteGroupDescription": "Group {{groupName}} will be deleted. Users in the group will not be removed from DocSpace. Are you sure you want to continue?",
"DeleteAllGroupDescription": "The selected groups will be deleted. Users in the groups will not be removed from DocSpace. Are you sure you want to continue?"
}

View File

@ -122,6 +122,7 @@
"EnterTime": "Enter time",
"EnterTitle": "Enter title",
"ErrorMessageBruteForceProtection": "Specified argument was out of the range of valid values.",
"ErrorOccuredDownloadLog": "Errors occurred during data import. Download the log to check them.",
"ErrorsWereFound": "{{errors}} errors were found",
"ExistingAccount": "Existing account",
"ForcePathStyle": "Force Path Style",
@ -171,6 +172,7 @@
"NewColorScheme": "New color scheme",
"NextStep": "Next step",
"NoEmail": "NO EMAIL",
"NoUsersInBackup": "No users found. Try again or upload other backup files.",
"NumberOfActiveEmployees": "Number of active employees: {{count}}",
"NumberOfAttempts": "Number of attempts",
"PasswordMinLenght": "Minimal password length",
@ -304,4 +306,4 @@
"WithoutEmailHint": "You dont have users without emails. Please proceed to the next step.",
"YouHaveUnsavedChanges": "You have unsaved changes",
"YourCurrentDomain": "Your current domain"
}
}

View File

@ -6,7 +6,7 @@
"AccessRightsChangeOwnerConfirmText": "Изменения будут применены после подтверждения по электронной почте.",
"AccessRightsProductUsersCan": "В модуле {{category}} участники портала со статусом Пользователи могут",
"AccessRightsUsersFromList": "Участников со статусом {{users}} из списка",
"AccountsWithoutEmails": "Мы нашли <1>{{users}} пользователей</1> без электронной почты. На следующем шаге вы можете добавить необходимые данные в их учетные записи.",
"AccountsWithoutEmails": "Мы нашли <1>{{users}} пользователей</1> без электронной почты. Вы можете заполнить их электронные почты или продолжить без этого действия.",
"AddAllowedIP": "Добавить разрешенный IP-адрес",
"AddEmails": "Добавьте адреса электронной почты в незавершенные аккаунты",
"AdditionalResources": "Дополнительные ресурсы",
@ -279,4 +279,4 @@
"WhiteLabelTooltip": "Размеры указаны для дисплеев Retina. Для дисплеев со стандартным разрешением ширина и высота логотипа будут соответственно изменены при загрузке.",
"YouHaveUnsavedChanges": "Имеются несохраненные изменения",
"YourCurrentDomain": "Ваш текущий домен"
}
}

View File

@ -31,7 +31,7 @@ import { combineUrl } from "@docspace/shared/utils/combineUrl";
import Badges from "../components/Badges";
import config from "PACKAGE_FILE";
import copy from "copy-to-clipboard";
import { copyShareLink } from "@docspace/shared/utils/copy";
import { toastr } from "@docspace/shared/components/toast";
import { isMobileOnly } from "react-device-detect";
@ -119,7 +119,7 @@ export default function withBadges(WrappedComponent) {
const { t, item, getPrimaryLink } = this.props;
const primaryLink = await getPrimaryLink(item.id);
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
}
};

View File

@ -27,8 +27,8 @@
import React from "react";
import { inject, observer } from "mobx-react";
import { toastr } from "@docspace/shared/components/toast";
import { copyShareLink } from "@docspace/shared/utils/copy";
import QuickButtons from "../components/QuickButtons";
import copy from "copy-to-clipboard";
export default function withQuickButtons(WrappedComponent) {
class WithQuickButtons extends React.Component {
@ -83,7 +83,7 @@ export default function withQuickButtons(WrappedComponent) {
const { t, item, getPrimaryFileLink, setShareChanged } = this.props;
const primaryLink = await getPrimaryFileLink(item.id);
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
item.shared
? toastr.success(t("Common:LinkSuccessfullyCopied"))
: toastr.success(t("Files:LinkSuccessfullyCreatedAndCopied"));
@ -95,7 +95,7 @@ export default function withQuickButtons(WrappedComponent) {
const { t, item, getPrimaryLink } = this.props;
const primaryLink = await getPrimaryLink(item.id);
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
}
};

View File

@ -402,10 +402,14 @@ const ArticleMainButtonContent = (props) => {
action: EmployeeType.Guest,
key: "user",
},
{
isSeparator: true,
key: "invite-users-separator",
},
...(!isMobileArticle
? [
{
isSeparator: true,
key: "invite-users-separator",
},
]
: []),
{
id: "invite_again",
className: "main-button_drop-down",
@ -541,6 +545,7 @@ const ArticleMainButtonContent = (props) => {
onShowSelectFileDialog,
onUploadFileClick,
onUploadFolderClick,
isMobileArticle,
]);
const mainButtonText = t("Common:Actions");

View File

@ -38,6 +38,12 @@ const StyledBody = styled.div`
max-height: 32px;
}
.quota_checkbox {
svg {
margin-right: 8px;
}
}
.quota_value {
max-width: fit-content;
padding: 0;

View File

@ -225,6 +225,7 @@ const QuotaForm = ({
<Checkbox
label={checkboxLabel}
isChecked={isChecked}
className="quota_checkbox"
onChange={onChangeCheckbox}
isDisabled={isLoading || isDisabled}
/>

View File

@ -60,7 +60,7 @@ const PureConnectDialogContainer = (props) => {
setIsConnectDialogReconnect,
saveAfterReconnectOAuth,
setSaveAfterReconnectOAuth,
setSelectedThirdPartyAccount,
setThirdPartyAccountsInfo,
} = props;
const { title, link, token, provider_id, provider_key, key } = item;
@ -169,9 +169,8 @@ const PureConnectDialogContainer = (props) => {
provider_key,
provider_id,
)
.then(() => {
onClose();
setSelectedThirdPartyAccount(null);
.then(async () => {
await setThirdPartyAccountsInfo();
})
.catch((err) => {
toastr.error(err);
@ -424,7 +423,7 @@ export default inject(
const { id, folders } = selectedFolderStore;
const {
selectedThirdPartyAccount: backupConnectionItem,
setSelectedThirdPartyAccount,
setThirdPartyAccountsInfo,
} = backup;
const {
connectDialogVisible: visible,
@ -455,12 +454,12 @@ export default inject(
openConnectWindow,
fetchThirdPartyProviders,
setConnectDialogVisible,
setSelectedThirdPartyAccount,
personal,
isConnectDialogReconnect,
saveAfterReconnectOAuth,
setSaveAfterReconnectOAuth,
setIsConnectDialogReconnect,
setThirdPartyAccountsInfo,
};
},
)(observer(ConnectDialog));

View File

@ -116,6 +116,9 @@ const CreateRoomDialog = ({
setRoomParams((prev) => ({
...prev,
type: newRoomType,
storageLocation: {
isThirdparty: false,
},
}));
};

View File

@ -42,7 +42,7 @@ const DialogHeader = ({ t, isEdit, isChooseRoomType, onArrowClick }) => {
) : (
<div className="header-with-button">
<IconButton
size="15px"
size={17}
iconName={ArrowPathReactSvgUrl}
className="sharing_panel-arrow"
onClick={onArrowClick}

View File

@ -52,22 +52,30 @@ const StyledDropdownMobile = styled.div`
StyledDropdownMobile.defaultProps = { theme: Base };
const DropdownMobile = ({ t, open, onClose, chooseRoomType }) => {
const DropdownMobile = ({
t,
open,
onClose,
chooseRoomType,
forсeHideDropdown,
}) => {
return (
<>
<Backdrop visible={open} onClick={onClose} zIndex={450} />
<StyledDropdownMobile className="dropdown-mobile" isOpen={open}>
{RoomsTypeValues.map((roomType) => (
<RoomType
id={roomType}
t={t}
key={roomType}
roomType={roomType}
type="dropdownItem"
onClick={() => chooseRoomType(roomType)}
/>
))}
</StyledDropdownMobile>
{!forсeHideDropdown && (
<StyledDropdownMobile className="dropdown-mobile" isOpen={open}>
{RoomsTypeValues.map((roomType) => (
<RoomType
id={roomType}
t={t}
key={roomType}
roomType={roomType}
type="dropdownItem"
onClick={() => chooseRoomType(roomType)}
/>
))}
</StyledDropdownMobile>
)}
</>
);
};

View File

@ -25,7 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { isMobile } from "@docspace/shared/utils";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import styled from "styled-components";
import RoomType from "../RoomType";
import DropdownDesktop from "./DropdownDesktop";
@ -49,6 +49,7 @@ const RoomTypeDropdown = ({
setRoomType,
setIsScrollLocked,
isDisabled,
forсeHideDropdown,
}) => {
const [isOpen, setIsOpen] = useState(false);
@ -68,6 +69,14 @@ const RoomTypeDropdown = ({
setRoomType(roomType);
toggleDropdown();
};
useEffect(() => {
if (forсeHideDropdown) {
setIsScrollLocked(false);
setIsOpen(false);
}
}, [forсeHideDropdown]);
return (
<StyledRoomTypeDropdown isOpen={isOpen}>
<RoomType
@ -85,6 +94,7 @@ const RoomTypeDropdown = ({
open={isOpen}
onClose={toggleDropdown}
chooseRoomType={chooseRoomType}
forсeHideDropdown={forсeHideDropdown}
/>
) : (
<DropdownDesktop t={t} open={isOpen} chooseRoomType={chooseRoomType} />

View File

@ -103,6 +103,9 @@ const SetRoomParams = ({
useState(true);
const [disableImageRescaling, setDisableImageRescaling] = useState(isEdit);
const [forceHideRoomTypeDropdown, setForceHideRoomTypeDropdown] =
useState(false);
const isFormRoom = roomParams.type === RoomsType.FormRoom;
const isPublicRoom = roomParams.type === RoomsType.PublicRoom;
@ -155,6 +158,7 @@ const SetRoomParams = ({
setRoomType={setRoomType}
setIsScrollLocked={setIsScrollLocked}
isDisabled={isDisabled}
forсeHideDropdown={forceHideRoomTypeDropdown}
/>
)}
{isEdit && (
@ -176,6 +180,8 @@ const SetRoomParams = ({
isDisabled={isDisabled}
isValidTitle={isValidTitle}
isWrongTitle={isWrongTitle}
onFocus={() => setForceHideRoomTypeDropdown(true)}
onBlur={() => setForceHideRoomTypeDropdown(false)}
errorMessage={
isWrongTitle
? t("Files:ContainsSpecCharacter")
@ -184,11 +190,14 @@ const SetRoomParams = ({
onKeyUp={onKeyUp}
isAutoFocussed={true}
/>
<TagInput
t={t}
tagHandler={tagHandler}
setIsScrollLocked={setIsScrollLocked}
isDisabled={isDisabled}
onFocus={() => setForceHideRoomTypeDropdown(true)}
onBlur={() => setForceHideRoomTypeDropdown(false)}
/>
{/* //TODO: Uncomment when private rooms are done

View File

@ -52,7 +52,14 @@ const StyledTagInput = styled.div`
${({ hasTags }) => !hasTags && "margin-bottom: -8px"}
`;
const TagInput = ({ t, tagHandler, setIsScrollLocked, isDisabled }) => {
const TagInput = ({
t,
tagHandler,
setIsScrollLocked,
isDisabled,
onFocus,
onBlur,
}) => {
const inputRef = useRef();
const [tagInput, setTagInput] = useState("");
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
@ -69,13 +76,6 @@ const TagInput = ({ t, tagHandler, setIsScrollLocked, isDisabled }) => {
setTagInput(text);
};
const handleFocus = (event) => {
const text = event.target.value;
if (text.trim().length > 0) {
openDropdown();
}
};
const openDropdown = () => {
if (isDisabled) return;
setIsScrollLocked(true);
@ -87,6 +87,19 @@ const TagInput = ({ t, tagHandler, setIsScrollLocked, isDisabled }) => {
setIsDropdownOpen(false);
};
const handleFocus = (event) => {
const text = event.target.value;
if (text.trim().length > 0) {
openDropdown();
}
onFocus();
};
const handleBlur = () => {
closeDropdown();
onBlur();
};
const handleKeyDown = (event) => {
const keyCode = event.code;
@ -110,8 +123,8 @@ const TagInput = ({ t, tagHandler, setIsScrollLocked, isDisabled }) => {
placeholder={t("TagsPlaceholder")}
value={tagInput}
onChange={onTagInputChange}
onBlur={closeDropdown}
onFocus={handleFocus}
onBlur={handleBlur}
isDisabled={isDisabled}
onKeyDown={handleKeyDown}
/>

View File

@ -92,6 +92,7 @@ const StyledStorageLocation = styled.div`
align-items: center;
justify-content: center;
width: 6.35px;
margin-top: -4px;
svg {
transform: ${(props) =>
props.isOpen ? "rotate(180deg)" : "rotate(0)"};

View File

@ -0,0 +1,137 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useEffect } from "react";
import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { Button } from "@docspace/shared/components/button";
import { Text } from "@docspace/shared/components/text";
import { toastr } from "@docspace/shared/components/toast";
import ModalDialogContainer from "../ModalDialogContainer";
const DeleteGroupDialog = (props) => {
const {
t,
visible,
onClose,
selection,
bufferSelection,
groupName,
onDeleteGroup,
onDeleteAllGroups,
isLoading,
} = props;
useEffect(() => {
document.addEventListener("keyup", onKeyUp, false);
return () => {
document.removeEventListener("keyup", onKeyUp, false);
};
}, []);
const onKeyUp = (e) => {
if (e.keyCode === 27) onClose();
if (e.keyCode === 13 || e.which === 13) onDeleteAction();
};
const hasMoreGroups = selection.length > 1;
const onDeleteAction = () => {
try {
if (hasMoreGroups) {
onDeleteAllGroups(t);
} else {
onDeleteGroup(t, bufferSelection?.id || selection[0].id);
}
} catch (err) {
toastr.error(err.message);
console.error(err);
}
};
return (
<ModalDialogContainer
visible={visible}
onClose={onClose}
displayType="modal"
>
<ModalDialog.Header>
{hasMoreGroups
? t("DeleteDialog:DeleteAllGroupsTitle")
: t("DeleteDialog:DeleteGroupTitle")}
</ModalDialog.Header>
<ModalDialog.Body>
<Text>
{hasMoreGroups
? t("DeleteDialog:DeleteAllGroupDescription")
: t("DeleteDialog:DeleteGroupDescription", { groupName })}
</Text>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
id="group-modal_delete"
key="Delete"
label={t("Common:Delete")}
size="normal"
primary
scale
onClick={onDeleteAction}
isLoading={isLoading}
/>
<Button
id="group-modal_cancel"
key="CancelButton"
label={t("Common:CancelButton")}
size="normal"
scale
onClick={onClose}
/>
</ModalDialog.Footer>
</ModalDialogContainer>
);
};
export default inject(({ peopleStore }) => {
const {
selection,
bufferSelection,
groupName,
onDeleteGroup,
onDeleteAllGroups,
isLoading,
} = peopleStore.groupsStore;
return {
selection,
bufferSelection,
groupName,
onDeleteGroup,
onDeleteAllGroups,
isLoading,
};
})(withTranslation(["Common", "DeleteDialog"])(observer(DeleteGroupDialog)));

View File

@ -50,6 +50,7 @@ const DeleteThirdPartyDialog = (props) => {
setDeleteThirdPartyDialogVisible,
isConnectionViaBackupModule,
updateInfo,
setConnectedThirdPartyAccount,
} = props;
const [isLoading, setIsLoading] = useState(false);
@ -60,10 +61,13 @@ const DeleteThirdPartyDialog = (props) => {
const onClose = () => setDeleteThirdPartyDialogVisible(false);
const onDeleteThirdParty = () => {
setIsLoading(true);
if (isConnectionViaBackupModule) {
deleteThirdParty(+removeItem.provider_id)
.catch((err) => toastr.error(err))
.finally(() => {
setConnectedThirdPartyAccount(null);
updateInfo && updateInfo();
setIsLoading(false);
onClose();
@ -76,7 +80,6 @@ const DeleteThirdPartyDialog = (props) => {
(x) => x.provider_id !== removeItem.id,
);
setIsLoading(true);
deleteThirdParty(+removeItem.id)
.then(() => {
setThirdPartyProviders(newProviders);
@ -145,7 +148,10 @@ export default inject(
const { providers, setThirdPartyProviders, deleteThirdParty } =
filesSettingsStore.thirdPartyStore;
const { setIsLoading } = filesStore;
const { selectedThirdPartyAccount: backupConnectionItem } = backup;
const {
selectedThirdPartyAccount: backupConnectionItem,
setConnectedThirdPartyAccount,
} = backup;
const {
deleteThirdPartyDialogVisible: visible,
setDeleteThirdPartyDialogVisible,
@ -168,6 +174,7 @@ export default inject(
setThirdPartyProviders,
deleteThirdParty,
setDeleteThirdPartyDialogVisible,
setConnectedThirdPartyAccount,
};
},
)(

View File

@ -68,6 +68,7 @@ import DeletePluginDialog from "./DeletePluginDialog";
import ShareFolderDialog from "./ShareFolderDialog";
import EditGroupMembersDialog from "./EditGroupMembersDialog";
import ChangeStorageQuotaDialog from "./ChangeStorageQuotaDialog";
import DeleteGroupDialog from "./DeleteGroupDialog";
export {
EmptyTrashDialog,
@ -114,4 +115,5 @@ export {
EditGroupMembersDialog,
ChangeQuotaDialog,
ChangeStorageQuotaDialog,
DeleteGroupDialog,
};

View File

@ -393,7 +393,13 @@ const AddUsersPanel = ({
isGroup?: boolean,
) => {
return (
<div style={{ width: "100%" }}>
<div
style={{
width: "100%",
overflow: "hidden",
marginInlineEnd: "16px",
}}
>
<Text
className="label"
fontWeight={600}

View File

@ -315,6 +315,8 @@ const InvitePanel = ({
addInfoPanelMembers(t, result.members);
}
console.log(result);
onClose();
toastr.success(t("Common:UsersInvited"));

View File

@ -66,7 +66,8 @@ export const GroupsContent = styled.div<{}>`
.email {
max-width: 180px;
color: #a3a9ae;
font-size: ${({ theme }) => theme.getCorrectFontSize("10px")};
font-size: ${({ theme }) => theme.getCorrectFontSize("12px")};
line-height: 16px;
font-style: normal;
font-weight: 400;
overflow: hidden;

View File

@ -30,7 +30,7 @@ import { withTranslation } from "react-i18next";
import { Text } from "@docspace/shared/components/text";
import DefaultUserPhoto from "PUBLIC_DIR/images/default_user_photo_size_82-82.png";
import { ContextMenuButton } from "@docspace/shared/components/context-menu-button";
import { Avatar } from "@docspace/shared/components/avatar";
import { Avatar, AvatarSize } from "@docspace/shared/components/avatar";
import { Badge } from "@docspace/shared/components/badge";
import Badges from "@docspace/client/src/pages/Home/Section/AccountsBody/Badges";
import { StyledAccountsItemTitle } from "../../styles/accounts";
@ -79,7 +79,7 @@ const AccountsItemTitle = ({
<Avatar
className="avatar"
role={infoPanelSelection.role ? infoPanelSelection.role : "user"}
size={"big"}
size={AvatarSize.max}
source={userAvatar}
/>
<div className="info-panel__info-text">

View File

@ -30,7 +30,7 @@ import { withTranslation } from "react-i18next";
import { Text } from "@docspace/shared/components/text";
import DefaultUserPhoto from "PUBLIC_DIR/images/default_user_photo_size_82-82.png";
import { ContextMenuButton } from "@docspace/shared/components/context-menu-button";
import { Avatar } from "@docspace/shared/components/avatar";
import { Avatar, AvatarSize } from "@docspace/shared/components/avatar";
import { StyledAccountsItemTitle } from "../../styles/accounts";
import { decode } from "he";
@ -56,7 +56,7 @@ const GroupsItemTitle = ({
<StyledAccountsItemTitle ref={itemTitleRef}>
<Avatar
className="avatar"
size={"big"}
size={AvatarSize.max}
userName={infoPanelSelection.name}
isGroup={true}
/>

View File

@ -43,11 +43,12 @@ import { Text } from "@docspace/shared/components/text";
import { Link } from "@docspace/shared/components/link";
import { IconButton } from "@docspace/shared/components/icon-button";
import { Tooltip } from "@docspace/shared/components/tooltip";
import { isDesktop } from "@docspace/shared/utils";
import LinksToViewingIconUrl from "PUBLIC_DIR/images/links-to-viewing.react.svg?url";
import PlusReactSvgUrl from "PUBLIC_DIR/images/actions.button.plus.react.svg?url";
import { Avatar } from "@docspace/shared/components/avatar";
import copy from "copy-to-clipboard";
import { copyShareLink } from "@docspace/shared/utils/copy";
import LinkRow from "./sub-components/LinkRow";
const Members = ({
@ -140,7 +141,7 @@ const Members = ({
} else {
getPrimaryLink(infoPanelSelection.id).then((link) => {
setExternalLink(link);
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Files:LinkSuccessfullyCreatedAndCopied"));
});
}
@ -174,7 +175,7 @@ const Members = ({
{additionalLinks.length >= LINKS_LIMIT_COUNT && (
<Tooltip
float
float={isDesktop()}
id="emailTooltip"
getContent={({ content }) => (
<Text fontSize="12px">{content}</Text>

View File

@ -174,12 +174,13 @@ const LinkRow = (props) => {
// onClick: onEmbeddingClick,
// },
!disabled && {
key: "copy-link-settings-key",
label: t("Files:CopySharedLink"),
icon: CopyToReactSvgUrl,
onClick: onCopyExternalLink,
},
!disabled &&
!isExpired && {
key: "copy-link-settings-key",
label: t("Files:CopySharedLink"),
icon: CopyToReactSvgUrl,
onClick: onCopyExternalLink,
},
// disabled
// ? {
@ -245,7 +246,7 @@ const LinkRow = (props) => {
{disabled && <Text color={textColor}>{t("Settings:Disabled")}</Text>}
<div className="external-row-icons">
{!disabled && !isArchiveFolder && (
{!disabled && !isExpired && !isArchiveFolder && (
<>
{isLocked && (
<IconButton

View File

@ -100,6 +100,9 @@ const StyledLinkRow = styled.div`
.avatar_role-wrapper {
${({ isExpired, theme }) => css`
svg {
border: ${(props) => `1px solid ${props.theme.backgroundColor}`};
border-radius: 50%;
path {
fill: ${isExpired
? theme.infoPanel.links.iconErrorColor

View File

@ -41,6 +41,7 @@ import {
ChangeNameDialog,
ResetApplicationDialog,
DataReassignmentDialog,
DeleteGroupDialog,
} from "SRC_DIR/components/dialogs";
const Dialogs = ({
@ -66,6 +67,7 @@ const Dialogs = ({
profile,
dataReassignmentDialogVisible,
deleteGroupDialogVisible,
}) => {
return (
<>
@ -146,6 +148,13 @@ const Dialogs = ({
user={data}
/>
)}
{deleteGroupDialogVisible && (
<DeleteGroupDialog
visible={deleteGroupDialogVisible}
onClose={closeDialogs}
/>
)}
</>
);
};
@ -166,6 +175,7 @@ export default inject(({ peopleStore, userStore }) => {
sendInviteDialogVisible,
resetAuthDialogVisible,
dataReassignmentDialogVisible,
deleteGroupDialogVisible,
} = peopleStore.dialogStore;
const { user: profile } = userStore;
@ -202,5 +212,6 @@ export default inject(({ peopleStore, userStore }) => {
profile,
dataReassignmentDialogVisible,
deleteGroupDialogVisible,
};
})(observer(Dialogs));

View File

@ -30,6 +30,7 @@ import { withTranslation } from "react-i18next";
import { TableHeader } from "@docspace/shared/components/table";
import { Events } from "@docspace/shared/enums";
import { SortByFieldName } from "SRC_DIR/helpers/constants";
const TABLE_VERSION = "6";
const TABLE_COLUMNS = `insideGroupTableColumns_ver-${TABLE_VERSION}`;
@ -87,6 +88,19 @@ class InsideGroupTableHeader extends React.Component {
},
];
props.showStorageInfo &&
defaultColumns.push({
key: "Storage",
title: props.isDefaultUsersQuotaSet
? t("Common:StorageAndQuota")
: t("Common:Storage"),
enable: props.storageAccountsColumnIsEnabled,
sortBy: SortByFieldName.UsedSpace,
resizable: true,
onChange: this.onColumnChange,
onClick: this.onFilter,
});
const columns = props.getColumns(defaultColumns);
this.state = { columns };
@ -188,6 +202,7 @@ export default inject(
settingsStore,
userStore,
tableStore,
currentQuotaStore,
}) => {
const { groupsStore } = peopleStore;
@ -203,8 +218,11 @@ export default inject(
typeAccountsInsideGroupColumnIsEnabled,
groupAccountsInsideGroupColumnIsEnabled,
emailAccountsInsideGroupColumnIsEnabled,
storageAccountsColumnIsEnabled,
} = tableStore;
const { showStorageInfo, isDefaultUsersQuotaSet } = currentQuotaStore;
return {
filter,
setFilter,
@ -219,6 +237,9 @@ export default inject(
typeAccountsInsideGroupColumnIsEnabled,
groupAccountsInsideGroupColumnIsEnabled,
emailAccountsInsideGroupColumnIsEnabled,
storageAccountsColumnIsEnabled,
showStorageInfo,
isDefaultUsersQuotaSet,
};
},
)(

View File

@ -25,6 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState } from "react";
import { inject, observer } from "mobx-react";
import styled, { css } from "styled-components";
import { withTranslation } from "react-i18next";
import { TableRow } from "@docspace/shared/components/table";
@ -38,6 +39,8 @@ import Badges from "../../Badges";
import { Base } from "@docspace/shared/themes";
import { useNavigate } from "react-router-dom";
import SpaceQuota from "SRC_DIR/components/SpaceQuota";
const StyledWrapper = styled.div`
display: contents;
`;
@ -119,6 +122,7 @@ const StyledPeopleRow = styled(TableRow)`
opacity: ${(props) => (props.hideColumns ? 0 : 1)};
& > div {
width: auto;
max-width: fit-content;
}
}
@ -220,6 +224,8 @@ const InsideGroupTableRow = (props) => {
typeAccountsInsideGroupColumnIsEnabled,
groupAccountsInsideGroupColumnIsEnabled,
emailAccountsInsideGroupColumnIsEnabled,
showStorageInfo,
storageAccountsColumnIsEnabled,
} = props;
const {
@ -373,7 +379,7 @@ const InsideGroupTableRow = (props) => {
plusBadgeValue={groups.length - 1}
onSelect={onOpenGroup}
options={groupItems}
scaled
scaled={false}
directionY="both"
size="content"
modernView
@ -413,7 +419,7 @@ const InsideGroupTableRow = (props) => {
}
options={typesOptions}
onSelect={onTypeChange}
scaled
scaled={false}
directionY="both"
size="content"
displaySelectedOption
@ -461,6 +467,7 @@ const InsideGroupTableRow = (props) => {
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.target.closest(".type-combobox") ||
e.target.closest(".groups-combobox") ||
e.target.closest(".paid-badge") ||
e.target.closest(".pending-badge") ||
e.target.closest(".disabled-badge") ||
@ -533,10 +540,7 @@ const InsideGroupTableRow = (props) => {
)}
{groupAccountsInsideGroupColumnIsEnabled ? (
<TableCell
className={"table-cell_groups"}
onClick={(e) => e.stopPropagation()}
>
<TableCell className={"table-cell_groups"}>
{renderGroupsCell()}
</TableCell>
) : (
@ -602,11 +606,32 @@ const InsideGroupTableRow = (props) => {
) : (
<div />
)}
{showStorageInfo &&
(storageAccountsColumnIsEnabled ? (
<TableCell className={"table-cell_Storage/Quota"}>
<SpaceQuota hideColumns={hideColumns} item={item} type="user" />
</TableCell>
) : (
<div />
))}
</StyledPeopleRow>
</StyledWrapper>
);
};
export default withTranslation(["People", "Common", "Settings"])(
withContent(InsideGroupTableRow),
export default inject(({ currentQuotaStore, tableStore }) => {
const { showStorageInfo } = currentQuotaStore;
const { storageAccountsColumnIsEnabled } = tableStore;
return {
showStorageInfo,
storageAccountsColumnIsEnabled,
};
})(
withContent(
withTranslation(["People", "Common", "Settings"])(
observer(InsideGroupTableRow),
),
),
);

View File

@ -127,6 +127,7 @@ const StyledPeopleRow = styled(TableRow)`
opacity: ${(props) => (props.hideColumns ? 0 : 1)};
& > div {
width: auto;
max-width: fit-content;
}
}
@ -382,7 +383,7 @@ const PeopleTableRow = (props) => {
plusBadgeValue={groups.length - 1}
onSelect={onOpenGroup}
options={groupItems}
scaled
scaled={false}
directionY="both"
size="content"
modernView
@ -421,7 +422,7 @@ const PeopleTableRow = (props) => {
}
options={typesOptions}
onSelect={onTypeChange}
scaled
scaled={false}
directionY="both"
size="content"
displaySelectedOption
@ -469,6 +470,7 @@ const PeopleTableRow = (props) => {
e.target.closest(".checkbox") ||
e.target.closest(".table-container_row-checkbox") ||
e.target.closest(".type-combobox") ||
e.target.closest(".groups-combobox") ||
e.target.closest(".paid-badge") ||
e.target.closest(".pending-badge") ||
e.target.closest(".disabled-badge") ||
@ -541,10 +543,7 @@ const PeopleTableRow = (props) => {
)}
{groupAccountsColumnIsEnabled ? (
<TableCell
className={"table-cell_groups"}
onClick={(e) => e.stopPropagation()}
>
<TableCell className={"table-cell_groups"}>
{renderGroupsCell()}
</TableCell>
) : (

View File

@ -179,7 +179,7 @@ const StyledTableRow = styled(TableRow)`
.table-container_element-wrapper,
.table-container_row-loader {
min-width: ${(props) => (props.isRoom ? "40px" : "36px")};
min-width: 40px;
border-bottom: unset;
${(props) =>
props.theme.interfaceDirection === "rtl"
@ -194,7 +194,7 @@ const StyledTableRow = styled(TableRow)`
}
.table-container_element-container {
width: 32px;
width: 36px;
height: 32px;
display: flex;
@ -207,10 +207,10 @@ const StyledTableRow = styled(TableRow)`
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: 4px;
margin-right: 8px;
`
: css`
margin-left: 4px;
margin-left: 8px;
`}
}
}

View File

@ -225,8 +225,6 @@ const Table = ({
isTrashFolder,
]);
console.log("Table log TableContainer columnStorageName", columnStorageName);
return (
<StyledTableContainer useReactWindow={!withPaging} forwardedRef={ref}>
<TableHeader

View File

@ -563,8 +563,6 @@ class FilesTableHeader extends React.Component {
const sortBy = isRooms ? roomsFilter.sortBy : filter.sortBy;
const sortOrder = isRooms ? roomsFilter.sortOrder : filter.sortOrder;
console.log("Table log TableHeader columnStorageName", columnStorageName);
return (
<TableHeader
isLengthenHeader={firstElemChecked || isHeaderChecked}

View File

@ -86,13 +86,14 @@ import {
FolderType,
ShareAccessRights,
} from "@docspace/shared/enums";
import { getLogoFromPath } from "@docspace/shared/utils";
import { copyShareLink } from "@docspace/shared/utils/copy";
import { CategoryType } from "SRC_DIR/helpers/constants";
import {
getCategoryTypeByFolderType,
getCategoryUrl,
} from "SRC_DIR/helpers/utils";
import { getLogoFromPath } from "@docspace/shared/utils";
import TariffBar from "SRC_DIR/components/TariffBar";
const StyledContainer = styled.div`
@ -182,6 +183,17 @@ const StyledContainer = styled.div`
display: none;
}
}
.title-icon {
svg {
path {
fill: ${(props) => props.theme.backgroundColor};
}
rect {
stroke: ${(props) => props.theme.backgroundColor};
}
}
}
}
`;
@ -816,12 +828,12 @@ const SectionHeaderContent = (props) => {
icon: CopyToReactSvgUrl,
onClick: async () => {
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
toastr.success(t("Translations:LinkCopySuccess"));
} else {
const link = await getPrimaryLink(currentFolderId);
if (link) {
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Files:LinkSuccessfullyCreatedAndCopied"));
setExternalLink(link);
}

View File

@ -157,7 +157,7 @@ const DNSSettings = (props) => {
isError && setIsError(false);
setErrorText("");
const { value } = e.target;
const value = e.target.value.trim();
const isValidDomain = parseDomain(value || "", setErrorText, t);
@ -202,7 +202,7 @@ const DNSSettings = (props) => {
<TextInput
{...textInputProps}
isDisabled={isLoading || !enable}
value={dnsName?.trim()}
value={dnsName}
onChange={onChangeTextInput}
hasError={isError}
/>

View File

@ -36,7 +36,7 @@ import { HelpButton } from "@docspace/shared/components/help-button";
import { toastr } from "@docspace/shared/components/toast";
const Wrapper = styled.div`
margin: 0 0 16px;
margin: 16px 0 16px;
display: flex;
align-items: center;
@ -47,7 +47,7 @@ const Wrapper = styled.div`
const InfoText = styled(Text)`
margin-top: -8px;
margin-bottom: 16px;
margin-bottom: 8px;
font-size: 12px;
color: ${(props) => props.theme.client.settings.migration.subtitleColor};
`;
@ -55,7 +55,7 @@ const InfoText = styled(Text)`
const ErrorText = styled(Text)`
font-size: 12px;
color: ${(props) => props.theme.client.settings.migration.errorTextColor};
margin-bottom: 16px;
margin-bottom: 8px;
`;
const ImportCompleteStep = ({
@ -70,9 +70,12 @@ const ImportCompleteStep = ({
const [importResult, setImportResult] = useState({
succeedUsers: 0,
failedUsers: 0,
errors: [],
});
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
const onDownloadLog = async () => {
try {
await getMigrationLog()
@ -101,7 +104,11 @@ const ImportCompleteStep = ({
}
clearCheckedAccounts();
clearMigration();
setTimeout(() => navigate(-1), 1000);
setIsSaving(true);
setTimeout(() => {
setIsSaving(false);
navigate(-1);
}, 1000);
};
useEffect(() => {
@ -110,6 +117,7 @@ const ImportCompleteStep = ({
setImportResult({
succeedUsers: res.parseResult.successedUsers,
failedUsers: res.parseResult.failedUsers,
errors: res.parseResult.errors,
}),
);
} catch (error) {
@ -134,6 +142,10 @@ const ImportCompleteStep = ({
</ErrorText>
)}
{importResult.errors.length > 0 && (
<ErrorText>{t("Settings:ErrorOccuredDownloadLog")}</ErrorText>
)}
<Wrapper>
<Checkbox
label={t("Settings:SendWelcomeLetter")}
@ -158,6 +170,7 @@ const ImportCompleteStep = ({
cancelButtonLabel={t("Settings:DownloadLog")}
displaySettings
showReminder
isSaving={isSaving}
/>
</>
);

View File

@ -111,6 +111,7 @@ const SelectFileStep = ({
const [showErrorText, setShowErrorText] = useState(false);
const [isFileError, setIsFileError] = useState(false);
const [fileName, setFileName] = useState(null);
const [isBackupEmpty, setIsBackupEmpty] = useState(false);
const [searchParams] = useSearchParams();
const isAbort = useRef(false);
const uploadInterval = useRef(null);
@ -140,7 +141,7 @@ const SelectFileStep = ({
setIsFileError(false);
setShowReminder(true);
if (res.parseResult.files.length > 0) {
if (res.parseResult.files?.length > 0) {
setFileName(res.parseResult.files.join(", "));
}
@ -150,9 +151,16 @@ const SelectFileStep = ({
setFileName(null);
clearInterval(uploadInterval.current);
} else if (res.isCompleted || res.progress === 100) {
setUsers(res.parseResult);
setShowReminder(true);
onNextStep && onNextStep();
if (
res.parseResult.users.length +
res.parseResult.existUsers.length +
res.parseResult.withoutEmailUsers.length >
0
) {
setUsers(res.parseResult);
setShowReminder(true);
onNextStep && onNextStep();
}
clearInterval(uploadInterval.current);
}
} catch (error) {
@ -216,8 +224,19 @@ const SelectFileStep = ({
setIsFileLoading(false);
setIsVisible(false);
setProgress(100);
setUsers(res.parseResult);
setShowReminder(true);
if (
res.parseResult.users.length +
res.parseResult.existUsers.length +
res.parseResult.withoutEmailUsers.length >
0
) {
setUsers(res.parseResult);
setShowReminder(true);
} else {
setIsBackupEmpty(true);
cancelMigration();
}
}
} catch (error) {
toastr.error(error || t("Common:SomethingWentWrong"));
@ -339,6 +358,19 @@ const SelectFileStep = ({
</Box>
)}
{isBackupEmpty && (
<Box>
<ProgressBar
percent={100}
className="complete-progress-bar"
label={t("Common:LoadingIsComplete")}
/>
<Text className="error-text">
{t("Settings:NoUsersInBackup")}
</Text>
</Box>
)}
{isError ? (
<SaveCancelButtons
className="save-cancel-buttons"

View File

@ -47,18 +47,21 @@ const StyledRowContainer = styled(RowContainer)`
.table-group-menu {
height: 61px;
position: absolute;
position: sticky;
z-index: 201;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
right: -16px;
margin-right: -16px;
`
: css`
left: -16px;
margin-left: -16px;
`}
width: 100%;
margin-top: -35.5px;
margin-top: 20px;
top: 61px;
margin-bottom: -29.5px;
.table-container_group-menu {
padding: 0px 16px;

View File

@ -48,12 +48,14 @@ const StyledTableContainer = styled(TableContainer)`
.table-group-menu {
height: 69px;
position: absolute;
position: sticky;
z-index: 201;
left: 0px;
width: 100%;
width: calc(100% + 40px);
margin-top: 20px;
margin-left: -20px;
top: 0;
margin-top: -35.5px;
margin-bottom: -37.5px;
.table-container_group-menu {
border-image-slice: 0;

View File

@ -31,28 +31,40 @@ import { Consumer } from "@docspace/shared/utils/context";
import TableView from "./TableView";
import RowView from "./RowView";
const checkedAccountType = "result";
const AccountsTable = ({
t,
viewAs,
accountsData,
changeGroupType,
UserTypes,
toggleAllAccounts,
}) => {
const typeOptions = [
{
key: UserTypes.DocSpaceAdmin,
label: t("Common:DocSpaceAdmin"),
onClick: () => changeGroupType(UserTypes.DocSpaceAdmin),
onClick: () => {
changeGroupType(UserTypes.DocSpaceAdmin);
toggleAllAccounts(false, [], checkedAccountType);
},
},
{
key: UserTypes.RoomAdmin,
label: t("Common:RoomAdmin"),
onClick: () => changeGroupType(UserTypes.RoomAdmin),
onClick: () => {
changeGroupType(UserTypes.RoomAdmin);
toggleAllAccounts(false, [], checkedAccountType);
},
},
{
key: UserTypes.User,
label: t("Common:PowerUser"),
onClick: () => changeGroupType(UserTypes.User),
onClick: () => {
changeGroupType(UserTypes.User);
toggleAllAccounts(false, [], checkedAccountType);
},
},
];
@ -81,12 +93,13 @@ const AccountsTable = ({
export default inject(({ setup, importAccountsStore }) => {
const { viewAs } = setup;
const { changeGroupType, UserTypes } = importAccountsStore;
const { changeGroupType, UserTypes, toggleAllAccounts } = importAccountsStore;
return {
viewAs,
changeGroupType,
UserTypes,
toggleAllAccounts,
};
})(
withTranslation(["ChangeUserTypeDialog", "People"])(observer(AccountsTable)),

View File

@ -88,14 +88,16 @@ const SelectUsersTypeStep = ({
displaySettings
/>
<StyledSearchInput
id="search-users-type-input"
placeholder={t("Common:Search")}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
{!checkedUsers.result.length > 0 && (
<StyledSearchInput
id="search-users-type-input"
placeholder={t("Common:Search")}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
)}
<AccountsTable t={t} accountsData={filteredAccounts} />

View File

@ -43,7 +43,7 @@ const UsersRow = (props) => {
const emailTextRef = useRef();
const [isPrevEmailValid, setIsPrevEmailValid] = useState(
data.email.length > 0,
data.email?.length > 0,
);
const handleAccountToggle = (e) => {
@ -68,7 +68,7 @@ const UsersRow = (props) => {
id={data.key}
sectionWidth={sectionWidth}
displayName={data.displayName}
email={data.email}
email={data.email || ""}
emailInputRef={emailInputRef}
emailTextRef={emailTextRef}
isChecked={isChecked}

View File

@ -171,6 +171,7 @@ const UsersRowContent = ({
onValidateInput={onValidateEmail}
hasError={hasError}
onBlur={checkEmailValidity}
isAutoFocussed
/>
<DecisionButton icon={<CheckSvg />} onClick={handleSaveClick} />

View File

@ -187,6 +187,7 @@ const UsersTableRow = ({
onKeyDown={handleKeyDown}
hasError={hasError}
onBlur={checkEmailValidity}
isAutoFocussed
/>
<DecisionButton icon={<CheckSvg />} onClick={handleSaveClick} />

View File

@ -42,6 +42,11 @@ const ErrorText = styled(Text)`
color: ${(props) => props.theme.client.settings.migration.errorTextColor};
margin-bottom: 16px;
`;
const InfoText = styled(Text)`
margin-bottom: 8px;
font-size: 12px;
color: ${(props) => props.theme.client.settings.migration.subtitleColor};
`;
const ImportCompleteStep = ({
t,
@ -55,9 +60,12 @@ const ImportCompleteStep = ({
const [importResult, setImportResult] = useState({
succeedUsers: 0,
failedUsers: 0,
errors: [],
});
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
const onDownloadLog = async () => {
try {
await getMigrationLog()
@ -85,7 +93,11 @@ const ImportCompleteStep = ({
}
clearCheckedAccounts();
clearMigration();
setTimeout(() => navigate(-1), 1000);
setIsSaving(true);
setTimeout(() => {
setIsSaving(false);
navigate(-1);
}, 1000);
};
useEffect(() => {
@ -94,6 +106,7 @@ const ImportCompleteStep = ({
setImportResult({
succeedUsers: res.parseResult.successedUsers,
failedUsers: res.parseResult.failedUsers,
errors: res.parseResult.errors,
}),
);
} catch (error) {
@ -103,18 +116,22 @@ const ImportCompleteStep = ({
return (
<Wrapper>
<Text fontSize="12px">
<InfoText>
{t("Settings:ImportedUsers", {
selectedUsers: importResult.succeedUsers,
importedUsers: importResult.succeedUsers + importResult.failedUsers,
})}
</Text>
</InfoText>
{importResult.failedUsers > 0 && (
<ErrorText>
{t("Settings:ErrorsWereFound", { errors: importResult.failedUsers })}
</ErrorText>
)}
{importResult.errors.length > 0 && (
<ErrorText>{t("Settings:ErrorOccuredDownloadLog")}</ErrorText>
)}
<div className="sendLetterBlockWrapper">
<Checkbox
@ -140,6 +157,7 @@ const ImportCompleteStep = ({
cancelButtonLabel={t("Settings:DownloadLog")}
displaySettings
showReminder
isSaving={isSaving}
/>
</Wrapper>
);

View File

@ -114,7 +114,7 @@ const SelectFileStep = ({
setIsFileLoading,
cancelMigration,
}) => {
const [isSaveDisabled, setIsSaveDisabled] = useState(false);
const [isSaveDisabled, setIsSaveDisabled] = useState(true);
const [progress, setProgress] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const [isError, setIsError] = useState(false);
@ -147,15 +147,16 @@ const SelectFileStep = ({
}
setIsFileError(false);
setIsSaveDisabled(true);
setIsSaveDisabled(false);
if (res.parseResult.files.length > 0) {
if (res.parseResult.files?.length > 0) {
setFileName(res.parseResult.files.join(", "));
}
if (!res || res.parseResult.failedArchives.length > 0 || res.error) {
toastr.error(res.error);
setIsFileError(true);
setIsSaveDisabled(false);
clearInterval(uploadInterval.current);
} else if (res.isCompleted || res.progress === 100) {
setUsers(res.parseResult);

View File

@ -36,7 +36,6 @@ import AccountsPaging from "../../../sub-components/AccountsPaging";
// import UsersInfoBlock from "../../../sub-components/UsersInfoBlock";
import { Wrapper } from "../StyledStepper";
import { NoEmailUsersBlock } from "../../../sub-components/NoEmailUsersBlock";
// const LICENSE_LIMIT = 100;
@ -45,12 +44,10 @@ const SelectUsersStep = (props) => {
t,
incrementStep,
decrementStep,
users,
withEmailUsers,
searchValue,
setSearchValue,
cancelMigration,
areCheckedUsersEmpty,
} = props;
const [dataPortion, setDataPortion] = useState(withEmailUsers.slice(0, 25));
@ -84,10 +81,6 @@ const SelectUsersStep = (props) => {
return (
<Wrapper>
{withEmailUsers.length > 0 && (
<NoEmailUsersBlock users={users.withoutEmail.length} t={t} />
)}
{withEmailUsers.length > 0 ? (
<>
<SaveCancelButtons
@ -98,7 +91,6 @@ const SelectUsersStep = (props) => {
cancelButtonLabel={t("Common:Back")}
showReminder
displaySettings
saveButtonDisabled={areCheckedUsersEmpty}
/>
{/* <UsersInfoBlock
@ -142,7 +134,6 @@ const SelectUsersStep = (props) => {
cancelButtonLabel={t("Common:Back")}
showReminder
displaySettings
saveButtonDisabled={areCheckedUsersEmpty}
/>
)}
</Wrapper>
@ -156,7 +147,6 @@ export default inject(({ importAccountsStore }) => {
searchValue,
setSearchValue,
cancelMigration,
areCheckedUsersEmpty,
} = importAccountsStore;
return {
@ -165,6 +155,5 @@ export default inject(({ importAccountsStore }) => {
searchValue,
setSearchValue,
cancelMigration,
areCheckedUsersEmpty,
};
})(observer(SelectUsersStep));

View File

@ -47,19 +47,21 @@ const StyledRowContainer = styled(RowContainer)`
.table-group-menu {
height: 61px;
position: absolute;
position: sticky;
z-index: 201;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
right: -16px;
margin-right: -16px;
`
: css`
left: -16px;
margin-left: -16px;
`}
width: 100%;
margin-top: -35.5px;
margin-top: 20px;
top: 61px;
margin-bottom: -29.5px;
.table-container_group-menu {
padding: 0px 16px;

View File

@ -46,12 +46,14 @@ import ClearEmptyFilterSvgUrl from "PUBLIC_DIR/images/clear.empty.filter.svg?url
const UserSelectTableContainer = styled(StyledTableContainer)`
.table-group-menu {
height: 69px;
position: absolute;
position: sticky;
z-index: 201;
left: 0px;
width: 100%;
width: calc(100% + 40px);
margin-top: 20px;
margin-left: -20px;
top: 0;
margin-top: -35.5px;
margin-bottom: -37.5px;
.table-container_group-menu {
border-image-slice: 0;

View File

@ -31,12 +31,30 @@ import { Consumer } from "@docspace/shared/utils/context";
import TableView from "./TableView";
import RowView from "./RowView";
const AccountsTable = (props) => {
const { t, viewAs, accountsData, changeGroupType, UserTypes } = props;
const checkedAccountType = "result";
const setTypeDocspaceAdmin = () => changeGroupType(UserTypes.DocSpaceAdmin);
const setTypeRoomAdmin = () => changeGroupType(UserTypes.RoomAdmin);
const setTypeUser = () => changeGroupType(UserTypes.User);
const AccountsTable = (props) => {
const {
t,
viewAs,
accountsData,
changeGroupType,
UserTypes,
toggleAllAccounts,
} = props;
const setTypeDocspaceAdmin = () => {
changeGroupType(UserTypes.DocSpaceAdmin);
toggleAllAccounts(false, [], checkedAccountType);
};
const setTypeRoomAdmin = () => {
changeGroupType(UserTypes.RoomAdmin);
toggleAllAccounts(false, [], checkedAccountType);
};
const setTypeUser = () => {
changeGroupType(UserTypes.User);
toggleAllAccounts(false, [], checkedAccountType);
};
const typeOptions = [
{
@ -80,12 +98,13 @@ const AccountsTable = (props) => {
};
export default inject(({ setup, importAccountsStore }) => {
const { viewAs } = setup;
const { changeGroupType, UserTypes } = importAccountsStore;
const { changeGroupType, UserTypes, toggleAllAccounts } = importAccountsStore;
return {
viewAs,
changeGroupType,
UserTypes,
toggleAllAccounts,
};
})(
withTranslation(["ChangeUserTypeDialog", "People"])(observer(AccountsTable)),

View File

@ -85,15 +85,17 @@ const SelectUsersTypeStep = (props) => {
displaySettings
/>
<SearchInput
id="search-checkedUsers-type-input"
className="importUsersSearch"
placeholder={t("Common:Search")}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
{!checkedUsers.result.length > 0 && (
<SearchInput
id="search-checkedUsers-type-input"
className="importUsersSearch"
placeholder={t("Common:Search")}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
)}
<AccountsTable t={t} accountsData={filteredAccounts} />

View File

@ -36,7 +36,7 @@ import { HelpButton } from "@docspace/shared/components/help-button";
import { toastr } from "@docspace/shared/components/toast";
const Wrapper = styled.div`
margin: 0 0 16px;
margin: 16px 0 16px;
display: flex;
align-items: center;
@ -47,7 +47,7 @@ const Wrapper = styled.div`
const InfoText = styled(Text)`
margin-top: -8px;
margin-bottom: 16px;
margin-bottom: 8px;
font-size: 12px;
color: ${(props) => props.theme.client.settings.migration.subtitleColor};
`;
@ -55,7 +55,7 @@ const InfoText = styled(Text)`
const ErrorText = styled(Text)`
font-size: 12px;
color: ${(props) => props.theme.client.settings.migration.errorTextColor};
margin-bottom: 16px;
margin-bottom: 8px;
`;
const ImportCompleteStep = ({
@ -70,9 +70,12 @@ const ImportCompleteStep = ({
const [importResult, setImportResult] = useState({
succeedUsers: 0,
failedUsers: 0,
errors: [],
});
const navigate = useNavigate();
const [isSaving, setIsSaving] = useState(false);
const onDownloadLog = async () => {
try {
await getMigrationLog()
@ -101,7 +104,11 @@ const ImportCompleteStep = ({
}
clearMigration();
clearCheckedAccounts();
setTimeout(() => navigate(-1), 1000);
setIsSaving(true);
setTimeout(() => {
setIsSaving(false);
navigate(-1);
}, 1000);
};
useEffect(() => {
@ -110,6 +117,7 @@ const ImportCompleteStep = ({
setImportResult({
succeedUsers: res.parseResult.successedUsers,
failedUsers: res.parseResult.failedUsers,
errors: res.parseResult.errors,
}),
);
} catch (error) {
@ -134,6 +142,10 @@ const ImportCompleteStep = ({
</ErrorText>
)}
{importResult.errors.length > 0 && (
<ErrorText>{t("Settings:ErrorOccuredDownloadLog")}</ErrorText>
)}
<Wrapper>
<Checkbox
label={t("Settings:SendWelcomeLetter")}
@ -158,6 +170,7 @@ const ImportCompleteStep = ({
cancelButtonLabel={t("Settings:DownloadLog")}
displaySettings
showReminder
isSaving={isSaving}
/>
</>
);

View File

@ -139,7 +139,7 @@ const SelectFileStep = ({
setIsFileError(false);
setShowReminder(true);
if (res.parseResult.files.length > 0) {
if (res.parseResult.files?.length > 0) {
setFileName(res.parseResult.files.join(", "));
}

View File

@ -47,19 +47,21 @@ const StyledRowContainer = styled(RowContainer)`
.table-group-menu {
height: 61px;
position: absolute;
position: sticky;
z-index: 201;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
right: -16px;
margin-right: -16px;
`
: css`
left: -16px;
margin-left: -16px;
`}
width: 100%;
margin-top: -35.5px;
margin-top: 20px;
top: 61px;
margin-bottom: -29.5px;
.table-container_group-menu {
padding: 0px 16px;

View File

@ -48,12 +48,14 @@ const StyledTableContainer = styled(TableContainer)`
.table-group-menu {
height: 69px;
position: absolute;
position: sticky;
z-index: 201;
left: 0px;
width: 100%;
width: calc(100% + 40px);
margin-top: 20px;
margin-left: -20px;
top: 0;
margin-top: -35.5px;
margin-bottom: -37.5px;
.table-container_group-menu {
border-image-slice: 0;

View File

@ -31,28 +31,40 @@ import { Consumer } from "@docspace/shared/utils/context";
import TableView from "./TableView";
import RowView from "./RowView";
const checkedAccountType = "result";
const AccountsTable = ({
t,
viewAs,
accountsData,
changeGroupType,
UserTypes,
toggleAllAccounts,
}) => {
const typeOptions = [
{
key: UserTypes.DocSpaceAdmin,
label: t("Common:DocSpaceAdmin"),
onClick: () => changeGroupType(UserTypes.DocSpaceAdmin),
onClick: () => {
changeGroupType(UserTypes.DocSpaceAdmin);
toggleAllAccounts(false, [], checkedAccountType);
},
},
{
key: UserTypes.RoomAdmin,
label: t("Common:RoomAdmin"),
onClick: () => changeGroupType(UserTypes.RoomAdmin),
onClick: () => {
changeGroupType(UserTypes.RoomAdmin);
toggleAllAccounts(false, [], checkedAccountType);
},
},
{
key: UserTypes.User,
label: t("Common:PowerUser"),
onClick: () => changeGroupType(UserTypes.User),
onClick: () => {
changeGroupType(UserTypes.User);
toggleAllAccounts(false, [], checkedAccountType);
},
},
];
@ -80,12 +92,13 @@ const AccountsTable = ({
};
export default inject(({ setup, importAccountsStore }) => {
const { viewAs } = setup;
const { changeGroupType, UserTypes } = importAccountsStore;
const { changeGroupType, UserTypes, toggleAllAccounts } = importAccountsStore;
return {
viewAs,
changeGroupType,
UserTypes,
toggleAllAccounts,
};
})(
withTranslation(["ChangeUserTypeDialog", "People"])(observer(AccountsTable)),

View File

@ -81,16 +81,17 @@ const SelectUsersTypeStep = ({
cancelButtonLabel={t("Common:Back")}
displaySettings
/>
<SearchInput
id="search-users-type-input"
placeholder={t("Common:Search")}
style={{ marginTop: "20px" }}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
{!checkedUsers.result.length > 0 && (
<SearchInput
id="search-users-type-input"
placeholder={t("Common:Search")}
style={{ marginTop: "20px" }}
value={searchValue}
onChange={onChangeInput}
refreshTimeout={100}
onClearSearch={onClearSearchInput}
/>
)}
<AccountsTable t={t} accountsData={filteredAccounts} />

View File

@ -86,10 +86,9 @@ const DataImport = ({
if (
migrationStatus &&
migrationStatus.parseResult.users.length +
migrationStatus.parseResult.existUsers.length +
migrationStatus.parseResult.withoutEmailUsers.length >
0
migrationStatus.parseResult?.failedArchives &&
migrationStatus.parseResult.failedArchives.length === 0 &&
!migrationStatus.error
) {
const workspacesEnum = {
GoogleWorkspace: "google",

View File

@ -29,11 +29,7 @@ import RefreshReactSvgUrl from "PUBLIC_DIR/images/refresh.react.svg?url";
import AccessNoneReactSvgUrl from "PUBLIC_DIR/images/access.none.react.svg?url";
import React, { useEffect, useReducer } from "react";
import { Button } from "@docspace/shared/components/button";
import {
getSettingsThirdParty,
getThirdPartyCapabilities,
saveSettingsThirdParty,
} from "@docspace/shared/api/files";
import { saveSettingsThirdParty } from "@docspace/shared/api/files";
import { StyledBackup } from "../StyledBackup";
import { ComboBox } from "@docspace/shared/components/combobox";
import { toastr } from "@docspace/shared/components/toast";
@ -43,8 +39,6 @@ import DeleteThirdPartyDialog from "../../../../../../components/dialogs/DeleteT
import { getOAuthToken } from "@docspace/shared/utils/common";
import FilesSelectorInput from "SRC_DIR/components/FilesSelectorInput";
import { useTranslation } from "react-i18next";
let accounts = [],
capabilities;
const initialState = {
folderList: {},
@ -68,130 +62,46 @@ const DirectThirdPartyConnection = (props) => {
setSelectedThirdPartyAccount,
connectedThirdPartyAccount,
selectedThirdPartyAccount,
setConnectedThirdPartyAccount,
buttonSize,
isTheSameThirdPartyAccount,
onSelectFile,
filterParam,
descriptionText,
isMobileScale,
accounts,
setThirdPartyAccountsInfo,
} = props;
const { t } = useTranslation("Translations");
useEffect(() => {
onSetInitialInfo();
return () => {
setSelectedThirdPartyAccount(null);
};
}, []);
const onSetInitialInfo = async () => {
try {
const capabilities = await getThirdPartyCapabilities();
onSetThirdPartySettings(connectedThirdPartyAccount, capabilities);
} catch (e) {
onSetThirdPartySettings();
if (!e) return;
toastr.error(e);
}
};
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }),
initialState,
);
const isDirectConnection = () => {
return state.isUpdatingInfo;
};
const updateAccountsInfo = async () => {
const { t } = useTranslation("Translations");
const onSetSettings = async () => {
try {
if (!isDirectConnection()) setState({ isUpdatingInfo: true });
onSelectFolder && onSelectFolder("");
let account;
[account, capabilities] = await Promise.all([
getSettingsThirdParty(),
getThirdPartyCapabilities(),
]);
setConnectedThirdPartyAccount(account);
onSetThirdPartySettings(account, capabilities);
} catch (e) {
onSetThirdPartySettings();
if (!e) return;
toastr.error(e);
}
};
const onSetThirdPartySettings = async (connectedAccount, capabilities) => {
try {
accounts = [];
let index = 0,
selectedAccount = {};
const setAccount = (providerKey, serviceTitle) => {
const accountIndex =
capabilities && capabilities.findIndex((x) => x[0] === providerKey);
if (accountIndex === -1) return;
const isConnected =
connectedAccount?.providerKey === "WebDav"
? serviceTitle === connectedAccount?.title
: capabilities[accountIndex][0] === connectedAccount?.providerKey;
accounts.push({
key: index.toString(),
label: serviceTitle,
title: serviceTitle,
provider_key: capabilities[accountIndex][0],
...(capabilities[accountIndex][1] && {
provider_link: capabilities[accountIndex][1],
}),
connected: isConnected,
...(isConnected && {
provider_id: connectedAccount?.providerId,
id: connectedAccount.id,
}),
});
if (isConnected) {
selectedAccount = { ...accounts[index] };
}
index++;
};
setAccount("GoogleDrive", t("Translations:TypeTitleGoogle"));
setAccount("Box", t("Translations:TypeTitleBoxNet"));
setAccount("DropboxV2", t("Translations:TypeTitleDropBox"));
setAccount("SharePoint", t("Translations:TypeTitleSharePoint"));
setAccount("OneDrive", t("Translations:TypeTitleSkyDrive"));
setAccount("WebDav", "Nextcloud");
setAccount("WebDav", "ownCloud");
setAccount("kDrive", t("Translations:TypeTitlekDrive"));
setAccount("Yandex", t("Translations:TypeTitleYandex"));
setAccount("WebDav", t("Translations:TypeTitleWebDav"));
setSelectedThirdPartyAccount(
Object.keys(selectedAccount).length !== 0
? selectedAccount
: { ...accounts[0] },
);
await setThirdPartyAccountsInfo();
setState({
isLoading: false,
isUpdatingInfo: false,
isInitialLoading: false,
folderList: connectedAccount ?? {},
});
} catch (e) {
setState({
isLoading: false,
isInitialLoading: false,
isUpdatingInfo: false,
});
if (!e) return;
toastr.error(e);
}
};
useEffect(() => {
onSetSettings();
return () => {
setSelectedThirdPartyAccount(null);
};
}, []);
const onConnect = () => {
clearLocalStorage();
onSelectFolder && onSelectFolder("");
@ -245,11 +155,12 @@ const DirectThirdPartyConnection = (props) => {
provider_id,
);
updateAccountsInfo();
await setThirdPartyAccountsInfo();
} catch (e) {
setState({ isLoading: false, isUpdatingInfo: false });
toastr.error(e);
}
setState({ isLoading: false, isUpdatingInfo: false });
};
const onSelectAccount = (options) => {
@ -280,13 +191,14 @@ const DirectThirdPartyConnection = (props) => {
];
};
const { isLoading, folderList, isInitialLoading } = state;
const { isLoading, isInitialLoading } = state;
const isDisabledComponent =
isDisabled || isInitialLoading || isLoading || accounts.length === 0;
const isDisabledSelector = isLoading || isDisabled;
const folderList = connectedThirdPartyAccount ?? {};
return (
<StyledBackup
isConnectedAccount={
@ -310,17 +222,19 @@ const DirectThirdPartyConnection = (props) => {
isDisabled={isDisabledComponent}
/>
{connectedThirdPartyAccount?.id && isTheSameThirdPartyAccount && (
<ContextMenuButton
zIndex={402}
className="backup_third-party-context"
iconName={VerticalDotsReactSvgUrl}
size={15}
getData={getContextOptions}
isDisabled={isDisabledComponent}
displayIconBorder
/>
)}
{connectedThirdPartyAccount?.id &&
selectedThirdPartyAccount &&
isTheSameThirdPartyAccount && (
<ContextMenuButton
zIndex={402}
className="backup_third-party-context"
iconName={VerticalDotsReactSvgUrl}
size={15}
getData={getContextOptions}
isDisabled={isDisabledComponent}
displayIconBorder
/>
)}
</div>
{!connectedThirdPartyAccount?.id || !isTheSameThirdPartyAccount ? (
@ -330,10 +244,11 @@ const DirectThirdPartyConnection = (props) => {
label={t("Common:Connect")}
onClick={onConnect}
size={buttonSize}
isDisabled={isDisabledComponent}
/>
) : (
<>
{folderList.id && (
{folderList.id && selectedThirdPartyAccount && (
<FilesSelectorInput
className={"restore-backup_input"}
descriptionText={descriptionText}
@ -352,7 +267,7 @@ const DirectThirdPartyConnection = (props) => {
)}
{deleteThirdPartyDialogVisible && (
<DeleteThirdPartyDialog
updateInfo={updateAccountsInfo}
updateInfo={setThirdPartyAccountsInfo}
key="thirdparty-delete-dialog"
isConnectionViaBackupModule
/>
@ -367,8 +282,10 @@ export default inject(({ backup, dialogsStore, filesSettingsStore }) => {
setSelectedThirdPartyAccount,
selectedThirdPartyAccount,
connectedThirdPartyAccount,
setConnectedThirdPartyAccount,
isTheSameThirdPartyAccount,
accounts,
setThirdPartyAccountsInfo,
} = backup;
const { openConnectWindow } = filesSettingsStore.thirdPartyStore;
@ -390,6 +307,8 @@ export default inject(({ backup, dialogsStore, filesSettingsStore }) => {
setSelectedThirdPartyAccount,
selectedThirdPartyAccount,
connectedThirdPartyAccount,
setConnectedThirdPartyAccount,
accounts,
setThirdPartyAccountsInfo,
};
})(observer(DirectThirdPartyConnection));

View File

@ -204,7 +204,7 @@ export const TableDataCell = styled.td`
}
:last-child {
text-align: center;
text-align: end;
}
.remove-icon {
svg {

View File

@ -35,8 +35,12 @@ import { toastr } from "@docspace/shared/components/toast";
import { AutoBackupPeriod } from "@docspace/shared/enums";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import config from "PACKAGE_FILE";
import { uploadBackup } from "@docspace/shared/api/files";
import {
getSettingsThirdParty,
getThirdPartyCapabilities,
uploadBackup,
} from "@docspace/shared/api/files";
import i18n from "../i18n";
const { EveryDayType, EveryWeekType } = AutoBackupPeriod;
class BackupStore {
@ -95,6 +99,9 @@ class BackupStore {
storageRegions = [];
selectedThirdPartyAccount = null;
connectedThirdPartyAccount = null;
accounts = [];
capabilities = [];
connectedAccount = [];
constructor() {
makeAutoObservable(this);
@ -188,14 +195,101 @@ class BackupStore {
return false;
}
setThirdPartyAccountsInfo = async () => {
const [connectedAccount, capabilities] = await Promise.all([
getSettingsThirdParty(),
getThirdPartyCapabilities(),
]);
this.setCapabilities(capabilities);
this.setConnectedThirdPartyAccount(connectedAccount);
const providerNames = [
["GoogleDrive", i18n.t("Translations:TypeTitleGoogle")],
["Box", i18n.t("Translations:TypeTitleBoxNet")],
["DropboxV2", i18n.t("Translations:TypeTitleDropBox")],
["SharePoint", i18n.t("Translations:TypeTitleSharePoint")],
["OneDrive", i18n.t("Translations:TypeTitleSkyDrive")],
["WebDav", "Nextcloud"],
["WebDav", "ownCloud"],
["kDrive", i18n.t("Translations:TypeTitlekDrive")],
["Yandex", i18n.t("Translations:TypeTitleYandex")],
["WebDav", i18n.t("Translations:TypeTitleWebDav")],
];
let accounts = [],
selectedAccount = {};
let index = 0;
providerNames.map((item) => {
const { account, isConnected } = this.getThirdPartyAccount(
item[0],
item[1],
index,
);
if (!account) return;
accounts.push(account);
if (isConnected) {
selectedAccount = { ...accounts[index] };
}
index++;
});
this.setThirdPartyAccounts(accounts);
console.log(selectedAccount, accounts);
this.setSelectedThirdPartyAccount(
Object.keys(selectedAccount).length !== 0
? selectedAccount
: { ...accounts[0] },
);
};
getThirdPartyAccount = (providerKey, serviceTitle, index) => {
const accountIndex =
this.capabilities &&
this.capabilities.findIndex((x) => x[0] === providerKey);
if (accountIndex === -1) return { account: null, isConnected: false };
const isConnected =
this.connectedThirdPartyAccount?.providerKey === "WebDav"
? serviceTitle === this.connectedThirdPartyAccount?.title
: this.capabilities[accountIndex][0] ===
this.connectedThirdPartyAccount?.providerKey;
const account = {
key: index.toString(),
label: serviceTitle,
title: serviceTitle,
provider_key: this.capabilities[accountIndex][0],
...(this.capabilities[accountIndex][1] && {
provider_link: this.capabilities[accountIndex][1],
}),
connected: isConnected,
...(isConnected && {
provider_id: this.connectedThirdPartyAccount?.providerId,
id: this.connectedThirdPartyAccount.id,
}),
};
return { account, isConnected };
};
setCapabilities = (capabilities) => {
this.capabilities = capabilities;
};
setThirdPartyAccounts = (accounts) => {
this.accounts = accounts;
};
setSelectedThirdPartyAccount = (elem) => {
this.selectedThirdPartyAccount = elem;
};
get selectedThirdPartyAccount() {
return this.selectedThirdPartyAccount;
}
toDefault = () => {
this.selectedMonthlySchedule = this.defaultMonthlySchedule;
this.selectedWeeklySchedule = this.defaultWeeklySchedule;

View File

@ -76,6 +76,7 @@ import { ShareAccessRights, RoomsType } from "@docspace/shared/enums";
import { combineUrl } from "@docspace/shared/utils/combineUrl";
import { isDesktop } from "@docspace/shared/utils";
import { Events } from "@docspace/shared/enums";
import { copyShareLink } from "@docspace/shared/utils/copy";
import { connectedCloudsTypeTitleTranslation } from "@docspace/client/src/helpers/filesUtils";
import { getOAuthToken } from "@docspace/shared/utils/common";
@ -367,7 +368,7 @@ class ContextOptionsStore {
if (isShared && !item.isFolder && !isArchive) {
const fileLinkData = await getFileLink(item.id);
copy(fileLinkData.sharedTo.shareLink);
copyShareLink(fileLinkData.sharedTo.shareLink);
return toastr.success(t("Translations:LinkCopySuccess"));
}
@ -407,7 +408,7 @@ class ContextOptionsStore {
const primaryLink = await this.filesStore.getPrimaryLink(item.id);
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
item.shared
? toastr.success(t("Common:LinkSuccessfullyCopied"))
: toastr.success(t("Files:LinkSuccessfullyCreatedAndCopied"));
@ -1311,7 +1312,7 @@ class ContextOptionsStore {
const primaryLink = await getPrimaryFileLink(item.id);
if (primaryLink) {
copy(primaryLink.sharedTo.shareLink);
copyShareLink(primaryLink.sharedTo.shareLink);
item.shared
? toastr.success(t("Files:LinkSuccessfullyCopied"))
: toastr.success(t("Files:LinkSuccessfullyCreatedAndCopied"));
@ -1556,17 +1557,7 @@ class ContextOptionsStore {
const pluginItems = this.onLoadPlugins(item);
if (pluginItems.length > 0) {
if (isDesktop()) {
options.splice(1, 0, {
id: "option_plugin-actions",
key: "plugin_actions",
label: t("Common:Actions"),
icon: PluginActionsSvgUrl,
disabled: false,
onLoad: () => this.onLoadPlugins(item),
});
} else {
if (!isDesktop() || pluginItems.length === 1) {
pluginItems.forEach((plugin) => {
options.splice(1, 0, {
id: `option_${plugin.key}`,
@ -1577,6 +1568,16 @@ class ContextOptionsStore {
onClick: plugin.onClick,
});
});
} else {
options.splice(1, 0, {
id: "option_plugin-actions",
key: "plugin_actions",
label: t("Common:Actions"),
icon: PluginActionsSvgUrl,
disabled: false,
onLoad: () => this.onLoadPlugins(item),
});
}
}

View File

@ -42,6 +42,7 @@ class DialogStore {
dataReassignmentDeleteProfile = false;
isDeletingUserWithReassignment = false;
changeEmailVisible = false;
deleteGroupDialogVisible = false;
constructor() {
makeAutoObservable(this);
@ -95,6 +96,10 @@ class DialogStore {
this.changeEmailVisible = visible;
};
setDeleteGroupDialogVisible = (visible) => {
this.deleteGroupDialogVisible = visible;
};
closeDialogs = () => {
this.setChangeOwnerDialogVisible(false);
this.setDeleteSelfProfileDialogVisible(false);
@ -108,6 +113,8 @@ class DialogStore {
this.setResetAuthDialogVisible(false);
this.setChangeEmailVisible(false);
this.setDeleteGroupDialogVisible(false);
};
}

View File

@ -55,10 +55,14 @@ class GroupsStore {
bufferSelection = null;
groupName = "";
selected = "none";
groupsFilter = GroupsFilter.getDefault();
isLoading = false;
groupsIsIsLoading = false;
insideGroupIsLoading = false;
@ -120,6 +124,14 @@ class GroupsStore {
window.DocSpace.navigate(`accounts/groups/filter?${filter.toUrlParams()}`);
};
setGroupName = (name: string) => {
this.groupName = name;
};
setIsLoading = (isLoading: boolean) => {
this.isLoading = isLoading;
};
get groupsFilterTotal() {
return this.groupsFilter.total;
}
@ -396,6 +408,56 @@ class GroupsStore {
this.setSelection(newSelections);
};
onDeleteClick = (name: string) => {
this.setGroupName(name);
this.peopleStore.dialogStore.setDeleteGroupDialogVisible(true);
};
onDeleteGroup = async (t, groupId) => {
this.setIsLoading(true);
if (!groupId) {
this.setIsLoading(false);
return;
}
try {
await groupsApi.deleteGroup(groupId);
toastr.success(t("PeopleTranslations:SuccessDeleteGroup"));
this.setSelection([]);
this.getGroups(this.groupsFilter, true);
this.infoPanelStore.setInfoPanelSelection(null);
this.setIsLoading(false);
this.peopleStore.dialogStore.setDeleteGroupDialogVisible(false);
} catch (err) {
toastr.error(err.message);
console.error(err);
this.setIsLoading(false);
this.peopleStore.dialogStore.setDeleteGroupDialogVisible(false);
}
};
onDeleteAllGroups = (t) => {
this.setIsLoading(true);
try {
Promise.all(
this.selection.map(async (group) => groupsApi.deleteGroup(group.id)),
).then(() => {
toastr.success(t("PeopleTranslations:SuccessDeleteGroups"));
this.setSelection([]);
this.getGroups(this.groupsFilter, true);
this.setIsLoading(false);
this.peopleStore.dialogStore.setDeleteGroupDialogVisible(false);
});
} catch (err) {
toastr.error(err.message);
console.error(err);
this.setIsLoading(false);
this.peopleStore.dialogStore.setDeleteGroupDialogVisible(false);
}
};
getGroupContextOptions = (
t,
item,
@ -444,21 +506,24 @@ class GroupsStore {
label: t("Common:Delete"),
title: t("Common:Delete"),
icon: TrashReactSvgUrl,
onClick: async () => {
const groupId = item.id;
groupsApi
.deleteGroup(groupId)!
.then(() => {
toastr.success(t("PeopleTranslations:SuccessDeleteGroup"));
this.setSelection([]);
this.getGroups(this.groupsFilter, true);
this.infoPanelStore.setInfoPanelSelection(null);
})
.catch((err) => {
toastr.error(err.message);
console.error(err);
});
},
onClick: () => this.onDeleteClick(item.name),
//
// onClick: async () => {
// const groupId = item.id;
// groupsApi
// .deleteGroup(groupId)!
// .then(() => {
// toastr.success(t("PeopleTranslations:SuccessDeleteGroup"));
// this.setSelection([]);
// this.getGroups(this.groupsFilter, true);
// this.infoPanelStore.setInfoPanelSelection(null);
// })
// .catch((err) => {
// toastr.error(err.message);
// console.error(err);
// });
// },
},
];
};

View File

@ -381,6 +381,14 @@ class PeopleStore {
return options;
}
};
onDeleteClick = () => {
const { setDeleteGroupDialogVisible } = this.dialogStore;
const { selection, setGroupName } = this.groupsStore;
setGroupName(selection[0].name);
setDeleteGroupDialogVisible(true);
};
getHeaderMenu = (t, isGroupsPage = false) => {
const {
hasUsersToMakeEmployees,
@ -394,8 +402,6 @@ class PeopleStore {
selection,
} = this.selectionStore;
const { selection: groupsSelection, groupsFilter } = this.groupsStore;
const { setSendInviteDialogVisible } = this.dialogStore;
const { toggleDeleteProfileEverDialog } = this.contextOptionsStore;
@ -407,20 +413,7 @@ class PeopleStore {
id: "menu-delete",
key: "delete",
label: t("Common:Delete"),
onClick: () => {
Promise.all(
groupsSelection.map(async (group) => deleteGroup(group.id)),
)
.then(() => {
toastr.success(t("PeopleTranslations:SuccessDeleteGroups"));
this.groupsStore.setSelection([]);
this.groupsStore.getGroups(groupsFilter, true);
})
.catch((err) => {
toastr.error(err.message);
console.error(err);
});
},
onClick: () => this.onDeleteClick(),
iconUrl: DeleteReactSvgUrl,
},
];

View File

@ -396,23 +396,6 @@ class TableStore {
if (isFrame) return `${TABLE_SDK_COLUMNS}=${userId}`;
console.log(
"Table log tableStorageName",
isRooms
? `${TABLE_ROOMS_COLUMNS}=${userId}`
: isAccountsPeople
? `${TABLE_ACCOUNTS_PEOPLE_COLUMNS}=${userId}`
: isAccountsGroups
? `${TABLE_ACCOUNTS_GROUPS_COLUMNS}=${userId}`
: isAccountsInsideGroup
? `${TABLE_ACCOUNTS_INSIDE_GROUP_COLUMNS}=${userId}`
: isTrashFolder
? `${TABLE_TRASH_COLUMNS}=${userId}`
: isRecentTab
? `${TABLE_RECENT_COLUMNS}=${userId}`
: `${TABLE_COLUMNS}=${userId}`,
);
return isRooms
? `${TABLE_ROOMS_COLUMNS}=${userId}`
: isAccountsPeople
@ -437,17 +420,6 @@ class TableStore {
if (isFrame) return `${COLUMNS_SDK_SIZE}=${userId}`;
console.log(
"Table log columnStorageName",
isRooms
? `${COLUMNS_ROOMS_SIZE}=${userId}`
: isTrashFolder
? `${COLUMNS_TRASH_SIZE}=${userId}`
: isRecentTab
? `${COLUMNS_RECENT_SIZE}=${userId}`
: `${COLUMNS_SIZE}=${userId}`,
);
return isRooms
? `${COLUMNS_ROOMS_SIZE}=${userId}`
: isTrashFolder

View File

@ -86,6 +86,7 @@ export const AccessRightSelectPure = ({
backgroundColor={item.color}
fontSize="9px"
isPaidBadge
noHover
/>
)}
</StyledItemTitle>

View File

@ -259,7 +259,7 @@ const StyledArticleItemImg = styled.div<{ isActive?: boolean }>`
height: ${(props) => props.theme.catalogItem.img.svg.height};
.icon {
.icon > div {
display: flex;
align-items: center;
justify-content: center;

View File

@ -124,19 +124,19 @@ const RoleWrapper = styled.div<{
props.theme.avatar.roleWrapperContainer.height.max) ||
(props.size === AvatarSize.medium &&
props.theme.avatar.roleWrapperContainer.height.medium) ||
"16px"};
"12px"};
width: ${(props) =>
(props.size === AvatarSize.max &&
props.theme.avatar.roleWrapperContainer.width.max) ||
(props.size === AvatarSize.medium &&
props.theme.avatar.roleWrapperContainer.width.medium) ||
"16px"};
"12px"};
min-width: ${(props) =>
(props.size === AvatarSize.max &&
props.theme.avatar.roleWrapperContainer.width.max) ||
(props.size === AvatarSize.medium &&
props.theme.avatar.roleWrapperContainer.width.medium) ||
"16px"};
"12px"};
`;
RoleWrapper.defaultProps = { theme: Base };

View File

@ -205,6 +205,7 @@ const AvatarPure = ({
id={uniqueTooltipId}
getContent={getTooltipContent}
place={tooltipPlace}
opacity={1}
/>
)}
</>

View File

@ -34,21 +34,23 @@ const BannerWrapper = styled.div<{
}>`
overflow: hidden;
position: relative;
min-height: 140px;
max-height: 140px;
border-radius: 4px;
border: 1px solid ${(props) => props.borderColor};
min-height: 142px;
max-height: 142px;
&::before {
content: "";
background-image: url(${(props) => props.background});
background-size: 100%;
background-repeat: no-repeat;
background-position: 0% 100%;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: -1000;
inset: 0px;
border-radius: 4px;
border: 1px solid ${(props) => props.borderColor};
${(props) =>
props.theme.interfaceDirection === "rtl" &&
css`
@ -73,12 +75,6 @@ const BannerWrapper = styled.div<{
fill: "#A3A9AE";
}
}
@media ${mobile} {
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
}
`;
BannerWrapper.defaultProps = { theme: Base };

View File

@ -73,6 +73,7 @@ const CampaignsBanner = (props: CampaignsBannerProps) => {
color={title?.color || "#333"}
fontSize={title?.fontSize}
fontWeight={title?.fontWeight}
lineHeight="12px"
>
{Header}
</TextComponent>
@ -111,6 +112,7 @@ const CampaignsBanner = (props: CampaignsBannerProps) => {
fontSize={action?.fontSize}
fontWeight={action?.fontWeight}
onClick={() => onAction(action?.type, Link)}
isHovered
>
{ButtonLabel}
</LinkComponent>

View File

@ -451,6 +451,7 @@ const ContextMenu = React.forwardRef((props: ContextMenuProps, ref) => {
imgClassName="drop-down-item_icon"
imgSrc={header.icon}
badgeUrl={badgeUrl}
color={header.color || ""}
/>
) : (
<RoomIcon

View File

@ -32,7 +32,6 @@ import { useTheme } from "styled-components";
import ArrowIcon from "PUBLIC_DIR/images/arrow.right.react.svg";
import OutsdideIcon from "PUBLIC_DIR/images/arrow.outside.react.svg";
import CheckEditIcon from "PUBLIC_DIR/images/check.edit.react.svg";
import {
classNames,
@ -294,7 +293,6 @@ const SubMenu = (props: {
{icon}
{label}
{subMenuIcon}
{item.checked && <CheckEditIcon className={subMenuIconClassName} />}
{item.isOutsideLink && (
<OutsdideIcon className={subMenuIconClassName} />
)}

View File

@ -35,6 +35,7 @@ import {
StyledIconBlock,
} from "./InputBlock.styled";
import { InputBlockProps } from "./InputBlock.types";
import { isTablet, isIOS } from "react-device-detect";
const InputBlock = ({
onIconClick,
@ -59,7 +60,7 @@ const InputBlock = ({
tabIndex = -1,
maxLength = 255,
onBlur,
onFocus,
onFocus: onFocusAction,
isAutoFocussed,
autoComplete = "off",
onKeyDown,
@ -112,6 +113,19 @@ const InputBlock = ({
return iconButtonSize;
};
const onFocus = (focusEvent: React.FocusEvent<HTMLInputElement>) => {
const scrollEvent = (e: Event) => {
e.preventDefault();
e.stopPropagation();
window.scrollTo(0, 0);
window.onscroll = () => {};
};
window.onscroll = scrollEvent;
if (onFocusAction) return onFocusAction(focusEvent);
};
const iconButtonSize = getIconSize();
return (
@ -146,7 +160,7 @@ const InputBlock = ({
tabIndex={tabIndex}
maxLength={maxLength}
onBlur={onBlur}
onFocus={onFocus}
onFocus={isTablet && isIOS ? onFocus : onFocusAction}
isReadOnly={isReadOnly}
isAutoFocussed={isAutoFocussed}
autoComplete={autoComplete}

View File

@ -89,7 +89,6 @@ export const ViewerPlayer = ({
isFullScreen,
panelVisible,
thumbnailSrc,
isThirdParty,
mobileDetails,
isPreviewFile,
isOpenContextMenu,
@ -624,8 +623,7 @@ export const ViewerPlayer = ({
};
}, [onKeyDown]);
const posterUrl =
thumbnailSrc && !isThirdParty ? `${thumbnailSrc}&size=1280x720` : undefined;
const posterUrl = thumbnailSrc ? `${thumbnailSrc}&size=1280x720` : undefined;
return (
<>

View File

@ -130,7 +130,7 @@ type RoomIconColor = {
};
type RoomIconImage = {
color?: undefined;
color?: string | undefined;
imgSrc: string;
imgClassName?: string;
};

View File

@ -477,10 +477,7 @@ const tabletProps = css<{ viewAs?: TViewAs }>`
width: 100%;
position: sticky;
top: 0;
background: ${(props) =>
props.viewAs === "profile" || props.viewAs === "settings"
? props.theme.section.header.backgroundColor
: props.theme.section.header.background};
background: ${(props) => props.theme.section.header.backgroundColor};
${(props) =>
props.theme.interfaceDirection === "rtl"

View File

@ -155,7 +155,7 @@ const StyledSelectAll = styled.div`
.checkbox {
svg {
margin-right: 0px;
margin-inline-end: 0px;
}
}
`;
@ -206,7 +206,7 @@ const StyledItem = styled.div<{
.checkbox {
svg {
margin-right: 0px;
margin-inline-end: 0px;
}
}

View File

@ -26,7 +26,6 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import copy from "copy-to-clipboard";
import moment from "moment";
import InfoIcon from "PUBLIC_DIR/images/info.outline.react.svg?url";
@ -40,6 +39,7 @@ import {
getPrimaryLink,
} from "../../api/files";
import { TAvailableExternalRights, TFileLink } from "../../api/files/types";
import { copyShareLink } from "../../utils/copy";
import { TOption } from "../combobox";
import { Text } from "../text";
import { IconButton } from "../icon-button";
@ -114,7 +114,7 @@ const Share = (props: ShareProps) => {
: await getPrimaryLink(infoPanelSelection.id);
setFileLinks([link]);
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Common:GeneralAccessLinkCopied"));
} catch (error) {
const message = (error as { message: string }).message
@ -204,7 +204,7 @@ const Share = (props: ShareProps) => {
);
updateLink(link, res);
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
} catch (e) {
toastr.error(e as TData);
@ -247,7 +247,7 @@ const Share = (props: ShareProps) => {
if (item.access === ShareAccessRights.DenyAccess) {
toastr.success(t("Common:LinkAccessDenied"));
} else {
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
}
}
@ -285,7 +285,7 @@ const Share = (props: ShareProps) => {
updateLink(link, res);
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
} catch (e) {
toastr.error(e as TData);

View File

@ -25,7 +25,6 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useTranslation } from "react-i18next";
import copy from "copy-to-clipboard";
import PlusIcon from "PUBLIC_DIR/images/plus.react.svg?url";
import UniverseIcon from "PUBLIC_DIR/images/universe.react.svg?url";
@ -34,6 +33,7 @@ import CopyIcon from "PUBLIC_DIR/images/copy.react.svg?url";
import { RowSkeleton } from "../../../skeletons/share";
import { TFileLink } from "../../../api/files/types";
import { copyShareLink } from "../../../utils/copy";
import { Avatar, AvatarRole, AvatarSize } from "../../avatar";
import { Link, LinkType } from "../../link";
import { ComboBox, ComboBoxSize, TOption } from "../../combobox";
@ -62,7 +62,7 @@ const LinkRow = ({
const accessOptions = getAccessOptions(t, availableExternalRights);
const onCopyLink = (link: TFileLink) => {
copy(link.sharedTo.shareLink);
copyShareLink(link.sharedTo.shareLink);
toastr.success(t("Common:LinkSuccessfullyCopied"));
};

View File

@ -46,8 +46,6 @@ const TableBody = (props: TableBodyProps) => {
infoPanelVisible = false,
} = props;
console.log("Table log TableBody columnStorageName", columnStorageName);
return useReactWindow ? (
<StyledTableBody
useReactWindow={useReactWindow}

View File

@ -45,7 +45,7 @@ const StyledButton = styled(Button)`
background-color: ${(props) => props.theme.button.backgroundColor.base};
.combo-button_selected-icon {
.combo-button_selected-icon > div {
display: flex;
align-items: center;
}

View File

@ -25,7 +25,6 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
import { ReactSVG } from "react-svg";
import { Text } from "../text";
@ -108,43 +107,10 @@ const StyledTag = styled.div<{
StyledTag.defaultProps = { theme: Base };
const StyledDropdownIcon = styled(ReactSVG)`
display: flex;
align-items: center;
${(props) =>
props.theme.interfaceDirection === "rtl" &&
css`
transform: scaleX(-1);
`}
pointer-events: none;
svg {
path:first-child {
stroke: ${(props) => props.theme.tag.color};
}
path:last-child {
fill: ${(props) => props.theme.tag.color};
}
}
`;
const StyledDropdownText = styled(Text)`
line-height: 30px;
${(props) =>
props.theme.interfaceDirection === "rtl"
? css`
margin-right: 8px !important;
`
: css`
margin-left: 8px !important;
`}
display: block;
pointer-events: none;
`;
export { StyledTag, StyledDropdownText, StyledDropdownIcon };
export { StyledTag, StyledDropdownText };

View File

@ -28,18 +28,13 @@ import React from "react";
import { ReactSVG } from "react-svg";
import CrossIconReactSvgUrl from "PUBLIC_DIR/images/cross.react.svg?url";
import TagIconReactSvgUrl from "PUBLIC_DIR/images/tag.react.svg?url";
import { DropDown } from "../drop-down";
import { DropDownItem } from "../drop-down-item";
import { IconButton } from "../icon-button";
import { Text } from "../text";
import {
StyledTag,
StyledDropdownIcon,
StyledDropdownText,
} from "./Tag.styled";
import { StyledTag, StyledDropdownText } from "./Tag.styled";
import { TagProps } from "./Tag.types";
export const TagPure = ({
@ -154,10 +149,6 @@ export const TagPure = ({
onClick={onClickAction}
data-tag={t}
>
<StyledDropdownIcon
className="tag__dropdown-item-icon"
src={TagIconReactSvgUrl}
/>
<StyledDropdownText
className="tag__dropdown-item-text"
fontWeight={600}

View File

@ -1019,7 +1019,6 @@ class SettingsStore {
if (window.DocSpaceConfig) window.DocSpaceConfig.isFrame = isFrame;
console.log("Table log isFrame", isFrame);
return isFrame;
}

View File

@ -1122,16 +1122,16 @@ export const getBaseTheme = () => {
roleWrapperContainer: {
right: {
min: "-5px",
min: "-2px",
small: "-2px",
base: "-2px",
medium: "-4px",
big: "3px",
max: "0px",
max: "10px",
},
bottom: {
min: "-5px",
min: "-2px",
small: "3px",
base: "4px",
medium: "6px",
@ -1140,13 +1140,15 @@ export const getBaseTheme = () => {
},
width: {
min: "12px",
medium: "16px",
max: "24px",
max: "22px",
},
height: {
min: "12px",
medium: "16px",
max: "24px",
max: "22px",
},
},
@ -2098,7 +2100,7 @@ export const getBaseTheme = () => {
links: {
iconColor: "#3B72A7",
iconErrorColor: "rgba(242, 28, 14, 0.5)", // "#F21C0E",
iconErrorColor: "#F24724",
primaryColor: "#555F65",
},

View File

@ -1090,7 +1090,7 @@ const Dark: TTheme = {
roleWrapperContainer: {
right: {
min: "-5px",
min: "-2px",
small: "-2px",
base: "-2px",
medium: "-4px",
@ -1099,7 +1099,7 @@ const Dark: TTheme = {
},
bottom: {
min: "-5px",
min: "-2px",
small: "3px",
base: "4px",
medium: "6px",
@ -1108,11 +1108,13 @@ const Dark: TTheme = {
},
width: {
min: "12px",
medium: "16px",
max: "24px",
},
height: {
min: "12px",
medium: "16px",
max: "24px",
},
@ -2070,7 +2072,7 @@ const Dark: TTheme = {
links: {
iconColor: "#858585",
iconErrorColor: "rgba(242, 28, 14, 0.5)", // "#F21C0E",
iconErrorColor: "#E06451",
primaryColor: "#ADADAD",
},

View File

@ -0,0 +1,37 @@
// (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 copy from "copy-to-clipboard";
const wait = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
export const copyShareLink = async (link: string) => {
await wait(100);
copy(link);
};

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 65 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.95446 14.6591C9.49648 15.1136 8.75709 15.1136 8.30146 14.6591L1.86207 8.6339C1.50204 8.297 1.07291 7.65859 1.04197 7.16717C0.959191 5.83771 1.01755 3.29201 1.05807 1.94233C1.07263 1.45042 1.4872 1.03804 1.9806 1.02673C3.70894 0.986639 7.37082 0.943817 7.7112 1.28372L14.8024 7.6841C15.2577 8.13865 14.8447 9.28836 14.3858 9.74526L9.95446 14.6591Z" stroke="#333333" stroke-width="2"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.70716 3.29277C4.31616 2.90241 3.68392 2.90241 3.29259 3.29277C2.90247 3.68434 2.90247 4.31663 3.29259 4.70698C3.6842 5.09767 4.31644 5.09767 4.70716 4.70698C5.09761 4.31663 5.09761 3.68401 4.70716 3.29277Z" fill="#333333"/>
</svg>

Before

Width:  |  Height:  |  Size: 815 B

View File

@ -153,7 +153,7 @@
"ExpiredLink": "Просроченная ссылка",
"FeedbackAndSupport": "Обратная связь и поддержка",
"FillFormButton": "Заполнить форму",
"Finish": "Заканчивать",
"Finish": "Завершить",
"FirstName": "Имя",
"Free": "Бесплатно",
"FreeProFeatures": "Бесплатный доступ к профессиональным функциональным возможностям",
@ -378,4 +378,4 @@
"Website": "Веб-сайт",
"Yes": "Да",
"Yesterday": "Вчера"
}
}