Merge pull request #534 from ONLYOFFICE/feature/VDR-watermarks

Feature/vdr-watermarks
This commit is contained in:
Alexey Safronov 2024-07-11 11:21:51 +04:00 committed by GitHub
commit 2a30838dc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1584 additions and 230 deletions

View File

@ -1,29 +1,42 @@
{
"ActivationRequired": "activation required",
"AddWatermarkElements": "Add watermark elements",
"AddStaticText": "Add static text",
"Diagonal": "Diagonal",
"ChooseRoomType": "Choose room type",
"CreateRoomConfirmation": "Continue without connecting the storage?\nYou have selected a third-party storage option that is not connected yet. If you proceed without connecting the service, this option will not be added.",
"CreateRoomWatermarksConfirmation": "You have not set a watermark to be applied to documents in this room. You can always add a watermark in the room editing settings. Continue without a watermark?",
"CreateTagOption": "Create tag",
"DisableRoomQuota": "Disable quota for this room",
"FormRoomBarDescription": "This room is available to anyone with the link. External users will have Form Filler permission for all the files.",
"Center": "Center",
"Horizontal": "Horizontal",
"Icon": "Icon",
"MakeRoomPrivateDescription": "All files in this room will be encrypted.",
"MakeRoomPrivateLimitationsWarningDescription": "With this feature, you can invite only existing {{productName}} users. After creating a room, you will not be able to change the list of users.",
"MakeRoomPrivateTitle": "Make the Room Private",
"PeopleSelectorInfo": "Only a room admin or a {{productName}} admin can become the owner of the room",
"PublicRoomBarDescription": "This room is available to anyone with the link. External users will have View Only permission for all the files.",
"ViewerInfo": "Viewer info",
"Position": "Position",
"PublicRoomSystemFoldersDescription": "System folders store copies of forms at different stages of completion. Forms that are being filled are stored in the In progress folder, and completed forms are stored in the Complete folder.",
"PublicRoomSystemFoldersTitle": "System Folders",
"RoomEditing": "Room editing",
"RootFolderLabel": "Root folder",
"StorageDescription": "Storage quota set per room. You can change this value or turn off storage limit.",
"TagsPlaceholder": "Add a tag",
"Text": "Text",
"ThirdPartyStorageComboBoxPlaceholder": "Select storage",
"ThirdPartyStorageDescription": "Use third-party services as data storage for this room. You can create a new folder or select the existing one in the connected storage.",
"ThirdPartyStorageNoStorageAlert": "Before, you need to connect the corresponding service in the “Integration” section. Otherwise, the connection will not be possible.",
"ThirdPartyStoragePermanentSettingDescription": "Files are stored in a third-party {{thirdpartyTitle}} storage in the \"{{thirdpartyFolderName}}\" folder.\n<strong>{{thirdpartyPath}}</strong>",
"ThirdPartyStorageRoomAdminNoStorageAlert": "To connect a third-party storage, you need to add the corresponding service in the Integration section of {{productName}} settings. Contact {{productName}} owner or administrator to enable the integration.",
"UserName": "User Name",
"UserEmail": "User Email",
"UserIPAddress": "User IP Address",
"ViewOnlyRoomDescription": "Share any ready documents, reports, documentation, and other files for viewing.",
"ViewOnlyRoomTitle": "View-only room",
"WatermarkPreview": "Watermark Preview",
"WatermarkPreviewHelp": "This image preview roughly shows how the watermark will be displayed in your files.",
"AutomaticIndexing": "Automatic indexing",
"AutomaticIndexingDescription": "Enable automatic indexing to index files and folders by serial number. Sorting by number will be set as default for all users.",
"FileLifetime": "File lifetime",

View File

@ -51,6 +51,7 @@ const CreateRoomEvent = ({
enableThirdParty,
deleteThirdParty,
startRoomType,
isNotWatermarkSet,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common", "Files"]);
const [fetchedTags, setFetchedTags] = useState([]);
@ -59,14 +60,17 @@ const CreateRoomEvent = ({
setRoomParams(roomParams);
setOnClose(onClose);
if (
const notConnectedThirdparty =
roomParams.storageLocation.isThirdparty &&
!roomParams.storageLocation.storageFolderId
) {
!roomParams.storageLocation.storageFolderId;
if (notConnectedThirdparty || isNotWatermarkSet()) {
setCreateRoomConfirmDialogVisible(true);
} else {
onCreateRoom(false, t);
return;
}
onCreateRoom(false, t);
};
const fetchTagsAction = useCallback(async () => {
@ -134,6 +138,8 @@ export default inject(
setIsLoading,
setOnClose,
confirmDialogIsLoading,
isNotWatermarkSet,
} = createEditRoomStore;
return {
@ -151,6 +157,8 @@ export default inject(
fetchThirdPartyProviders,
enableThirdParty,
deleteThirdParty,
isNotWatermarkSet,
};
},
)(observer(CreateRoomEvent));

View File

@ -31,8 +31,13 @@ import isEqual from "lodash/isEqual";
import { EditRoomDialog } from "../dialogs";
import { Encoder } from "@docspace/shared/utils/encoder";
import api from "@docspace/shared/api";
import { getRoomInfo } from "@docspace/shared/api/rooms";
import {
deleteWatermarkSettings,
getRoomInfo,
getWatermarkSettings,
} from "@docspace/shared/api/rooms";
import { toastr } from "@docspace/shared/components/toast";
import { setWatermarkSettings } from "@docspace/shared/api/rooms";
const EditRoomEvent = ({
addActiveItems,
@ -77,12 +82,17 @@ const EditRoomEvent = ({
defaultRoomsQuota,
isDefaultRoomsQuotaSet,
changeRoomLifetime,
setInitialWatermarks,
getWatermarkRequest,
watermarksSettings,
isNotWatermarkSet,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common", "Files"]);
const [fetchedTags, setFetchedTags] = useState([]);
const [fetchedImage, setFetchedImage] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [isInitLoading, setIsInitLoading] = useState(false);
const startTags = Object.values(item.tags);
const startObjTags = startTags.map((tag, i) => ({ id: i, name: tag }));
@ -193,6 +203,15 @@ const EditRoomEvent = ({
if (removedTags.length)
actions.push(removeTagsFromRoom(room.id, removedTags));
if (watermarksSettings && !isNotWatermarkSet()) {
const request = getWatermarkRequest(room, watermarksSettings);
actions.push(request);
}
await Promise.all(actions);
if (!!item.logo.original && !roomParams.icon.uploadedFile) {
@ -281,24 +300,28 @@ const EditRoomEvent = ({
setFetchedImage(file);
}, []);
useEffect(() => {
const logo = item?.logo?.original ? item.logo.original : "";
if (logo) {
fetchLogoAction(logo);
}
}, []);
const fetchTagsAction = useCallback(async () => {
const tags = await fetchTags();
setFetchedTags(tags);
}, []);
useEffect(() => {
fetchTagsAction();
}, [fetchTagsAction]);
useEffect(() => {
setCreateRoomDialogVisible(true);
setIsInitLoading(true);
const logo = item?.logo?.original ? item.logo.original : "";
const requests = [fetchTags(), getWatermarkSettings(item.id)];
if (logo) requests.push(fetchLogoAction);
const fetchInfo = async () => {
const [tags, watermarks] = await Promise.all(requests);
setFetchedTags(tags);
setInitialWatermarks(watermarks);
setIsInitLoading(false);
};
fetchInfo();
return () => setCreateRoomDialogVisible(false);
}, []);
@ -312,6 +335,7 @@ const EditRoomEvent = ({
fetchedTags={fetchedTags}
fetchedImage={fetchedImage}
isLoading={isLoading}
isInitLoading={isInitLoading}
/>
);
};
@ -327,6 +351,7 @@ export default inject(
filesSettingsStore,
infoPanelStore,
currentQuotaStore,
createEditRoomStore,
}) => {
const {
editRoom,
@ -359,6 +384,12 @@ export default inject(
const { updateInfoPanelSelection } = infoPanelStore;
const { defaultRoomsQuota, isDefaultRoomsQuotaSet } = currentQuotaStore;
const {
setInitialWatermarks,
watermarksSettings,
isNotWatermarkSet,
getWatermarkRequest,
} = createEditRoomStore;
return {
defaultRoomsQuota,
@ -397,6 +428,10 @@ export default inject(
updateInfoPanelSelection,
changeRoomOwner,
changeRoomLifetime,
setInitialWatermarks,
watermarksSettings,
isNotWatermarkSet,
getWatermarkRequest,
};
},
)(observer(EditRoomEvent));

View File

@ -25,6 +25,8 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useEffect, useRef, useCallback } from "react";
import { inject, observer } from "mobx-react";
import isEqual from "lodash/isEqual";
import TagHandler from "./handlers/TagHandler";
import SetRoomParams from "./sub-components/SetRoomParams";
@ -42,6 +44,8 @@ const EditRoomDialog = ({
fetchedRoomParams,
fetchedTags,
fetchedImage,
isInitLoading,
isEqualWatermarkChanges,
}) => {
const [isScrollLocked, setIsScrollLocked] = useState(false);
const [isValidTitle, setIsValidTitle] = useState(true);
@ -77,7 +81,8 @@ const EditRoomDialog = ({
prevParams.icon.uploadedFile === currentParams.icon.uploadedFile) &&
prevParams.quota === currentParams.quota &&
prevParams.indexing === currentParams.indexing &&
isEqual(prevParams.lifetime, currentParams.lifetime)
isEqual(prevParams.lifetime, currentParams.lifetime) &&
isEqualWatermarkChanges()
);
};
@ -132,6 +137,7 @@ const EditRoomDialog = ({
visible={visible}
onClose={onCloseAction}
isScrollLocked={isScrollLocked}
isLoading={isInitLoading}
withFooterBorder
>
<ModalDialog.Header>
@ -183,4 +189,10 @@ const EditRoomDialog = ({
);
};
export default EditRoomDialog;
export default inject(({ createEditRoomStore }) => {
const { isEqualWatermarkChanges } = createEditRoomStore;
return {
isEqualWatermarkChanges,
};
})(observer(EditRoomDialog));

View File

@ -223,6 +223,7 @@ const SetRoomParams = ({
t={t}
roomParams={roomParams}
setRoomParams={setRoomParams}
isEdit={isEdit}
/>
)}

View File

@ -1,16 +1,17 @@
import { useState } from "react";
import { Trans } from "react-i18next";
import styled from "styled-components";
import { inject, observer } from "mobx-react";
import { Text } from "@docspace/shared/components/text";
import { ToggleButton } from "@docspace/shared/components/toggle-button";
import FileLifetime from "./FileLifetime";
import WatermarkBlock from "./Watermarks/WatermarkBlock";
const StyledVirtualDataRoomBlock = styled.div`
.virtual-data-room-block {
margin-bottom: 18px;
:last-child {
margin-bottom: -26px;
}
.virtual-data-room-block_header {
display: flex;
@ -26,6 +27,9 @@ const StyledVirtualDataRoomBlock = styled.div`
color: ${({ theme }) => theme.editLink.text.color};
}
.virtual-data-room-block_content {
margin-top: 16px;
}
}
`;
@ -64,14 +68,13 @@ const Block = ({
);
};
const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams, isEdit }) => {
const role = t("Translations:RoleViewer");
const [fileLifetimeChecked, setFileLifetimeChecked] = useState(
!!roomParams?.lifetime,
);
const [copyAndDownloadChecked, setCopyAndDownloadChecked] = useState(false);
const [watermarksChecked, setWatermarksChecked] = useState(false);
const onChangeAutomaticIndexing = () => {
setRoomParams({ ...roomParams, indexing: !roomParams.indexing });
@ -86,10 +89,6 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
setCopyAndDownloadChecked(!copyAndDownloadChecked);
};
const onChangeAddWatermarksToDocuments = () => {
setWatermarksChecked(!watermarksChecked);
};
return (
<StyledVirtualDataRoomBlock>
<Block
@ -124,13 +123,8 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
isDisabled={false}
isChecked={copyAndDownloadChecked}
></Block>
<Block
headerText={t("AddWatermarksToDocuments")}
bodyText={t("AddWatermarksToDocumentsDescription")}
onChange={onChangeAddWatermarksToDocuments}
isDisabled={false}
isChecked={watermarksChecked}
></Block>
<WatermarkBlock BlockComponent={Block} t={t} isEdit={isEdit} />
</StyledVirtualDataRoomBlock>
);
};

View File

@ -0,0 +1,334 @@
// (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
// (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 { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { Text } from "@docspace/shared/components/text";
import { ComboBox } from "@docspace/shared/components/combobox";
import { DropDownItem } from "@docspace/shared/components/drop-down-item";
import { FileInput } from "@docspace/shared/components/file-input";
import { imageProcessing } from "@docspace/shared/utils/common";
import { ButtonDelete } from "@docspace/shared/components/image-editor";
import { HelpButton } from "@docspace/shared/components/help-button";
import { StyledWatermark } from "./StyledComponent";
const scaleOptions = [
{ key: 100, label: "100" },
{ key: 200, label: "200" },
{ key: 300, label: "300" },
{ key: 400, label: "400" },
{ key: 500, label: "500" },
];
const rotateOptions = [
{ key: 0, label: "0" },
{ key: 30, label: "30" },
{ key: 45, label: "45" },
{ key: 60, label: "60" },
{ key: 90, label: "90" },
];
const getInitialScale = (scale, isEdit) => {
if (!isEdit || !scale) return scaleOptions[0];
return scaleOptions.find((item) => {
return item.key === scale;
});
};
const getInitialRotate = (rotate, isEdit) => {
if (!isEdit) return rotateOptions[0];
const item = rotateOptions.find((item) => {
return item.key === rotate;
});
return !item ? rotateOptions[0] : item;
};
const ImageWatermark = ({
isEdit,
setWatermarks,
initialWatermarksSettings,
imageUrl,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const initialInfo = useRef(null);
const previewRef = useRef(null);
if (initialInfo.current === null) {
initialInfo.current = {
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit),
scale: getInitialScale(initialWatermarksSettings?.imageScale, isEdit),
};
}
const initialInfoRef = initialInfo.current;
useEffect(() => {
const { enabled, isImage } = initialWatermarksSettings;
if (isEdit && enabled && isImage) {
setWatermarks(initialWatermarksSettings);
return;
}
setWatermarks({
rotate: initialInfoRef.rotate.key,
scale: initialInfoRef.scale.key,
additions: 0,
isImage: true,
enabled: true,
});
}, []);
useEffect(() => {
return () => {
URL.revokeObjectURL(previewRef.current);
previewRef.current = null;
};
}, []);
const [selectedRotate, setRotate] = useState(initialInfoRef.rotate);
const [selectedScale, setScale] = useState(initialInfoRef.scale);
const [selectedImageUrl, setImageUrl] = useState(imageUrl);
const onInput = (file) => {
imageProcessing(file)
.then((f) => {
if (f instanceof File) {
setWatermarks({ image: f });
const img = new Image();
previewRef.current = URL.createObjectURL(f);
img.src = previewRef.current;
img.onload = () => {
setImageUrl(previewRef.current);
};
}
})
.catch((error) => {
if (
error instanceof Error &&
error.message === "recursion depth exceeded"
) {
toastr.error(t("Common:SizeImageLarge"));
}
});
};
const onScaleChange = (item) => {
setScale(item);
setWatermarks({ imageScale: item.key });
};
const onRotateChange = (item) => {
setRotate(item);
setWatermarks({ rotate: item.key });
};
const onButtonClick = () => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current);
previewRef.current = null;
}
setWatermarks({ image: null, imageUrl: null });
setImageUrl("");
};
const rotateItems = () => {
const items = rotateOptions.map((item) => {
return (
<DropDownItem
className="access-right-item"
key={item.key}
data-key={item.key}
onClick={() => onRotateChange(item)}
>
{item.label}&deg;
</DropDownItem>
);
});
return <div style={{ display: "contents" }}>{items}</div>;
};
const scaleItems = () => {
const items = scaleOptions.map((item) => {
return (
<DropDownItem
className="access-right-item"
key={item.key}
data-key={item.key}
onClick={() => onScaleChange(item)}
>
{item.label}&#37;
</DropDownItem>
);
});
return <div style={{ display: "contents" }}>{items}</div>;
};
// const onSelectFile = (fileInfo) => {
// setWatermarks({ image: fileInfo });
// };
console.log("selectedRotate", selectedRotate.key, selectedScale.key);
return (
<StyledWatermark
rotate={selectedRotate.key}
scale={selectedScale.key / 100}
mainHeight={50}
>
{!selectedImageUrl && (
<FileInput
accept={["image/png", "image/jpeg"]}
onInput={onInput}
scale
/>
)}
{/* <FilesSelectorInput
onSelectFile={onSelectFile}
filterParam={FilesSelectorFilterTypes.IMG}
isSelect
scale
/> */}
{selectedImageUrl && (
<div className="image-wrapper">
<div>
<div className="image-description">
<Text fontWeight={600} className="image-watermark_text">
{t("WatermarkPreview")}
</Text>
<HelpButton
tooltipContent={
<Text fontSize="12px">{t("WatermarkPreviewHelp")}</Text>
}
offsetRight={0}
className="settings_unavailable"
/>
</div>
<div className="image-watermark_wrapper">
<img
alt="logo"
src={selectedImageUrl}
className="header-logo-icon"
/>
</div>
<ButtonDelete t={t} onClick={onButtonClick} />
</div>
<div className="options-wrapper">
<div>
<Text fontWeight={600} lineHeight="20px">
{t("Scale")}
</Text>
<ComboBox
onSelect={onScaleChange}
scaled
scaledOptions
advancedOptions={scaleItems()}
options={[]}
selectedOption={{}}
>
<div>{selectedScale.label}&#37;</div>
</ComboBox>
</div>
<div>
<Text fontWeight={600} lineHeight="20px">
{t("Rotate")}
</Text>
<ComboBox
onSelect={onRotateChange}
scaled
scaledOptions
advancedOptions={rotateItems()}
options={[]}
selectedOption={{}}
advancedOptionsCount={rotateOptions.length}
fillIcon={false}
>
<div>{selectedRotate.label}&deg;</div>
</ComboBox>
</div>
</div>
</div>
)}
</StyledWatermark>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings, watermarksSettings } =
createEditRoomStore;
const { imageUrl } = watermarksSettings;
return {
setWatermarks,
initialWatermarksSettings,
imageUrl,
};
})(observer(ImageWatermark));

View File

@ -0,0 +1,90 @@
// (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 styled, { css } from "styled-components";
const StyledWatermark = styled.div`
margin-top: 16px;
.watermark-title {
margin: 16px 0 8px 0;
}
.title-without-top {
margin-top: 0px;
}
.watermark-checkbox {
margin: 18px 0 0 0;
}
.options-wrapper {
display: grid;
grid-template-rows: 56px 56px;
gap: 16px;
}
.image-wrapper {
display: grid;
grid-template-columns: 216px auto;
gap: 16px;
.image-description {
display: flex;
gap: 8px;
align-items: baseline;
.image-watermark_text {
margin-bottom: 8px;
}
}
.image-watermark_wrapper {
width: 216px;
height: 216px;
border: 1px solid #eceef1;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
img {
width: 88%;
height: 88%;
transform: ${(props) =>
`rotate(${props.rotate}deg) scale(${props.scale})`};
opacity: 0.4;
margin: auto;
}
}
}
`;
const StyledBody = styled.div`
.types-content {
}
`;
export { StyledWatermark, StyledBody };

View File

@ -0,0 +1,240 @@
// (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 { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { TextInput } from "@docspace/shared/components/text-input";
import { Text } from "@docspace/shared/components/text";
import { ComboBox } from "@docspace/shared/components/combobox";
import { WatermarkAdditions } from "@docspace/shared/enums";
import { StyledWatermark } from "./StyledComponent";
import { Tabs, TabsTypes } from "@docspace/shared/components/tabs";
const tabsOptions = (t) => [
{
id: "UserName",
name: t("UserName"),
index: 0,
},
{
id: "UserEmail",
name: t("UserEmail"),
index: 1,
},
{
id: "UserIpAdress",
name: t("UserIPAddress"),
index: 2,
},
{
id: "CurrentDate",
name: t("Common:CurrentDate"),
index: 3,
},
{
id: "RoomName",
name: t("Common:RoomName"),
index: 4,
},
];
const getInitialState = (initialTab) => {
const state = {
UserName: false,
UserEmail: false,
UserIpAdress: false,
CurrentDate: false,
RoomName: false,
};
initialTab.map((item) => {
state[item.id] = true;
});
return state;
};
const getInitialText = (text, isEdit) => {
return isEdit && text ? text : "";
};
const getInitialTabs = (additions, isEdit, t) => {
const dataTabs = tabsOptions(t);
if (!isEdit || !additions) return [dataTabs[0]];
return dataTabs.filter((item) => additions & WatermarkAdditions[item.id]);
};
const rotateOptions = (t) => [
{ key: -45, label: t("Diagonal") },
{ key: 0, label: t("Horizontal") },
];
const getInitialRotate = (rotate, isEdit, t) => {
const dataRotate = rotateOptions(t);
if (!isEdit) return dataRotate[0];
const item = dataRotate.find((item) => {
return item.key === rotate;
});
return !item ? dataRotate[0] : item;
};
const ViewerInfoWatermark = ({
isEdit,
setWatermarks,
initialWatermarksSettings,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const elements = useRef(null);
const initialInfo = useRef(null);
if (initialInfo.current === null) {
initialInfo.current = {
dataRotate: rotateOptions(t),
dataTabs: tabsOptions(t),
tabs: getInitialTabs(initialWatermarksSettings?.additions, isEdit, t),
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit, t),
text: getInitialText(initialWatermarksSettings?.text, isEdit),
};
elements.current = getInitialState(initialInfo.current.tabs);
}
const initialInfoRef = initialInfo.current;
useEffect(() => {
const { enabled, isImage } = initialWatermarksSettings;
if (isEdit && enabled && !isImage) {
setWatermarks(initialWatermarksSettings);
return;
}
setWatermarks({
rotate: initialInfoRef.rotate.key,
additions: WatermarkAdditions.UserName,
isImage: false,
enabled: true,
image: "",
imageWidth: 0,
imageHeight: 0,
imageScale: 0,
});
}, []);
const [selectedPosition, setSelectedPosition] = useState(
initialInfoRef.rotate,
);
const [textValue, setTextValue] = useState(initialInfoRef.text);
const onSelect = (item) => {
let elementsData = elements.current;
let flagsCount = 0;
const key = item.id;
elementsData[key] = !elementsData[item.id];
for (const key in elementsData) {
const value = elementsData[key];
if (value) {
flagsCount += WatermarkAdditions[key];
}
}
setWatermarks({ additions: flagsCount });
};
const onPositionChange = (item) => {
setSelectedPosition(item);
setWatermarks({ rotate: item.key });
};
const onTextChange = (e) => {
const { value } = e.target;
setTextValue(value);
setWatermarks({ text: value });
};
return (
<StyledWatermark>
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
{t("AddWatermarkElements")}
</Text>
<Tabs
items={initialInfoRef.dataTabs}
selectedItems={initialInfoRef.tabs.map((item) => item.index)}
onSelect={onSelect}
type={TabsTypes.Secondary}
multiple
/>
<Text
className="watermark-title title-without-top"
fontWeight={600}
lineHeight="20px"
>
{t("AddStaticText")}
</Text>
<TextInput scale value={textValue} tabIndex={1} onChange={onTextChange} />
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
{t("Position")}
</Text>
<ComboBox
selectedOption={selectedPosition}
options={initialInfoRef.dataRotate}
onSelect={onPositionChange}
scaled
scaledOptions
/>
</StyledWatermark>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
return {
setWatermarks,
initialWatermarksSettings,
};
})(observer(ViewerInfoWatermark));

View File

@ -0,0 +1,102 @@
// (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
// (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 { useState, useEffect } from "react";
import { inject, observer } from "mobx-react";
import Watermarks from "./index";
const WatermarkBlock = ({
BlockComponent,
setWatermarks,
isEdit = false,
isWatermarks = false,
resetWatermarks,
t,
}) => {
useEffect(() => {
return () => resetWatermarks();
}, []);
const [watermarksChecked, setWatermarksChecked] = useState(
isWatermarks && isEdit,
);
const onChangeAddWatermarksToDocuments = () => {
setWatermarksChecked(!watermarksChecked);
setWatermarks({ enabled: !watermarksChecked });
};
return (
<BlockComponent
headerText={t("AddWatermarksToDocuments")}
bodyText={t("AddWatermarksToDocumentsDescription")}
onChange={onChangeAddWatermarksToDocuments}
isDisabled={false}
isChecked={watermarksChecked}
>
<Watermarks isEdit={isEdit} />
</BlockComponent>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings, resetWatermarks } =
createEditRoomStore;
return {
setWatermarks,
isWatermarks: initialWatermarksSettings?.enabled,
resetWatermarks,
};
})(observer(WatermarkBlock));

View File

@ -0,0 +1,102 @@
// (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 { useState } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { RadioButtonGroup } from "@docspace/shared/components/radio-button-group";
import ViewerInfoWatermark from "./ViewerInfo";
import { StyledBody } from "./StyledComponent";
import ImageWatermark from "./ImageWatermark";
const imageWatermark = "image",
viewerInfoWatermark = "viewerInfo";
const options = (t) => [
{
label: t("ViewerInfo"),
value: viewerInfoWatermark,
},
{
label: t("Common:Image"),
value: imageWatermark,
},
];
const getOptionType = (additions, isEdit) => {
if (isEdit) {
return additions === 0 ? imageWatermark : viewerInfoWatermark;
}
return viewerInfoWatermark;
};
const Watermarks = ({ isEdit, setWatermarks, initialWatermarksSettings }) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const [type, setType] = useState(
getOptionType(initialWatermarksSettings?.additions, isEdit),
);
const onSelectType = (e) => {
const { value } = e.target;
setType(value);
setWatermarks({
isImage: type === imageWatermark,
});
};
const typeOptions = options(t);
return (
<StyledBody>
<RadioButtonGroup
name="watermarks-radiobutton"
fontSize="13px"
fontWeight="400"
spacing="8px"
options={typeOptions}
selected={type}
onClick={onSelectType}
/>
{type === imageWatermark ? (
<ImageWatermark isEdit={isEdit} />
) : (
<ViewerInfoWatermark isEdit={isEdit} />
)}
</StyledBody>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
return {
setWatermarks,
initialWatermarksSettings,
};
})(observer(Watermarks));

View File

@ -29,6 +29,7 @@ import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { withTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { Button } from "@docspace/shared/components/button";
import { RoomsType } from "@docspace/shared/enums";
const CreateRoomConfirmDialog = ({
t,
@ -46,6 +47,10 @@ const CreateRoomConfirmDialog = ({
const onClose = () => setVisible(false);
const bodyText = RoomsType.VirtualDataRoom
? t("CreateEditRoomDialog:CreateRoomWatermarksConfirmation")
: t("CreateEditRoomDialog:CreateRoomConfirmation");
return (
<ModalDialog
visible={visible || confirmDialogIsLoading}
@ -54,9 +59,7 @@ const CreateRoomConfirmDialog = ({
zIndex={310}
>
<ModalDialog.Header>{t("Common:Warning")}</ModalDialog.Header>
<ModalDialog.Body>
{t("CreateEditRoomDialog:CreateRoomConfirmation")}
</ModalDialog.Body>
<ModalDialog.Body>{bodyText}</ModalDialog.Body>
<ModalDialog.Footer>
<Button
label={t("Common:ContinueButton")}

View File

@ -25,12 +25,15 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { makeAutoObservable } from "mobx";
import isEqual from "lodash/isEqual";
import { toastr } from "@docspace/shared/components/toast";
import { isDesktop } from "@docspace/shared/utils";
import FilesFilter from "@docspace/shared/api/files/filter";
import { getCategoryUrl } from "SRC_DIR/helpers/utils";
import { CategoryType } from "SRC_DIR/helpers/constants";
import { RoomsType } from "@docspace/shared/enums";
import { setWatermarkSettings } from "@docspace/shared/api/rooms";
class CreateEditRoomStore {
roomParams = null;
@ -46,6 +49,9 @@ class CreateEditRoomStore {
settingsStore = null;
infoPanelStore = null;
currentQuotaStore = null;
watermarksSettings = {};
initialWatermarksSettings = {};
isImageType = false;
constructor(
filesStore,
@ -91,6 +97,113 @@ class CreateEditRoomStore {
this.onClose = onClose;
};
setInitialWatermarks = (watermarksSettings) => {
this.initialWatermarksSettings = !watermarksSettings
? { enabled: false }
: watermarksSettings;
this.initialWatermarksSettings.isImage =
!!this.initialWatermarksSettings.imageUrl;
this.setWatermarks(this.initialWatermarksSettings);
};
setWatermarks = (object) => {
for (const [key, value] of Object.entries(object)) {
this.watermarksSettings[key] = value;
}
};
resetWatermarks = () => {
this.watermarksSettings = {};
this.initialWatermarksSettings = {};
};
isEqualWatermarkChanges = () => {
return isEqual(this.watermarksSettings, this.initialWatermarksSettings);
};
isNotWatermarkSet = () => {
if (
this.watermarksSettings.isImage &&
!this.watermarksSettings.image &&
!this.watermarksSettings.imageUrl
)
return true;
if (
!this.watermarksSettings.isImage &&
this.watermarksSettings.additions === 0
)
return true;
return false;
};
getWatermarkRequest = async (room) => {
if (!this.watermarksSettings.isImage) {
return setWatermarkSettings(room.id, {
enabled: this.watermarksSettings.enabled,
rotate: this.watermarksSettings.rotate,
text: this.watermarksSettings.text,
additions: this.watermarksSettings.additions,
});
}
const watermarkImage = this.watermarksSettings.image;
const watermarksSettings = this.watermarksSettings;
const getMeta = (url, onSetInfo) => {
//url for this.watermarksSettings.image.viewUrl
const img = new Image();
const imgUrl = url ?? URL.createObjectURL(watermarkImage);
img.src = imgUrl;
img.onload = () => {
URL.revokeObjectURL(imgUrl);
onSetInfo(null, img);
};
img.onerror = (err) => onSetInfo(err);
};
if (!watermarkImage && this.watermarksSettings.imageUrl) {
return setWatermarkSettings(room.id, {
enabled: watermarksSettings.enabled,
imageScale: watermarksSettings.imageScale,
rotate: watermarksSettings.rotate,
imageUrl: watermarksSettings.imageUrl,
// imageId: watermarksSettings.image.id,
imageWidth: watermarksSettings.imageWidth,
imageHeight: watermarksSettings.imageHeight,
});
}
const { uploadRoomLogo } = this.filesStore;
const uploadWatermarkData = new FormData();
uploadWatermarkData.append(0, watermarkImage);
const response = await uploadRoomLogo(uploadWatermarkData);
getMeta(null, (err, img) => {
if (err) {
toastr.error(err);
return;
}
return setWatermarkSettings(room.id, {
enabled: watermarksSettings.enabled,
imageScale: watermarksSettings.imageScale,
rotate: watermarksSettings.rotate,
imageUrl: response.data,
// imageId: watermarksSettings.image.id,
imageWidth: img.naturalWidth,
imageHeight: img.naturalHeight,
});
});
};
onCreateRoom = async (withConfirm = false, t) => {
const roomParams = this.roomParams;
@ -158,10 +271,17 @@ class CreateEditRoomStore {
const actions = [];
const requests = [];
if (this.watermarksSettings.enabled && !this.isNotWatermarkSet()) {
requests.push(this.getWatermarkRequest(room));
}
// delete thirdparty account if not needed
if (!isThirdparty && storageFolderId)
await deleteThirdParty(thirdpartyAccount.providerId);
requests.push(deleteThirdParty(thirdpartyAccount.providerId));
await Promise.all(requests);
// create new tags
for (let i = 0; i < createTagsData.length; i++) {
actions.push(createTag(createTagsData[i]));
@ -176,6 +296,7 @@ class CreateEditRoomStore {
await uploadRoomLogo(uploadLogoData).then(async (response) => {
const url = URL.createObjectURL(roomParams.icon.uploadedFile);
const img = new Image();
img.onload = async () => {
const { x, y, zoom } = roomParams.icon;
try {

View File

@ -0,0 +1,117 @@
import { useEffect } from "react";
const Watermark = ({
text,
rotate,
image,
color,
isSemitransparent,
children,
}) => {
const setCtxText = (ctx) => {
const getColor = () => {
if (color)
return Array.isArray(color)
? `rgb(${color[0]}, ${color[1]}, ${color[2]}, 1)`
: color;
if (isSemitransparent) return "rgba(223, 226, 227, 1)";
return "rgba(208, 213, 218, 1)";
};
ctx.fillStyle = getColor();
ctx.textAlign = "center";
ctx.font = `${13}px Arial`;
};
const setCtxCenteredText = (imgContent, ctx) => {
ctx.translate(imgContent.width / 2, imgContent.height / 2);
};
const setCtxRotate = (ctx) => {
const angle = (Math.PI / 180) * Number(rotate);
ctx.rotate(angle);
};
const setCtxTextWrap = (ctx, canvas) => {
let line = "",
marginTop = 0,
marginLeft = 0,
lineHeight = 15;
for (var n = 0; n < text.length; n++) {
let testLine = line + text[n];
let testWidth = ctx.measureText(testLine).width;
const percentWidth = ((canvas.width - testWidth) * 100) / canvas.width;
if (
(percentWidth < 32 && text[n] === " ") ||
testWidth > canvas.width - 4
) {
ctx.fillText(line, marginLeft, marginTop);
line = text[n];
marginTop += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, marginLeft, marginTop);
};
const getContent = (canvas, ctx, imgContent, imgWidth, imgHeight) => {
setCtxText(ctx);
ctx.drawImage(imgContent, 0, 0, imgWidth, imgHeight);
setCtxCenteredText(imgContent, ctx);
setCtxRotate(ctx);
setCtxTextWrap(ctx, canvas);
};
const drawCanvas = (drawContent, ctx, canvas) => {
canvas.width = drawContent.naturalWidth;
canvas.height = drawContent.naturalHeight;
getContent(
canvas,
ctx,
drawContent || "",
drawContent.naturalWidth,
drawContent.naturalHeight
);
};
const renderWatermark = () => {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.onload = () => {
drawCanvas(img, ctx, canvas);
};
img.onerror = () => {
drawCanvas(text);
};
img.crossOrigin = "anonymous";
img.referrerPolicy = "no-referrer";
img.src = image;
};
useEffect(() => {
renderWatermark();
}, [text, rotate, isSemitransparent]);
return (
<canvas>
<div>{children}</div>
</canvas>
);
};
export default Watermark;

View File

@ -506,3 +506,39 @@ export function getExportRoomIndexProgress() {
url: `files/rooms/indexexport`,
}) as Promise<TExportRoomIndexTask>;
}
export function setWatermarkSettings(
roomId: number | string,
data: {
enabled: boolean;
rotate: number;
text: string;
additions: number;
imageScale: number;
imageUrl: string;
imageWidth: string;
imageHeight: string;
},
) {
const options = {
method: "put",
url: `files/rooms/${roomId}/watermark`,
data,
};
return request(options);
}
export function getWatermarkSettings(roomId: number | string) {
return request({
method: "get",
url: `files/rooms/${roomId}/watermark`,
});
}
export function deleteWatermarkSettings(roomId: number | string) {
return request({
method: "delete",
url: `files/rooms/${roomId}/watermark`,
});
}

View File

@ -0,0 +1,123 @@
// (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
// (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 { ReactSVG } from "react-svg";
import styled from "styled-components";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import { TTranslation } from "../../../types";
const StyledButton = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 6px 0;
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
border-radius: 3px;
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBackground};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBorderColor};
}
&-text {
user-select: none;
font-weight: 600;
line-height: 20px;
color: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
}
svg {
path {
fill: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
}
}
`;
const ButtonDelete = ({
onClick,
t,
}: {
onClick: (e: React.MouseEvent) => void;
t: TTranslation;
}) => {
return (
<StyledButton
className="icon_cropper-delete_button"
onClick={onClick}
title={t("Common:Delete")}
>
<ReactSVG src={TrashReactSvgUrl} />
<div className="icon_cropper-delete_button-text">
{t("Common:Delete")}
</div>
</StyledButton>
);
};
export default ButtonDelete;

View File

@ -25,15 +25,11 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useRef, useEffect } from "react";
import resizeImage from "resize-image";
import { imageProcessing } from "@docspace/shared/utils/common";
import DropzoneComponent from "../../dropzone";
import { toastr } from "../../toast";
const ONE_MEGABYTE = 1024 * 1024;
const COMPRESSION_RATIO = 2;
const NO_COMPRESSION_RATIO = 1;
const Dropzone = ({
t,
setUploadedFile,
@ -57,70 +53,12 @@ const Dropzone = ({
};
}, []);
async function resizeRecursiveAsync(
img: { width: number; height: number },
canvas: HTMLCanvasElement,
compressionRatio = COMPRESSION_RATIO,
depth = 0,
): Promise<unknown> {
const data = resizeImage.resize(
// @ts-expect-error canvas
canvas,
img.width / compressionRatio,
img.height / compressionRatio,
resizeImage.JPEG,
);
const file = await fetch(data)
.then((res) => res.blob())
.then((blob) => {
const f = new File([blob], "File name", {
type: "image/jpg",
});
return f;
});
// const stepMessage = `Step ${depth + 1}`;
// const sizeMessage = `size = ${file.size} bytes`;
// const compressionRatioMessage = `compressionRatio = ${compressionRatio}`;
// console.log(`${stepMessage} ${sizeMessage} ${compressionRatioMessage}`);
if (file.size < maxImageSize) {
return file;
}
if (depth > 5) {
// console.log("start");
throw new Error("recursion depth exceeded");
}
return new Promise((resolve) => {
// eslint-disable-next-line no-promise-executor-return
return resolve(file);
}).then(() =>
resizeRecursiveAsync(img, canvas, compressionRatio + 1, depth + 1),
);
}
const onDrop = async ([file]: File[]) => {
timer.current = setTimeout(() => {
setLoadingFile(true);
}, 50);
try {
const imageBitMap = await createImageBitmap(file);
const width = imageBitMap.width;
const height = imageBitMap.height;
// @ts-expect-error imageBitMap
const canvas = resizeImage.resize2Canvas(imageBitMap, width, height);
resizeRecursiveAsync(
{ width, height },
canvas,
file.size > maxImageSize ? COMPRESSION_RATIO : NO_COMPRESSION_RATIO,
)
imageProcessing(file)
.then((f) => {
if (mount.current) {
if (f instanceof File) setUploadedFile(f);

View File

@ -32,12 +32,12 @@ import AvatarEditor, { Position } from "react-avatar-editor";
import ZoomMinusReactSvgUrl from "PUBLIC_DIR/images/zoom-minus.react.svg?url";
import ZoomPlusReactSvgUrl from "PUBLIC_DIR/images/zoom-plus.react.svg?url";
import IconCropperGridSvgUrl from "PUBLIC_DIR/images/icon-cropper-grid.svg?url";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import { Slider } from "../../slider";
import { IconButton } from "../../icon-button";
import { StyledImageCropper } from "../ImageEditor.styled";
import { ImageCropperProps } from "../ImageEditor.types";
import ButtonDelete from "../ButtonDelete";
const ImageCropper = ({
t,
@ -132,16 +132,8 @@ const ImageCropper = ({
crossOrigin="anonymous"
/>
</div>
<div
className="icon_cropper-delete_button"
onClick={handleDeleteImage}
title={t("Common:Delete")}
>
<ReactSVG src={TrashReactSvgUrl} />
<div className="icon_cropper-delete_button-text">
{t("Common:Delete")}
</div>
</div>
<ButtonDelete onClick={handleDeleteImage} t={t} />
{typeof uploadedFile !== "string" &&
uploadedFile?.name &&

View File

@ -63,50 +63,6 @@ const StyledImageCropper = styled.div<{ disableImageRescaling?: boolean }>`
`};
}
.icon_cropper-delete_button {
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 6px 0;
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
border-radius: 3px;
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBackground};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBorderColor};
}
&-text {
user-select: none;
font-weight: 600;
line-height: 20px;
color: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
}
svg {
path {
fill: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
}
}
}
.icon_cropper-zoom-container {
display: flex;
flex-direction: row;

View File

@ -27,6 +27,7 @@
import React from "react";
import Dropzone from "./Dropzone";
import ImageCropper from "./ImageCropper";
import ButtonDelete from "./ButtonDelete";
import { ImageEditorProps } from "./ImageEditor.types";
import AvatarPreview from "./AvatarPreview";
@ -77,4 +78,4 @@ const ImageEditor = ({
);
};
export { ImageEditor, AvatarPreview };
export { ImageEditor, AvatarPreview, Dropzone, ButtonDelete };

View File

@ -24,7 +24,5 @@
// 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
export const INDEX_NOT_FOUND = -1;
export const OFFSET_RIGHT = 48;
export const OFFSET_LEFT = 48;

View File

@ -31,9 +31,14 @@ import { TabsTypes } from "./Tabs.enums";
export const StyledTabs = styled.div<{
stickyTop?: string;
multiple: boolean;
}>`
${(props) =>
props.multiple &&
css`
display: flex;
flex-direction: column;
`};
.sticky {
height: 33px;
@ -128,6 +133,7 @@ export const ScrollbarTabs = styled(Scrollbar)<{
export const TabList = styled.div<{
$type?: TabsTypes;
multiple: boolean;
}>`
display: flex;
align-items: center;
@ -136,6 +142,13 @@ export const TabList = styled.div<{
width: 100%;
height: 32px;
${(props) =>
props.multiple &&
css`
flex-wrap: wrap;
height: fit-content;
`};
gap: ${(props) => (props.$type === TabsTypes.Primary ? "20px" : "8px")};
border-bottom: ${(props) =>

View File

@ -39,26 +39,26 @@ import {
} from "./Tabs.styled";
import { TabsProps, TTabItem } from "./Tabs.types";
import { TabsTypes } from "./Tabs.enums";
import { OFFSET_RIGHT, OFFSET_LEFT, INDEX_NOT_FOUND } from "./Tabs.constants";
import { OFFSET_RIGHT, OFFSET_LEFT } from "./Tabs.constants";
const Tabs = (props: TabsProps) => {
const {
items,
selectedItemId,
selectedItems = [],
type = TabsTypes.Primary,
stickyTop,
onSelect,
multiple = false,
...rest
} = props;
let selectedItemIndex = items.findIndex((item) => item.id === selectedItemId);
if (selectedItemIndex === INDEX_NOT_FOUND) {
selectedItemIndex = 0;
}
const selectedItemIndex = !selectedItemId
? 0
: items.findIndex((item) => item.id === selectedItemId);
const [currentItem, setCurrentItem] = useState<TTabItem>(
items[selectedItemIndex],
);
const [currentItem, setCurrentItem] = useState(selectedItemIndex);
const [multipleItems, setMultipleItems] = useState(selectedItems);
const tabsRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<ScrollbarType>(null);
@ -67,8 +67,8 @@ const Tabs = (props: TabsProps) => {
const isViewLastTab = useViewTab(scrollRef, tabsRef, items.length - 1);
useEffect(() => {
setCurrentItem(items[selectedItemIndex]);
}, [selectedItemIndex, items]);
if (!multiple) setCurrentItem(selectedItemIndex);
}, [selectedItemIndex, items, multiple]);
const scrollToTab = (index: number): void => {
if (!scrollRef.current || !tabsRef.current) return;
@ -95,19 +95,42 @@ const Tabs = (props: TabsProps) => {
};
const setSelectedItem = (selectedTabItem: TTabItem, index: number): void => {
setCurrentItem(selectedTabItem);
scrollToTab(index);
onSelect?.(selectedTabItem);
if (multiple) {
const indexOperation = () => {
const newArray = [...multipleItems];
const deletionIndex = newArray.indexOf(index);
if (deletionIndex !== -1) {
newArray.splice(deletionIndex, 1);
return newArray;
}
newArray.push(index);
return newArray;
};
return (
<StyledTabs {...rest} stickyTop={stickyTop}>
<div className="sticky">
{!isViewFirstTab && <div className="blur-ahead" />}
<ScrollbarTabs ref={scrollRef} autoHide={false} noScrollY $type={type}>
<TabList ref={tabsRef} $type={type}>
const updatedActiveTab = indexOperation();
setMultipleItems(updatedActiveTab);
onSelect?.(selectedTabItem);
return;
}
setCurrentItem(index);
onSelect?.(selectedTabItem);
scrollToTab(index);
};
const renderContent = (
<TabList ref={tabsRef} $type={type} multiple={multiple}>
{items.map((item, index) => {
const isActive = item.id === currentItem.id;
const isActive = multiple
? multipleItems.indexOf(index) !== -1
: index === currentItem;
return (
<Tab
key={item.id}
@ -125,13 +148,31 @@ const Tabs = (props: TabsProps) => {
);
})}
</TabList>
);
return (
<StyledTabs {...rest} stickyTop={stickyTop} multiple={multiple}>
{multiple && renderContent}
{!multiple && (
<div className="sticky">
{!isViewFirstTab && <div className="blur-ahead" />}
<ScrollbarTabs
ref={scrollRef}
autoHide={false}
noScrollY
$type={type}
>
{renderContent}
</ScrollbarTabs>
{!isViewLastTab && <div className="blur-back" />}
</div>
)}
<div className="sticky-indent" />
<div className="tabs-body">{currentItem?.content}</div>
{!multiple && (
<div className="tabs-body">{items[currentItem]?.content}</div>
)}
</StyledTabs>
);
};

View File

@ -44,10 +44,13 @@ export interface TabsProps {
items: TTabItem[];
/** Selected item of tabs. */
selectedItemId?: number | string;
selectedItems?: number[];
/** Theme for displaying tabs. */
type?: TabsTypes;
/** Tab indentation for sticky positioning. */
stickyTop?: string;
/** Enables multiple select */
multiple?: boolean;
/** Sets a callback function that is triggered when the tab is selected. */
onSelect?: (element: TTabItem) => void;
}

View File

@ -550,6 +550,18 @@ export const enum EditorConfigErrorType {
TenantQuotaException = "ASC.Core.Tenants.TenantQuotaException",
}
/**
* Enum for watermarks.
* @readonly
*/
export const enum WatermarkAdditions {
UserName = 1,
UserEmail = 2,
UserIpAdress = 4,
CurrentDate = 8,
RoomName = 16,
}
export const enum RoomsStorageFilter {
internal = 1,
thirdparty = 2,

View File

@ -102,7 +102,8 @@ export const convertFilesToItems: (
filterParam?: string | number,
) => {
const items = files.map((file) => {
const { id, title, security, folderId, rootFolderType, fileExst } = file;
const { id, title, security, folderId, rootFolderType, fileExst, viewUrl } =
file;
const icon = getIcon(fileExst || DEFAULT_FILE_EXTS);
const label = getTitleWithoutExtension(file, false);
@ -117,6 +118,7 @@ export const convertFilesToItems: (
rootFolderType,
isDisabled: !filterParam,
fileExst,
viewUrl,
};
});
return items;

View File

@ -134,6 +134,7 @@ const FilesSelector = ({
title: string;
path?: string[];
fileExst?: string;
viewUrl?: string;
inPublic?: boolean;
} | null>(null);
const [total, setTotal] = React.useState<number>(0);
@ -347,6 +348,7 @@ const FilesSelector = ({
id: item.id,
title: item.label,
fileExst: item.fileExst,
viewUrl: item.viewUrl,
inPublic,
});

View File

@ -61,7 +61,6 @@ const {
strongBlue,
darkRed,
darkErrorStatus,
charlestonGreen,
outerSpace,

View File

@ -33,6 +33,7 @@ import moment from "moment-timezone";
import { isMobile } from "react-device-detect";
import { I18nextProviderProps } from "react-i18next";
import sjcl from "sjcl";
import resizeImage from "resize-image";
import { flagsIcons } from "@docspace/shared/utils/image-flags";
@ -1136,3 +1137,66 @@ export function setLanguageForUnauthorized(culture: string) {
window.location.reload();
}
export const imageProcessing = async (file: File, maxSize?: number) => {
const ONE_MEGABYTE = 1024 * 1024;
const COMPRESSION_RATIO = 2;
const NO_COMPRESSION_RATIO = 1;
const maxImageSize = maxSize ?? ONE_MEGABYTE;
const imageBitMap = await createImageBitmap(file);
const width = imageBitMap.width;
const height = imageBitMap.height;
// @ts-expect-error imageBitMap
const canvas = resizeImage.resize2Canvas(imageBitMap, width, height);
async function resizeRecursiveAsync(
img: { width: number; height: number },
compressionRatio = COMPRESSION_RATIO,
depth = 0,
): Promise<unknown> {
const data = resizeImage.resize(
// @ts-expect-error canvas
canvas,
img.width / compressionRatio,
img.height / compressionRatio,
resizeImage.JPEG,
);
const newFile = await fetch(data)
.then((res) => res.blob())
.then((blob) => {
const f = new File([blob], "File name", {
type: "image/jpg",
});
return f;
});
// const stepMessage = `Step ${depth + 1}`;
// const sizeMessage = `size = ${file.size} bytes`;
// const compressionRatioMessage = `compressionRatio = ${compressionRatio}`;
// console.log(`${stepMessage} ${sizeMessage} ${compressionRatioMessage}`);
if (file.size < maxImageSize) {
return file;
}
if (depth > 5) {
// console.log("start");
throw new Error("recursion depth exceeded");
}
return new Promise((resolve) => {
// eslint-disable-next-line no-promise-executor-return
return resolve(newFile);
}).then(() => resizeRecursiveAsync(img, compressionRatio + 1, depth + 1));
}
return resizeRecursiveAsync(
{ width, height },
file.size > maxImageSize ? COMPRESSION_RATIO : NO_COMPRESSION_RATIO,
);
};

View File

@ -109,6 +109,7 @@
"Culture_vi": "Tiếng Việt (Việt Nam)",
"Culture_zh-CN": "中文(简体,中国)",
"Custom": "Custom",
"CurrentDate": "Current Date",
"CustomFilter": "Custom filter",
"CustomQuota": "Custom quota",
"CustomRoomDescription": "Apply your own settings to use this room for any custom purpose.",
@ -368,6 +369,7 @@
"ReviewRoomTitle": "Review room",
"Role": "Role",
"Room": "Room",
"RoomName": "Room Name",
"RoomAdmin": "Room admin",
"RoomList": "Room list",
"Rooms": "Rooms",