diff --git a/packages/client/public/locales/en/CreateEditRoomDialog.json b/packages/client/public/locales/en/CreateEditRoomDialog.json
index 6fb4e78567..5328f3b6e4 100644
--- a/packages/client/public/locales/en/CreateEditRoomDialog.json
+++ b/packages/client/public/locales/en/CreateEditRoomDialog.json
@@ -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{{thirdpartyPath}} ",
"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",
diff --git a/packages/client/public/locales/en/Files.json b/packages/client/public/locales/en/Files.json
index ebad476fc1..a771075f1d 100644
--- a/packages/client/public/locales/en/Files.json
+++ b/packages/client/public/locales/en/Files.json
@@ -68,9 +68,12 @@
"EnableNotifications": "Enable notifications",
"ErrorChangeIndex": "Error when changing index. The problem may be caused on the server side. Reload the page or check your Internet connection settings.",
"ExcludeSubfolders": "Exclude subfolders",
+ "ExportRoomIndex": "Export room index",
+ "ExportRoomIndexAlreadyInProgressError": "Room index export is already in progress. Please wait until the current export is completed to start the new one.",
"FavoritesEmptyContainerDescription": "To mark files as favorites or remove them from this list, use the context menu.",
"FileContents": "File contents",
"FileDownloadingIsRestricted": "File downloading is restricted in this room.",
+ "FileExportedToMyDocuments": "file exported to My Documents",
"FileRemoved": "File moved to Trash",
"FileRenamed": "The document '{{oldTitle}}' is renamed to '{{newTitle}}'",
"FilesWillAppearHere": "Files and folders added to the room will appear here.",
@@ -185,6 +188,10 @@
"WantToRestoreTheRooms": "All shared links in restored rooms will become active, and their contents will be available to everyone with the room links. Do you want to restore the rooms?",
"WithSubfolders": "With subfolders",
"YouLeftTheRoom": "You have left the room",
+ "RoomFilesLifetime": "The file lifetime is set to {{days}} {{period}} in this room",
+ "FileWillBeDeleted": "The file will be deleted {{date}}",
+ "LifetimeDialogDescription": "Lifetime countdown begins at the file creation date. Some files in this room exceed the proposed lifetime and will be deleted once you enable the setting.",
+ "LifetimeDialogDescriptionHeader": "Older files with exceeded lifetime will be deleted",
"Protected": "protected",
"Embed": "Embed"
}
diff --git a/packages/client/public/locales/en/InfoPanel.json b/packages/client/public/locales/en/InfoPanel.json
index 2b381b2378..d50fceabe4 100644
--- a/packages/client/public/locales/en/InfoPanel.json
+++ b/packages/client/public/locales/en/InfoPanel.json
@@ -6,6 +6,7 @@
"CreationDate": "Creation date",
"Data": "Data",
"DateModified": "Date modified",
+ "LifetimeEnds": "Lifetime ends",
"DeletedRoomTags": "Tags removed.",
"ExpectUsers": "Expect users",
"FeedLinkWasDeleted": "Link was deleted",
diff --git a/packages/client/src/HOCs/withQuickButtons.js b/packages/client/src/HOCs/withQuickButtons.js
index 77142676f6..2cd895c0c1 100644
--- a/packages/client/src/HOCs/withQuickButtons.js
+++ b/packages/client/src/HOCs/withQuickButtons.js
@@ -26,9 +26,12 @@
import React from "react";
import { inject, observer } from "mobx-react";
+import moment from "moment";
import { toastr } from "@docspace/shared/components/toast";
import { copyShareLink } from "@docspace/shared/utils/copy";
import QuickButtons from "../components/QuickButtons";
+import { LANGUAGE } from "@docspace/shared/constants";
+import { getCookie, getCorrectDate } from "@docspace/shared/utils";
export default function withQuickButtons(WrappedComponent) {
class WithQuickButtons extends React.Component {
@@ -100,6 +103,39 @@ export default function withQuickButtons(WrappedComponent) {
}
};
+ getStartDate = () => {
+ const { period, value } = this.props.roomLifetime;
+ const date = new Date(this.props.item.expired);
+
+ switch (period) {
+ case 0:
+ return new Date(date.setDate(date.getDate() - value));
+ case 1:
+ return new Date(date.setMonth(date.getMonth() - value));
+ case 2:
+ return new Date(date.setFullYear(date.getFullYear() - value));
+ default:
+ break;
+ }
+ };
+
+ getShowLifetimeIcon = () => {
+ const { item } = this.props;
+
+ const startDate = this.getStartDate();
+ const dateDiff = moment(startDate).diff(item.expired) * 0.1;
+ const showDate = moment(item.expired).add(dateDiff, "milliseconds");
+
+ return moment().valueOf() >= showDate.valueOf();
+ };
+
+ getItemExpiredDate = () => {
+ const { culture, item } = this.props;
+
+ const locale = getCookie(LANGUAGE) || culture;
+ return getCorrectDate(locale, item.expired);
+ };
+
render() {
const { isLoading } = this.state;
@@ -116,8 +152,14 @@ export default function withQuickButtons(WrappedComponent) {
isArchiveFolder,
isIndexEditingMode,
currentDeviceType,
+ roomLifetime,
} = this.props;
+ const showLifetimeIcon =
+ item.expired && roomLifetime ? this.getShowLifetimeIcon() : false;
+ const expiredDate =
+ item.expired && roomLifetime ? this.getItemExpiredDate() : null;
+
const quickButtonsComponent = (
);
@@ -180,10 +224,12 @@ export default function withQuickButtons(WrappedComponent) {
isTrashFolder || isArchiveFolderRoot || isPersonalFolderRoot;
const { isPublicRoom } = publicRoomStore;
- const { getPrimaryFileLink, setShareChanged } = infoPanelStore;
+ const { getPrimaryFileLink, setShareChanged, infoPanelRoom } =
+ infoPanelStore;
return {
theme: settingsStore.theme,
+ culture: settingsStore.culture,
currentDeviceType: settingsStore.currentDeviceType,
isAdmin: authStore.isAdmin,
lockFileAction,
@@ -198,6 +244,7 @@ export default function withQuickButtons(WrappedComponent) {
getPrimaryFileLink,
setShareChanged,
isIndexEditingMode,
+ roomLifetime: infoPanelRoom?.lifetime,
};
},
)(observer(WithQuickButtons));
diff --git a/packages/client/src/components/FilesPanels/index.js b/packages/client/src/components/FilesPanels/index.js
index 7dc5f48395..bad54aa5f9 100644
--- a/packages/client/src/components/FilesPanels/index.js
+++ b/packages/client/src/components/FilesPanels/index.js
@@ -77,6 +77,7 @@ import ChangeRoomOwnerPanel from "../panels/ChangeRoomOwnerPanel";
import { CreatedPDFFormDialog } from "../dialogs/CreatedPDFFormDialog";
import { PDFFormEditingDialog } from "../dialogs/PDFFormEditingDialog";
import ReorderIndexDialog from "../dialogs/ReorderIndexDialog";
+import LifetimeDialog from "../dialogs/LifetimeDialog";
import { SharePDFFormDialog } from "../dialogs/SharePDFFormDialog";
const Panels = (props) => {
@@ -91,6 +92,7 @@ const Panels = (props) => {
deleteThirdPartyDialogVisible,
versionHistoryPanelVisible,
deleteDialogVisible,
+ lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,
@@ -271,6 +273,7 @@ const Panels = (props) => {
),
deleteDialogVisible && ,
+ lifetimeDialogVisible && ,
emptyTrashDialogVisible && ,
downloadDialogVisible && ,
@@ -381,6 +384,7 @@ export default inject(
connectDialogVisible,
deleteThirdPartyDialogVisible,
deleteDialogVisible,
+ lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,
@@ -446,6 +450,7 @@ export default inject(
deleteThirdPartyDialogVisible,
versionHistoryPanelVisible,
deleteDialogVisible,
+ lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,
diff --git a/packages/client/src/components/GlobalEvents/CreateRoomEvent.js b/packages/client/src/components/GlobalEvents/CreateRoomEvent.js
index b567b48de1..4ca16d8f8e 100644
--- a/packages/client/src/components/GlobalEvents/CreateRoomEvent.js
+++ b/packages/client/src/components/GlobalEvents/CreateRoomEvent.js
@@ -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));
diff --git a/packages/client/src/components/GlobalEvents/EditRoomEvent.js b/packages/client/src/components/GlobalEvents/EditRoomEvent.js
index ae61ca971b..78421bdf04 100644
--- a/packages/client/src/components/GlobalEvents/EditRoomEvent.js
+++ b/packages/client/src/components/GlobalEvents/EditRoomEvent.js
@@ -27,11 +27,17 @@
import React, { useState, useEffect, useCallback } from "react";
import { inject, observer } from "mobx-react";
import { useTranslation } from "react-i18next";
+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,
@@ -75,12 +81,18 @@ 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 }));
@@ -106,6 +118,7 @@ const EditRoomEvent = ({
},
roomOwner: item.createdBy,
indexing: item.indexing,
+ lifetime: item.lifetime,
...(isDefaultRoomsQuotaSet && {
quota: item.quotaLimit,
@@ -140,6 +153,7 @@ const EditRoomEvent = ({
const isTitleChanged = roomParams?.title !== item.title;
const isQuotaChanged = quotaLimit !== item.quotaLimit;
const isOwnerChanged = roomParams?.roomOwner?.id !== item.createdBy.id;
+ const lifetimeChanged = !isEqual(roomParams.lifetime, item.lifetime);
const tags = roomParams.tags.map((tag) => tag.name);
const newTags = roomParams.tags.filter((t) => t.isNew).map((t) => t.name);
@@ -176,6 +190,12 @@ const EditRoomEvent = ({
displayName: roomParams.roomOwner.label,
};
}
+
+ if (lifetimeChanged) {
+ actions.push(changeRoomLifetime(room.id, roomParams.lifetime));
+ room.lifetime = roomParams.lifetime;
+ }
+
if (tags.length) {
actions.push(addTagsToRoom(room.id, newTags));
room.tags = tags;
@@ -183,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) {
@@ -238,7 +267,11 @@ const EditRoomEvent = ({
if (withPaging) await updateCurrentFolder(null, currentFolderId);
if (item.id === currentFolderId) {
- updateEditedSelectedRoom(editRoomParams.title, tags);
+ updateEditedSelectedRoom(
+ editRoomParams.title,
+ tags,
+ roomParams.lifetime,
+ );
if (item.logo.original && !roomParams.icon.uploadedFile) {
removeLogoPaths();
// updateInfoPanelSelection();
@@ -267,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);
}, []);
@@ -298,6 +335,7 @@ const EditRoomEvent = ({
fetchedTags={fetchedTags}
fetchedImage={fetchedImage}
isLoading={isLoading}
+ isInitLoading={isInitLoading}
/>
);
};
@@ -313,6 +351,7 @@ export default inject(
filesSettingsStore,
infoPanelStore,
currentQuotaStore,
+ createEditRoomStore,
}) => {
const {
editRoom,
@@ -337,13 +376,20 @@ export default inject(
removeLogoPaths,
updateLogoPathsCacheBreaker,
} = selectedFolderStore;
- const { updateCurrentFolder, changeRoomOwner } = filesActionsStore;
+ const { updateCurrentFolder, changeRoomOwner, changeRoomLifetime } =
+ filesActionsStore;
const { getThirdPartyIcon } = filesSettingsStore.thirdPartyStore;
const { setCreateRoomDialogVisible } = dialogsStore;
const { withPaging } = settingsStore;
const { updateInfoPanelSelection } = infoPanelStore;
const { defaultRoomsQuota, isDefaultRoomsQuotaSet } = currentQuotaStore;
+ const {
+ setInitialWatermarks,
+ watermarksSettings,
+ isNotWatermarkSet,
+ getWatermarkRequest,
+ } = createEditRoomStore;
return {
defaultRoomsQuota,
@@ -381,6 +427,11 @@ export default inject(
updateInfoPanelSelection,
changeRoomOwner,
+ changeRoomLifetime,
+ setInitialWatermarks,
+ watermarksSettings,
+ isNotWatermarkSet,
+ getWatermarkRequest,
};
},
)(observer(EditRoomEvent));
diff --git a/packages/client/src/components/QuickButtons.js b/packages/client/src/components/QuickButtons.js
index ad83ab4eb0..e2d989b7cf 100644
--- a/packages/client/src/components/QuickButtons.js
+++ b/packages/client/src/components/QuickButtons.js
@@ -32,21 +32,38 @@ import LinkReactSvgUrl from "PUBLIC_DIR/images/link.react.svg?url";
import LockedReactSvgUrl from "PUBLIC_DIR/images/locked.react.svg?url";
import FileActionsFavoriteReactSvgUrl from "PUBLIC_DIR/images/file.actions.favorite.react.svg?url";
import FavoriteReactSvgUrl from "PUBLIC_DIR/images/favorite.react.svg?url";
+import LifetimeReactSvgUrl from "PUBLIC_DIR/images/lifetime.react.svg?url";
import LockedReact12SvgUrl from "PUBLIC_DIR/images/icons/12/lock.react.svg?url";
import React, { useMemo } from "react";
import styled from "styled-components";
-
-import { isTablet, isMobile, commonIconsStyles } from "@docspace/shared/utils";
+import { isTablet } from "@docspace/shared/utils";
import {
DeviceType,
FileStatus,
RoomsType,
ShareAccessRights,
} from "@docspace/shared/enums";
+import { Tooltip } from "@docspace/shared/components/tooltip";
+import { Text } from "@docspace/shared/components/text";
import { ColorTheme, ThemeId } from "@docspace/shared/components/color-theme";
+const StyledQuickButtons = styled.div`
+ .file-lifetime {
+ svg {
+ rect {
+ fill: ${({ theme }) => theme.filesQuickButtons.lifeTimeColor};
+ }
+
+ circle,
+ path {
+ stroke: ${({ theme }) => theme.filesQuickButtons.lifeTimeColor};
+ }
+ }
+ }
+`;
+
const QuickButtons = (props) => {
const {
t,
@@ -66,6 +83,8 @@ const QuickButtons = (props) => {
isArchiveFolder,
isIndexEditingMode,
currentDeviceType,
+ showLifetimeIcon,
+ expiredDate,
} = props;
const isMobile = currentDeviceType === DeviceType.mobile;
@@ -137,8 +156,36 @@ const QuickButtons = (props) => {
!isArchiveFolder &&
!isTile;
+ const getTooltipContent = () => (
+
+ {t("Files:FileWillBeDeleted", { date: expiredDate })}.
+
+ );
+
return (
-
+
+ {showLifetimeIcon && (
+ <>
+
+
+
+ >
+ )}
+
{isAvailableLockFile && !isIndexEditingMode && (
{
hoverColor={theme.filesQuickButtons.hoverColor}
/>
)} */}
-
+
);
};
diff --git a/packages/client/src/components/dialogs/ConflictResolveDialog/index.tsx b/packages/client/src/components/dialogs/ConflictResolveDialog/index.tsx
index 0e5b5b3ba2..7bab03dc07 100644
--- a/packages/client/src/components/dialogs/ConflictResolveDialog/index.tsx
+++ b/packages/client/src/components/dialogs/ConflictResolveDialog/index.tsx
@@ -220,13 +220,13 @@ const ConflictResolveDialog = (props: ConflictResolveDialogProps) => {
isLoading={!ready}
onSubmit={isUploadConflict ? onAcceptUploadType : onAcceptType}
onClose={onCloseDialog}
- cancelButtonLabel={t("Common:CancelButton")}
- submitButtonLabel={t("Common:OKButton")}
+ cancelButtonLabel={t("CancelButton")}
+ submitButtonLabel={t("OKButton")}
messageText={messageText}
selectActionText={t("Common:ConflictResolveSelectAction")}
overwriteTitle={t("Common:OverwriteTitle")}
overwriteDescription={t("Common:OverwriteDescription")}
- duplicateTitle={t("Common:CreateFileCopy")}
+ duplicateTitle={t("CreateFileCopy")}
duplicateDescription={t("Common:CreateDescription")}
skipTitle={t("Common:SkipTitle")}
skipDescription={t("Common:SkipDescription")}
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/EditRoomDialog.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/EditRoomDialog.js
index 115945a69c..eb2c73b5c2 100644
--- a/packages/client/src/components/dialogs/CreateEditRoomDialog/EditRoomDialog.js
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/EditRoomDialog.js
@@ -25,7 +25,9 @@
// 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";
import DialogHeader from "./sub-components/DialogHeader";
@@ -42,6 +44,8 @@ const EditRoomDialog = ({
fetchedRoomParams,
fetchedTags,
fetchedImage,
+ isInitLoading,
+ isEqualWatermarkChanges,
}) => {
const [isScrollLocked, setIsScrollLocked] = useState(false);
const [isValidTitle, setIsValidTitle] = useState(true);
@@ -76,7 +80,9 @@ const EditRoomDialog = ({
currentParams.icon.uploadedFile === undefined)) ||
prevParams.icon.uploadedFile === currentParams.icon.uploadedFile) &&
prevParams.quota === currentParams.quota &&
- prevParams.indexing === currentParams.indexing
+ prevParams.indexing === currentParams.indexing &&
+ isEqual(prevParams.lifetime, currentParams.lifetime) &&
+ isEqualWatermarkChanges()
);
};
@@ -131,6 +137,7 @@ const EditRoomDialog = ({
visible={visible}
onClose={onCloseAction}
isScrollLocked={isScrollLocked}
+ isLoading={isInitLoading}
withFooterBorder
>
@@ -182,4 +189,10 @@ const EditRoomDialog = ({
);
};
-export default EditRoomDialog;
+export default inject(({ createEditRoomStore }) => {
+ const { isEqualWatermarkChanges } = createEditRoomStore;
+
+ return {
+ isEqualWatermarkChanges,
+ };
+})(observer(EditRoomDialog));
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/FileLifetime.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/FileLifetime.js
index fe4431de0a..f7f2d8f3bf 100644
--- a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/FileLifetime.js
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/FileLifetime.js
@@ -1,5 +1,6 @@
-import React, { useState } from "react";
+import { useState, useEffect } from "react";
import styled from "styled-components";
+import { capitalize } from "lodash";
import { Text } from "@docspace/shared/components/text";
import { TextInput } from "@docspace/shared/components/text-input";
import { ComboBox } from "@docspace/shared/components/combobox";
@@ -44,22 +45,28 @@ const StyledFileLifetime = styled.div`
}
`;
-const FileLifetime = ({ t }) => {
+const FileLifetime = ({ t, roomParams, setRoomParams }) => {
+ const lifetime = roomParams.lifetime ?? {
+ value: 12,
+ deletePermanently: false,
+ period: 0,
+ };
+
const dateOptions = [
{
key: 1,
- label: t("Common:Days"),
- "data-type": 1,
+ label: capitalize(t("Common:Days")),
+ value: 0,
},
{
key: 2,
label: t("Common:Months"),
- "data-type": 2,
+ value: 1,
},
{
key: 3,
label: t("Common:Years"),
- "data-type": 3,
+ value: 2,
},
];
@@ -67,36 +74,66 @@ const FileLifetime = ({ t }) => {
{
key: 1,
label: t("Common:MoveToTrash"),
- "data-type": 1,
+ value: false,
},
{
key: 2,
label: t("Common:DeletePermanently"),
- "data-type": 2,
+ value: true,
},
];
- const [inputValue, setInputValue] = useState("");
- const [selectedDate, setSelectedDate] = useState(dateOptions[0]);
- const [selectedDelete, setSelectedDelete] = useState(deleteOptions[0]);
+ const selectedInputValue = lifetime.value + "";
+ const selectedDateOption = dateOptions.find(
+ (o) => o.value === lifetime.period,
+ );
+ const selectedDeleteOptions = lifetime.deletePermanently
+ ? deleteOptions[1]
+ : deleteOptions[0];
+
+ const [inputValue, setInputValue] = useState(selectedInputValue);
+ const [selectedDate, setSelectedDate] = useState(selectedDateOption);
+ const [selectedDelete, setSelectedDelete] = useState(selectedDeleteOptions);
+
+ useEffect(() => {
+ if (!roomParams.lifetime) {
+ setRoomParams({
+ ...roomParams,
+ lifetime,
+ });
+ }
+ }, [roomParams.lifetime]);
const onChange = (e) => {
// /^(?:[1-9][0-9]*|0)$/
if (e.target.value && !/^(?:[1-9][0-9]*)$/.test(e.target.value)) return;
setInputValue(e.target.value);
+
+ setRoomParams({
+ ...roomParams,
+ lifetime: { ...lifetime, value: +e.target.value },
+ });
};
const isLoading = false;
const onSelectDate = (option) => {
setSelectedDate(option);
- console.log("onDateSelect", option);
+
+ setRoomParams({
+ ...roomParams,
+ lifetime: { ...lifetime, period: option.value },
+ });
};
const onSelectDelete = (option) => {
setSelectedDelete(option);
- console.log("onSelectDelete", option);
+
+ setRoomParams({
+ ...roomParams,
+ lifetime: { ...lifetime, deletePermanently: option.value },
+ });
};
return (
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/SetRoomParams.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/SetRoomParams.js
index e8d34001d3..aff841a6da 100644
--- a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/SetRoomParams.js
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/SetRoomParams.js
@@ -223,6 +223,7 @@ const SetRoomParams = ({
t={t}
roomParams={roomParams}
setRoomParams={setRoomParams}
+ isEdit={isEdit}
/>
)}
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/VirtualDataRoomBlock.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/VirtualDataRoomBlock.js
index 94033d48a9..4c833ddb1d 100644
--- a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/VirtualDataRoomBlock.js
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/VirtualDataRoomBlock.js
@@ -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,18 +68,20 @@ const Block = ({
);
};
-const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
+const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams, isEdit }) => {
const role = t("Translations:RoleViewer");
- const [fileLifetimeChecked, setFileLifetimeChecked] = useState(false);
+ const [fileLifetimeChecked, setFileLifetimeChecked] = useState(
+ !!roomParams?.lifetime,
+ );
const [copyAndDownloadChecked, setCopyAndDownloadChecked] = useState(false);
- const [watermarksChecked, setWatermarksChecked] = useState(false);
const onChangeAutomaticIndexing = () => {
setRoomParams({ ...roomParams, indexing: !roomParams.indexing });
};
const onChangeFileLifetime = () => {
+ if (fileLifetimeChecked) setRoomParams({ ...roomParams, lifetime: null });
setFileLifetimeChecked(!fileLifetimeChecked);
};
@@ -83,10 +89,6 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
setCopyAndDownloadChecked(!copyAndDownloadChecked);
};
- const onChangeAddWatermarksToDocuments = () => {
- setWatermarksChecked(!watermarksChecked);
- };
-
return (
{
isDisabled={false}
isChecked={fileLifetimeChecked}
>
-
+
{
isDisabled={false}
isChecked={copyAndDownloadChecked}
>
-
+
+
);
};
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ImageWatermark.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ImageWatermark.js
new file mode 100644
index 0000000000..178f12b03c
--- /dev/null
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ImageWatermark.js
@@ -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 (
+ onRotateChange(item)}
+ >
+ {item.label}°
+
+ );
+ });
+
+ return {items}
;
+ };
+
+ const scaleItems = () => {
+ const items = scaleOptions.map((item) => {
+ return (
+ onScaleChange(item)}
+ >
+ {item.label}%
+
+ );
+ });
+
+ return {items}
;
+ };
+
+ // const onSelectFile = (fileInfo) => {
+ // setWatermarks({ image: fileInfo });
+ // };
+
+ console.log("selectedRotate", selectedRotate.key, selectedScale.key);
+ return (
+
+ {!selectedImageUrl && (
+
+ )}
+
+ {/* */}
+
+ {selectedImageUrl && (
+
+
+
+
+ {t("WatermarkPreview")}
+
+ {t("WatermarkPreviewHelp")}
+ }
+ offsetRight={0}
+ className="settings_unavailable"
+ />
+
+
+
+
+
+
+
+
+
+
+ {t("Scale")}
+
+
+ {selectedScale.label}%
+
+
+
+
+ {t("Rotate")}
+
+
+
+ {selectedRotate.label}°
+
+
+
+
+ )}
+
+ );
+};
+
+export default inject(({ createEditRoomStore }) => {
+ const { setWatermarks, initialWatermarksSettings, watermarksSettings } =
+ createEditRoomStore;
+
+ const { imageUrl } = watermarksSettings;
+
+ return {
+ setWatermarks,
+ initialWatermarksSettings,
+ imageUrl,
+ };
+})(observer(ImageWatermark));
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/StyledComponent.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/StyledComponent.js
new file mode 100644
index 0000000000..3d80b6f43a
--- /dev/null
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/StyledComponent.js
@@ -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 };
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ViewerInfo.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ViewerInfo.js
new file mode 100644
index 0000000000..7cf56facd5
--- /dev/null
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/ViewerInfo.js
@@ -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 (
+
+
+ {t("AddWatermarkElements")}
+
+
+ item.index)}
+ onSelect={onSelect}
+ type={TabsTypes.Secondary}
+ multiple
+ />
+
+
+ {t("AddStaticText")}
+
+
+
+
+ {t("Position")}
+
+
+
+ );
+};
+
+export default inject(({ createEditRoomStore }) => {
+ const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
+
+ return {
+ setWatermarks,
+ initialWatermarksSettings,
+ };
+})(observer(ViewerInfoWatermark));
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/WatermarkBlock.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/WatermarkBlock.js
new file mode 100644
index 0000000000..49acda1955
--- /dev/null
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/WatermarkBlock.js
@@ -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 (
+
+
+
+ );
+};
+
+export default inject(({ createEditRoomStore }) => {
+ const { setWatermarks, initialWatermarksSettings, resetWatermarks } =
+ createEditRoomStore;
+
+ return {
+ setWatermarks,
+ isWatermarks: initialWatermarksSettings?.enabled,
+ resetWatermarks,
+ };
+})(observer(WatermarkBlock));
diff --git a/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/index.js b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/index.js
new file mode 100644
index 0000000000..d1a42f5a38
--- /dev/null
+++ b/packages/client/src/components/dialogs/CreateEditRoomDialog/sub-components/Watermarks/index.js
@@ -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 (
+
+
+
+ {type === imageWatermark ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+export default inject(({ createEditRoomStore }) => {
+ const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
+ return {
+ setWatermarks,
+ initialWatermarksSettings,
+ };
+})(observer(Watermarks));
diff --git a/packages/client/src/components/dialogs/CreateRoomConfirmDialog/index.js b/packages/client/src/components/dialogs/CreateRoomConfirmDialog/index.js
index 2cc3368462..27fe736cf5 100644
--- a/packages/client/src/components/dialogs/CreateRoomConfirmDialog/index.js
+++ b/packages/client/src/components/dialogs/CreateRoomConfirmDialog/index.js
@@ -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 (
{t("Common:Warning")}
-
- {t("CreateEditRoomDialog:CreateRoomConfirmation")}
-
+ {bodyText}
+ getCorrectFourValuesStyle("0 -24px 0 -16px", theme.interfaceDirection)};
+ padding: ${({ theme }) =>
+ getCorrectFourValuesStyle("0 0 0 16px", theme.interfaceDirection)};
+ }
+
+ .modal-dialog-aside-footer {
+ @media ${tablet} {
+ width: 100%;
+
+ ${({ theme }) =>
+ theme.interfaceDirection === "rtl"
+ ? `padding-left: 32px;`
+ : `padding-right: 32px;`}
+ display: flex;
+ }
+ }
+
+ .button-dialog-accept {
+ @media ${tablet} {
+ width: 100%;
+ }
+ }
+
+ .button-dialog {
+ @media ${tablet} {
+ width: 100%;
+ }
+
+ display: inline-block;
+ }
+`;
+
+export { StyledLifetimeDialog };
diff --git a/packages/client/src/components/dialogs/LifetimeDialog/index.js b/packages/client/src/components/dialogs/LifetimeDialog/index.js
new file mode 100644
index 0000000000..3c7aa633ba
--- /dev/null
+++ b/packages/client/src/components/dialogs/LifetimeDialog/index.js
@@ -0,0 +1,91 @@
+import { useEffect } from "react";
+import { ModalDialog } from "@docspace/shared/components/modal-dialog";
+import { StyledLifetimeDialog } from "./StyledLifetimeDialog";
+import { Button } from "@docspace/shared/components/button";
+import { Text } from "@docspace/shared/components/text";
+import { withTranslation } from "react-i18next";
+import { inject, observer } from "mobx-react";
+
+const LifetimeDialogComponent = (props) => {
+ const { t, setLifetimeDialogVisible, visible, tReady } = 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 onClose = () => {
+ setLifetimeDialogVisible(false);
+ };
+
+ const onAcceptClick = () => {
+ console.log("onAcceptClick");
+ onClose();
+ };
+
+ const onDeleteAction = () => {
+ onAcceptClick();
+ };
+
+ return (
+
+ {t("Common:Warning")}
+
+
+
+ {t("Files:LifetimeDialogDescriptionHeader")}
+
+
+ {t("Files:LifetimeDialogDescription")}
+
+
+
+
+
+
+
+
+ );
+};
+
+const LifetimeDialog = withTranslation(["Common", "Files"])(
+ LifetimeDialogComponent,
+);
+
+export default inject(({ dialogsStore }) => {
+ const { lifetimeDialogVisible: visible, setLifetimeDialogVisible } =
+ dialogsStore;
+
+ return {
+ visible,
+ setLifetimeDialogVisible,
+ };
+})(observer(LifetimeDialog));
diff --git a/packages/client/src/helpers/toast-helpers.tsx b/packages/client/src/helpers/toast-helpers.tsx
new file mode 100644
index 0000000000..3f3b724a7d
--- /dev/null
+++ b/packages/client/src/helpers/toast-helpers.tsx
@@ -0,0 +1,57 @@
+// (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 { TFunction } from "i18next";
+
+import { Link, LinkTarget } from "@docspace/shared/components/link";
+import { Text } from "@docspace/shared/components/text";
+import { toastr } from "@docspace/shared/components/toast";
+
+export const showSuccessExportRoomIndexToast = (
+ t: TFunction,
+ fileName: string,
+ fileUrl: string,
+ openOnNewPage: boolean,
+) => {
+ const toastMessage = (
+ <>
+
+ {fileName}
+
+
+
+ {t("Files:FileExportedToMyDocuments")}
+
+ >
+ );
+
+ toastr.success(toastMessage);
+};
diff --git a/packages/client/src/pages/Home/InfoPanel/Body/helpers/DetailsHelper.js b/packages/client/src/pages/Home/InfoPanel/Body/helpers/DetailsHelper.js
index 65dc811baa..8ce195da34 100644
--- a/packages/client/src/pages/Home/InfoPanel/Body/helpers/DetailsHelper.js
+++ b/packages/client/src/pages/Home/InfoPanel/Body/helpers/DetailsHelper.js
@@ -144,6 +144,8 @@ class DetailsHelper {
return "info_details_comments";
case "Tags":
return "info_details_tags";
+ case "Lifetime ends":
+ return "info_details_lifetime";
case "Storage":
return "info_details_storage";
}
@@ -183,6 +185,7 @@ class DetailsHelper {
"Date modified",
"Last modified by",
"Creation date",
+ this.item.expired && "Lifetime ends",
"Versions",
this.item.order && "Index",
"Comments",
@@ -216,6 +219,8 @@ class DetailsHelper {
return this.t("LastModifiedBy");
case "Creation date":
return this.t("CreationDate");
+ case "Lifetime ends":
+ return this.t("LifetimeEnds");
case "Index":
return this.t("Files:Index");
@@ -269,6 +274,8 @@ class DetailsHelper {
return this.getAuthorDecoration("updatedBy");
case "Creation date":
return this.getItemCreationDate();
+ case "Lifetime ends":
+ return this.getItemExpiredDate();
case "Versions":
return this.getItemVersions();
@@ -350,6 +357,10 @@ class DetailsHelper {
return text(parseAndFormatDate(this.item.created, this.culture));
};
+ getItemExpiredDate = () => {
+ return text(parseAndFormatDate(this.item.expired, this.culture));
+ };
+
getItemVersions = () => {
return text(this.item.version);
};
diff --git a/packages/client/src/pages/Home/Section/Header/index.js b/packages/client/src/pages/Home/Section/Header/index.js
index 26b33a7478..9e8c8bc353 100644
--- a/packages/client/src/pages/Home/Section/Header/index.js
+++ b/packages/client/src/pages/Home/Section/Header/index.js
@@ -25,6 +25,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import PublicRoomIconUrl from "PUBLIC_DIR/images/public-room.react.svg?url";
+import LifetimeRoomIconUrl from "PUBLIC_DIR/images/lifetime-room.react.svg?url";
import React from "react";
import { inject, observer } from "mobx-react";
@@ -51,6 +52,7 @@ import {
} from "SRC_DIR/helpers/utils";
import TariffBar from "SRC_DIR/components/TariffBar";
import IndexMenu from "../IndexHeader";
+import { getLifetimePeriodTranslation } from "@docspace/shared/utils/common";
const StyledContainer = styled.div`
width: 100%;
@@ -146,6 +148,21 @@ const StyledContainer = styled.div`
}
}
}
+
+ ${(props) =>
+ props.isVirtualDataRoomType &&
+ css`
+ .title-icon {
+ svg {
+ path {
+ fill: ${({ theme }) =>
+ theme.navigation.lifetimeIconFill} !important;
+ stroke: ${({ theme }) =>
+ theme.navigation.lifetimeIconStroke} !important;
+ }
+ }
+ }
+ `}
`;
const SectionHeaderContent = (props) => {
@@ -214,6 +231,8 @@ const SectionHeaderContent = (props) => {
isPublicRoom,
theme,
isPublicRoomType,
+ isVirtualDataRoomType,
+
moveToPublicRoom,
currentDeviceType,
isFrame,
@@ -485,6 +504,17 @@ const SectionHeaderContent = (props) => {
const logo = getLogoUrl(WhiteLabelLogoType.LightSmall, !theme.isBase);
const burgerLogo = getLogoUrl(WhiteLabelLogoType.LeftMenu, !theme.isBase);
+ const titleIcon =
+ (isPublicRoomType && !isPublicRoom && PublicRoomIconUrl) ||
+ (isVirtualDataRoomType && selectedFolder.lifetime && LifetimeRoomIconUrl);
+
+ const titleIconTooltip = selectedFolder.lifetime
+ ? t("Files:RoomFilesLifetime", {
+ days: selectedFolder.lifetime.value,
+ period: getLifetimePeriodTranslation(selectedFolder.lifetime.period, t),
+ })
+ : null;
+
const navigationButtonLabel = showNavigationButton
? t("Files:ShareRoom")
: null;
@@ -492,7 +522,10 @@ const SectionHeaderContent = (props) => {
return (
{(context) => (
-
+
{tableGroupMenuVisible ? (
) : isIndexEditingMode ? (
@@ -547,9 +580,8 @@ const SectionHeaderContent = (props) => {
withLogo={isPublicRoom && logo}
burgerLogo={isPublicRoom && burgerLogo}
isPublicRoom={isPublicRoom}
- titleIcon={
- currentIsPublicRoomType && !isPublicRoom && PublicRoomIconUrl
- }
+ titleIcon={titleIcon}
+ titleIconTooltip={titleIconTooltip}
showRootFolderTitle={insideTheRoom || isInsideGroup}
currentDeviceType={currentDeviceType}
isFrame={isFrame}
@@ -681,6 +713,7 @@ export default inject(
const isRoom = !!roomType;
const isPublicRoomType = roomType === RoomsType.PublicRoom;
+ const isVirtualDataRoomType = roomType === RoomsType.VirtualDataRoom;
const isCustomRoomType = roomType === RoomsType.CustomRoom;
const isFormRoomType = roomType === RoomsType.FormRoom;
@@ -792,6 +825,7 @@ export default inject(
moveToRoomsPage,
onClickBack,
isPublicRoomType,
+ isVirtualDataRoomType,
isPublicRoom,
moveToPublicRoom,
diff --git a/packages/client/src/store/ContextOptionsStore.js b/packages/client/src/store/ContextOptionsStore.js
index 74f3c66428..b0aadbe653 100644
--- a/packages/client/src/store/ContextOptionsStore.js
+++ b/packages/client/src/store/ContextOptionsStore.js
@@ -81,6 +81,7 @@ import ActionsUploadReactSvgUrl from "PUBLIC_DIR/images/actions.upload.react.svg
import PluginMoreReactSvgUrl from "PUBLIC_DIR/images/plugin.more.react.svg?url";
import CodeReactSvgUrl from "PUBLIC_DIR/images/code.react.svg?url";
import ClearTrashReactSvgUrl from "PUBLIC_DIR/images/clear.trash.react.svg?url";
+import ExportRoomIndexSvgUrl from "PUBLIC_DIR/images/icons/16/export-room-index.react.svg?url";
import { getCategoryUrl } from "@docspace/client/src/helpers/utils";
@@ -917,6 +918,10 @@ class ContextOptionsStore {
this.filesActionsStore.setMuteAction(action, item, t);
};
+ onExportRoomIndex = (t, roomId) => {
+ this.filesActionsStore.exportRoomIndex(t, roomId);
+ };
+
onEditIndex = () => {
this.indexingStore.setIsIndexEditingMode(true);
};
@@ -1398,6 +1403,7 @@ class ContextOptionsStore {
item.roomType === RoomsType.PublicRoom ||
item.roomType === RoomsType.FormRoom ||
item.roomType === RoomsType.CustomRoom;
+ const isVDRRoomType = item.roomType === RoomsType.VirtualDataRoom;
const { shared, navigationPath } = this.selectedFolderStore;
@@ -1601,6 +1607,14 @@ class ContextOptionsStore {
},
...pinOptions,
...muteOptions,
+ {
+ id: "option_export-room-index",
+ key: "export-room-index",
+ label: t("Files:ExportRoomIndex"),
+ icon: ExportRoomIndexSvgUrl,
+ onClick: () => this.onExportRoomIndex(t, item.id),
+ disabled: !isVDRRoomType || !item.indexing,
+ },
{
id: "option_owner-change",
key: "owner-change",
diff --git a/packages/client/src/store/CreateEditRoomStore.js b/packages/client/src/store/CreateEditRoomStore.js
index 664fa66a8b..187125c949 100644
--- a/packages/client/src/store/CreateEditRoomStore.js
+++ b/packages/client/src/store/CreateEditRoomStore.js
@@ -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;
@@ -126,6 +239,7 @@ class CreateEditRoomStore {
roomType: roomParams.type,
title: roomParams.title || t("Common:NewRoom"),
indexing: roomParams.indexing,
+ lifetime: roomParams.lifetime,
createAsNewFolder: roomParams.createAsNewFolder ?? true,
...(quotaLimit && {
quota: +quotaLimit,
@@ -157,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]));
@@ -175,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 {
diff --git a/packages/client/src/store/DialogsStore.js b/packages/client/src/store/DialogsStore.js
index aba3c4f375..867751a467 100644
--- a/packages/client/src/store/DialogsStore.js
+++ b/packages/client/src/store/DialogsStore.js
@@ -49,6 +49,7 @@ class DialogsStore {
connectDialogVisible = false;
thirdPartyMoveDialogVisible = false;
deleteDialogVisible = false;
+ lifetimeDialogVisible = false;
downloadDialogVisible = false;
emptyTrashDialogVisible = false;
newFilesPanelVisible = false;
@@ -233,6 +234,10 @@ class DialogsStore {
this.deleteDialogVisible = deleteDialogVisible;
};
+ setLifetimeDialogVisible = (lifetimeDialogVisible) => {
+ this.lifetimeDialogVisible = lifetimeDialogVisible;
+ };
+
setEventDialogVisible = (eventDialogVisible) => {
this.eventDialogVisible = eventDialogVisible;
};
diff --git a/packages/client/src/store/FilesActionsStore.js b/packages/client/src/store/FilesActionsStore.js
index 7b26ab4a1a..6603fe0bd3 100644
--- a/packages/client/src/store/FilesActionsStore.js
+++ b/packages/client/src/store/FilesActionsStore.js
@@ -60,6 +60,7 @@ import {
import {
ConflictResolveType,
Events,
+ ExportRoomIndexTaskStatus,
FileAction,
FileStatus,
FolderType,
@@ -90,6 +91,9 @@ import {
} from "SRC_DIR/helpers/utils";
import { MEDIA_VIEW_URL } from "@docspace/shared/constants";
import { openingNewTab } from "@docspace/shared/utils/openingNewTab";
+import { changeRoomLifetime } from "@docspace/shared/api/rooms";
+import api from "@docspace/shared/api";
+import { showSuccessExportRoomIndexToast } from "SRC_DIR/helpers/toast-helpers";
class FilesActionStore {
settingsStore;
@@ -114,6 +118,7 @@ class FilesActionStore {
isGroupMenuBlocked = false;
emptyTrashInProgress = false;
processCreatingRoomFromData = false;
+ alreadyExportingRoomIndex = false;
constructor(
settingsStore,
@@ -2724,6 +2729,10 @@ class FilesActionStore {
await refreshFiles();
};
+ changeRoomLifetime = (roomId, lifetime) => {
+ return changeRoomLifetime(roomId, lifetime);
+ };
+
copyFromTemplateForm = async (fileInfo, t) => {
const selectedItemId = this.selectedFolderStore.id;
const fileIds = [fileInfo.id];
@@ -2819,6 +2828,118 @@ class FilesActionStore {
toastr.error(t("Files:ErrorChangeIndex"));
}
};
+
+ checkExportRoomIndexProgress = async () => {
+ return await new Promise((resolve, reject) => {
+ setTimeout(async () => {
+ try {
+ const res = await api.rooms.getExportRoomIndexProgress();
+
+ resolve(res);
+ } catch (e) {
+ reject(e);
+ }
+ }, 1000);
+ });
+ };
+
+ loopExportRoomIndexStatusChecking = async (pbData) => {
+ const { setSecondaryProgressBarData } =
+ this.uploadDataStore.secondaryProgressDataStore;
+
+ let isCompleted = false;
+ let res;
+
+ while (!isCompleted) {
+ res = await this.checkExportRoomIndexProgress();
+
+ if (res?.isCompleted) {
+ isCompleted = true;
+ }
+
+ if (res?.percentage) {
+ setSecondaryProgressBarData({
+ icon: pbData.icon,
+ visible: true,
+ percent: res.percentage,
+ label: "",
+ alert: false,
+ operationId: pbData.operationId,
+ });
+ }
+ }
+
+ return res;
+ };
+
+ checkPreviousExportRoomIndexInProgress = async () => {
+ try {
+ if (this.alreadyExportingRoomIndex) {
+ return true;
+ }
+
+ const previousExport = await api.rooms.getExportRoomIndexProgress();
+
+ return previousExport && !previousExport.isCompleted;
+ } catch (e) {
+ toastr.error(e);
+ }
+ };
+
+ onSuccessExportRoomIndex = (t, fileName, fileUrl) => {
+ const { openOnNewPage } = this.filesSettingsStore;
+ const urlWithProxy = combineUrl(window.ClientConfig?.proxy?.url, fileUrl);
+
+ showSuccessExportRoomIndexToast(t, fileName, urlWithProxy, openOnNewPage);
+ };
+
+ exportRoomIndex = async (t, roomId) => {
+ const previousExportInProgress =
+ await this.checkPreviousExportRoomIndexInProgress();
+
+ if (previousExportInProgress) {
+ return toastr.error(t("Files:ExportRoomIndexAlreadyInProgressError"));
+ }
+
+ const { setSecondaryProgressBarData, clearSecondaryProgressData } =
+ this.uploadDataStore.secondaryProgressDataStore;
+
+ const pbData = { icon: "exportIndex", operationId: uniqueid("operation_") };
+
+ setSecondaryProgressBarData({
+ icon: pbData.icon,
+ visible: true,
+ percent: 0,
+ label: "",
+ alert: false,
+ operationId: pbData.operationId,
+ });
+
+ this.alreadyExportingRoomIndex = true;
+
+ try {
+ let res = await api.rooms.exportRoomIndex(roomId);
+
+ if (!res.isCompleted) {
+ res = await this.loopExportRoomIndexStatusChecking(pbData);
+ }
+
+ if (res.status === ExportRoomIndexTaskStatus.Failed) {
+ toastr.error(res.error);
+ return;
+ }
+
+ if (res.status === ExportRoomIndexTaskStatus.Completed) {
+ this.onSuccessExportRoomIndex(t, res.resultFileName, res.resultFileUrl);
+ }
+ } catch (e) {
+ toastr.error(e);
+ } finally {
+ this.alreadyExportingRoomIndex = false;
+
+ setTimeout(() => clearSecondaryProgressData(pbData.operationId), TIMEOUT);
+ }
+ };
}
export default FilesActionStore;
diff --git a/packages/client/src/store/FilesStore.js b/packages/client/src/store/FilesStore.js
index f30205e89a..146922fcfd 100644
--- a/packages/client/src/store/FilesStore.js
+++ b/packages/client/src/store/FilesStore.js
@@ -2394,6 +2394,7 @@ class FilesStore {
"unpin-room",
"mute-room",
"unmute-room",
+ "export-room-index",
"separator1",
"download",
"archive-room",
@@ -3247,6 +3248,7 @@ class FilesStore {
inRoom,
requestToken,
indexing,
+ lifetime,
lastOpened,
quotaLimit,
usedSpace,
@@ -3255,6 +3257,7 @@ class FilesStore {
order,
startFilling,
draftLocation,
+ expired,
} = item;
const thirdPartyIcon = this.thirdPartyStore.getThirdPartyIcon(
@@ -3417,6 +3420,7 @@ class FilesStore {
...pluginOptions,
inRoom,
indexing,
+ lifetime,
type,
hasDraft,
isForm,
@@ -3430,6 +3434,7 @@ class FilesStore {
order,
startFilling,
draftLocation,
+ expired,
};
});
};
diff --git a/packages/client/src/store/SelectedFolderStore.ts b/packages/client/src/store/SelectedFolderStore.ts
index 75c7953aec..4f4775ae9c 100644
--- a/packages/client/src/store/SelectedFolderStore.ts
+++ b/packages/client/src/store/SelectedFolderStore.ts
@@ -40,7 +40,11 @@ import type {
TPathParts,
} from "@docspace/shared/types";
import { TFolder, TFolderSecurity } from "@docspace/shared/api/files/types";
-import { TLogo, TRoomSecurity } from "@docspace/shared/api/rooms/types";
+import {
+ TLogo,
+ TRoomLifetime,
+ TRoomSecurity,
+} from "@docspace/shared/api/rooms/types";
import { setDocumentTitle } from "../helpers/utils";
@@ -139,8 +143,12 @@ class SelectedFolderStore {
canShare = false;
+ indexing = false;
+
parentRoomType: Nullable = null;
+ lifetime: TRoomLifetime | null = null;
+
indexing = false;
constructor(settingsStore: SettingsStore) {
@@ -187,6 +195,7 @@ class SelectedFolderStore {
type: this.type,
isRootFolder: this.isRootFolder,
parentRoomType: this.parentRoomType,
+ lifetime: this.lifetime,
indexing: this.indexing,
};
};
@@ -233,6 +242,7 @@ class SelectedFolderStore {
this.type = null;
this.inRoom = false;
this.parentRoomType = null;
+ this.lifetime = null;
this.indexing = false;
};
@@ -256,9 +266,14 @@ class SelectedFolderStore {
this.shared = shared;
};
- updateEditedSelectedRoom = (title = this.title, tags = this.tags) => {
+ updateEditedSelectedRoom = (
+ title = this.title,
+ tags = this.tags,
+ lifetime = this.lifetime,
+ ) => {
this.title = title;
this.tags = tags;
+ this.lifetime = lifetime;
};
setInRoom = (inRoom: boolean) => {
diff --git a/packages/common/components/Watermarks/index.js b/packages/common/components/Watermarks/index.js
new file mode 100644
index 0000000000..9007ebfeb2
--- /dev/null
+++ b/packages/common/components/Watermarks/index.js
@@ -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 (
+
+ {children}
+
+ );
+};
+export default Watermark;
diff --git a/packages/shared/api/rooms/index.ts b/packages/shared/api/rooms/index.ts
index 3cef69b3b0..7232900938 100644
--- a/packages/shared/api/rooms/index.ts
+++ b/packages/shared/api/rooms/index.ts
@@ -35,7 +35,7 @@ import {
toUrlParams,
} from "../../utils/common";
import RoomsFilter from "./filter";
-import { TGetRooms } from "./types";
+import { TGetRooms, TRoomLifetime, TExportRoomIndexTask } from "./types";
export async function getRooms(filter: RoomsFilter, signal?: AbortSignal) {
let params;
@@ -478,3 +478,67 @@ export function resetRoomQuota(roomIds) {
return request(options);
}
+
+export function changeRoomLifetime(
+ roomId: string | number,
+ lifetime: TRoomLifetime | null,
+) {
+ const data = lifetime ? { ...lifetime } : null;
+ const options = {
+ method: "put",
+ url: `files/rooms/${roomId}/lifetime`,
+ data,
+ };
+
+ return request(options);
+}
+
+export function exportRoomIndex(roomId: number) {
+ return request({
+ method: "post",
+ url: `files/rooms/${roomId}/indexexport`,
+ }) as Promise;
+}
+
+export function getExportRoomIndexProgress() {
+ return request({
+ method: "get",
+ url: `files/rooms/indexexport`,
+ }) as Promise;
+}
+
+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`,
+ });
+}
diff --git a/packages/shared/api/rooms/types.ts b/packages/shared/api/rooms/types.ts
index 5c442a6d33..6a481769ff 100644
--- a/packages/shared/api/rooms/types.ts
+++ b/packages/shared/api/rooms/types.ts
@@ -25,7 +25,12 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { TFile, TFolder } from "../files/types";
-import { FolderType, RoomsType, ShareAccessRights } from "../../enums";
+import {
+ ExportRoomIndexTaskStatus,
+ FolderType,
+ RoomsType,
+ ShareAccessRights,
+} from "../../enums";
import { TCreatedBy, TPathParts } from "../../types";
export type TLogo = {
@@ -54,6 +59,12 @@ export type TRoomSecurity = {
CopySharedLink: boolean;
};
+export type TRoomLifetime = {
+ deletePermanently: boolean;
+ period: number;
+ value: number;
+};
+
export type TRoom = {
parentId: number;
filesCount: number;
@@ -79,6 +90,7 @@ export type TRoom = {
updatedBy: TCreatedBy;
isArchive?: boolean;
security: TRoomSecurity;
+ lifetime: TRoomLifetime;
};
export type TGetRooms = {
@@ -91,3 +103,14 @@ export type TGetRooms = {
total: number;
new: number;
};
+
+export type TExportRoomIndexTask = {
+ id: string;
+ error: string;
+ percentage: number;
+ isCompleted: boolean;
+ status: ExportRoomIndexTaskStatus;
+ resultFileId: number;
+ resultFileName: string;
+ resultFileUrl: string;
+};
diff --git a/packages/shared/components/floating-button/FloatingButton.enums.ts b/packages/shared/components/floating-button/FloatingButton.enums.ts
index b6fd35fb67..edee0c7420 100644
--- a/packages/shared/components/floating-button/FloatingButton.enums.ts
+++ b/packages/shared/components/floating-button/FloatingButton.enums.ts
@@ -33,4 +33,5 @@ export const enum FloatingButtonIcons {
plus = "plus",
minus = "minus",
refresh = "refresh",
+ exportIndex = "exportIndex",
}
diff --git a/packages/shared/components/floating-button/FloatingButton.tsx b/packages/shared/components/floating-button/FloatingButton.tsx
index 72ae37ec25..eb6ef85e97 100644
--- a/packages/shared/components/floating-button/FloatingButton.tsx
+++ b/packages/shared/components/floating-button/FloatingButton.tsx
@@ -37,6 +37,7 @@ import ButtonPlusIcon from "PUBLIC_DIR/images/icons/16/button.plus.react.svg";
import ButtonMinusIcon from "PUBLIC_DIR/images/icons/16/button.minus.react.svg";
import RefreshIcon from "PUBLIC_DIR/images/refresh.react.svg";
import CloseIcon from "PUBLIC_DIR/images/close-icon.react.svg";
+import ExportRoomIndexIcon from "PUBLIC_DIR/images/icons/16/export-room-index.react.svg";
import { FloatingButtonTheme } from "./FloatingButton.theme";
@@ -67,6 +68,7 @@ const icons = {
minus: ,
refresh: ,
duplicate: ,
+ exportIndex: ,
};
const FloatingButton = (props: FloatingButtonProps) => {
diff --git a/packages/shared/components/image-editor/ButtonDelete/index.tsx b/packages/shared/components/image-editor/ButtonDelete/index.tsx
new file mode 100644
index 0000000000..a8d2b7e569
--- /dev/null
+++ b/packages/shared/components/image-editor/ButtonDelete/index.tsx
@@ -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 (
+
+
+
+ {t("Common:Delete")}
+
+
+ );
+};
+
+export default ButtonDelete;
diff --git a/packages/shared/components/image-editor/Dropzone/index.tsx b/packages/shared/components/image-editor/Dropzone/index.tsx
index b2dd4225a3..b318f048d3 100644
--- a/packages/shared/components/image-editor/Dropzone/index.tsx
+++ b/packages/shared/components/image-editor/Dropzone/index.tsx
@@ -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 {
- 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);
diff --git a/packages/shared/components/image-editor/ImageCropper/index.tsx b/packages/shared/components/image-editor/ImageCropper/index.tsx
index b520d4a65f..11ffbc7172 100644
--- a/packages/shared/components/image-editor/ImageCropper/index.tsx
+++ b/packages/shared/components/image-editor/ImageCropper/index.tsx
@@ -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"
/>
-
-
-
- {t("Common:Delete")}
-
-
+
+
{typeof uploadedFile !== "string" &&
uploadedFile?.name &&
diff --git a/packages/shared/components/image-editor/ImageEditor.styled.ts b/packages/shared/components/image-editor/ImageEditor.styled.ts
index e65a20f69e..9f858ae972 100644
--- a/packages/shared/components/image-editor/ImageEditor.styled.ts
+++ b/packages/shared/components/image-editor/ImageEditor.styled.ts
@@ -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;
diff --git a/packages/shared/components/image-editor/index.tsx b/packages/shared/components/image-editor/index.tsx
index 653b685625..04ef188479 100644
--- a/packages/shared/components/image-editor/index.tsx
+++ b/packages/shared/components/image-editor/index.tsx
@@ -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 };
diff --git a/packages/shared/components/navigation/Navigation.tsx b/packages/shared/components/navigation/Navigation.tsx
index 507d1361c6..917dd791de 100644
--- a/packages/shared/components/navigation/Navigation.tsx
+++ b/packages/shared/components/navigation/Navigation.tsx
@@ -26,19 +26,20 @@
import React, { useCallback } from "react";
import { ReactSVG } from "react-svg";
+import NavigationText from "./sub-components/Text";
import { Consumer, DomHelpers } from "../../utils";
-import { DeviceType } from "../../enums";
-
import { Backdrop } from "../backdrop";
import ArrowButton from "./sub-components/ArrowBtn";
-import Text from "./sub-components/Text";
import ControlButtons from "./sub-components/ControlBtn";
import ToggleInfoPanelButton from "./sub-components/ToggleInfoPanelBtn";
import NavigationLogo from "./sub-components/LogoBlock";
import DropBox from "./sub-components/DropBox";
+import { Tooltip } from "../tooltip";
+import { Text } from "../text";
+import { DeviceType } from "../../enums";
import { StyledContainer } from "./Navigation.styled";
import { INavigationProps } from "./Navigation.types";
@@ -75,6 +76,7 @@ const Navigation = ({
burgerLogo,
isPublicRoom,
titleIcon,
+ titleIconTooltip,
currentDeviceType,
rootRoomTitle,
showTitle,
@@ -168,13 +170,35 @@ const Navigation = ({
((navigationItems && navigationItems.length > 1) || rootRoomTitle) &&
currentDeviceType !== DeviceType.mobile;
+ const getContent = () => (
+
+ {titleIconTooltip}
+
+ );
+
const navigationTitleNode = (
- {titleIcon &&
}
-
+ )}
+
+ {titleIconTooltip && (
+
+ )}
+
+
-
>
)}
diff --git a/packages/shared/components/navigation/Navigation.types.ts b/packages/shared/components/navigation/Navigation.types.ts
index dcf25adc90..e95884e9bd 100644
--- a/packages/shared/components/navigation/Navigation.types.ts
+++ b/packages/shared/components/navigation/Navigation.types.ts
@@ -203,5 +203,6 @@ export interface INavigationProps {
onNavigationButtonClick?: () => void;
tariffBar: React.ReactElement;
showNavigationButton: boolean;
+ titleIconTooltip?: string;
onContextOptionsClick?: () => void;
}
diff --git a/packages/shared/components/selector/Selector.styled.ts b/packages/shared/components/selector/Selector.styled.ts
index 8a24e2ca1f..cb7508d7fd 100644
--- a/packages/shared/components/selector/Selector.styled.ts
+++ b/packages/shared/components/selector/Selector.styled.ts
@@ -269,6 +269,29 @@ const StyledItem = styled.div<{
}
}`}
`}
+
+ .selector-item_name {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+
+ .label {
+ width: unset;
+ }
+
+ svg {
+ path {
+ fill: ${({ theme }) => theme.navigation.lifetimeIconFill} !important;
+ stroke: ${({ theme }) =>
+ theme.navigation.lifetimeIconStroke} !important;
+ }
+ }
+ }
+
+ .title-icon {
+ cursor: pointer;
+ }
`;
const StyledEmptyScreen = styled.div<{ withSearch: boolean }>`
diff --git a/packages/shared/components/selector/Selector.types.ts b/packages/shared/components/selector/Selector.types.ts
index 81d0a9277d..ceb3196998 100644
--- a/packages/shared/components/selector/Selector.types.ts
+++ b/packages/shared/components/selector/Selector.types.ts
@@ -541,6 +541,7 @@ export type TSelectorItem = TSelectorItemType & {
isSelected?: boolean;
isDisabled?: boolean;
disabledText?: string;
+ lifetimeTooltip?: string | null;
};
export type Data = {
diff --git a/packages/shared/components/selector/sub-components/Item.tsx b/packages/shared/components/selector/sub-components/Item.tsx
index aa85bd36cd..f0b03c2d06 100644
--- a/packages/shared/components/selector/sub-components/Item.tsx
+++ b/packages/shared/components/selector/sub-components/Item.tsx
@@ -26,12 +26,15 @@
import React from "react";
import { useTranslation } from "react-i18next";
+import { ReactSVG } from "react-svg";
import Planet12ReactSvgUrl from "PUBLIC_DIR/images/icons/12/planet.react.svg?url";
+import LifetimeRoomIconUrl from "PUBLIC_DIR/images/lifetime-room.react.svg?url";
import { getUserTypeLabel } from "../../../utils/common";
import { Avatar, AvatarRole, AvatarSize } from "../../avatar";
import { Text } from "../../text";
import { Checkbox } from "../../checkbox";
import { RoomIcon } from "../../room-icon";
+import { Tooltip } from "../../tooltip";
import { StyledItem } from "../Selector.styled";
import { ItemProps, Data, TSelectorItem } from "../Selector.types";
@@ -106,6 +109,7 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
isGroup,
disabledText,
dropDownItems,
+ lifetimeTooltip,
} = item;
if (isInputItem) {
@@ -175,6 +179,12 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
onSelect?.(item, isDoubleClick);
};
+ const getContent = () => (
+
+ {lifetimeTooltip}
+
+ );
+
return (
{
{renderCustomItem ? (
renderCustomItem(label, typeLabel, email, isGroup)
) : (
-
- {label}
-
+
+
+ {label}
+
+
+ {lifetimeTooltip && (
+ <>
+
+
+ >
+ )}
+
)}
{isDisabled && disabledText ? (
diff --git a/packages/shared/components/tabs/Tabs.constants.ts b/packages/shared/components/tabs/Tabs.constants.ts
index 6209284596..b112296edc 100644
--- a/packages/shared/components/tabs/Tabs.constants.ts
+++ b/packages/shared/components/tabs/Tabs.constants.ts
@@ -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;
diff --git a/packages/shared/components/tabs/Tabs.styled.ts b/packages/shared/components/tabs/Tabs.styled.ts
index da5bf0a795..0355c91669 100644
--- a/packages/shared/components/tabs/Tabs.styled.ts
+++ b/packages/shared/components/tabs/Tabs.styled.ts
@@ -31,9 +31,14 @@ import { TabsTypes } from "./Tabs.enums";
export const StyledTabs = styled.div<{
stickyTop?: string;
+ multiple: boolean;
}>`
- display: flex;
- flex-direction: column;
+ ${(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) =>
diff --git a/packages/shared/components/tabs/Tabs.tsx b/packages/shared/components/tabs/Tabs.tsx
index e4fe6603a8..0a53f9f37a 100644
--- a/packages/shared/components/tabs/Tabs.tsx
+++ b/packages/shared/components/tabs/Tabs.tsx
@@ -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(
- items[selectedItemIndex],
- );
+ const [currentItem, setCurrentItem] = useState(selectedItemIndex);
+ const [multipleItems, setMultipleItems] = useState(selectedItems);
const tabsRef = useRef(null);
const scrollRef = useRef(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,43 +95,84 @@ const Tabs = (props: TabsProps) => {
};
const setSelectedItem = (selectedTabItem: TTabItem, index: number): void => {
- setCurrentItem(selectedTabItem);
- scrollToTab(index);
+ 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;
+ };
+
+ const updatedActiveTab = indexOperation();
+
+ setMultipleItems(updatedActiveTab);
+ onSelect?.(selectedTabItem);
+ return;
+ }
+
+ setCurrentItem(index);
onSelect?.(selectedTabItem);
+
+ scrollToTab(index);
};
+ const renderContent = (
+
+ {items.map((item, index) => {
+ const isActive = multiple
+ ? multipleItems.indexOf(index) !== -1
+ : index === currentItem;
+
+ return (
+ {
+ item.onClick?.();
+ setSelectedItem(item, index);
+ }}
+ >
+ {item.name}
+
+
+ );
+ })}
+
+ );
return (
-
-
- {!isViewFirstTab &&
}
-
-
- {items.map((item, index) => {
- const isActive = item.id === currentItem.id;
- return (
- {
- item.onClick?.();
- setSelectedItem(item, index);
- }}
- >
- {item.name}
-
-
- );
- })}
-
-
- {!isViewLastTab &&
}
-
+
+ {multiple && renderContent}
+ {!multiple && (
+
+ {!isViewFirstTab &&
}
+
+
+ {renderContent}
+
+
+ {!isViewLastTab &&
}
+
+ )}
-
- {currentItem?.content}
+ {!multiple && (
+ {items[currentItem]?.content}
+ )}
);
};
diff --git a/packages/shared/components/tabs/Tabs.types.ts b/packages/shared/components/tabs/Tabs.types.ts
index 45e33dc8ad..88a799b6c2 100644
--- a/packages/shared/components/tabs/Tabs.types.ts
+++ b/packages/shared/components/tabs/Tabs.types.ts
@@ -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;
}
diff --git a/packages/shared/enums/index.ts b/packages/shared/enums/index.ts
index d61f09109b..90f7c864df 100644
--- a/packages/shared/enums/index.ts
+++ b/packages/shared/enums/index.ts
@@ -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,
@@ -585,3 +597,11 @@ export const enum LDAPCertificateProblem {
CertUntrustedCa = -2146762478,
CertUnrecognizedError = -2146762477,
}
+
+export const enum ExportRoomIndexTaskStatus {
+ Created = 0,
+ Running = 1,
+ Completed = 2,
+ Canceled = 3,
+ Failed = 4,
+}
diff --git a/packages/shared/selectors/Files/FilesSelector.utils.ts b/packages/shared/selectors/Files/FilesSelector.utils.ts
index e8c389778a..c96b14d319 100644
--- a/packages/shared/selectors/Files/FilesSelector.utils.ts
+++ b/packages/shared/selectors/Files/FilesSelector.utils.ts
@@ -27,10 +27,14 @@
import { TSelectorItem } from "../../components/selector";
import { TFile, TFolder } from "../../api/files/types";
import { TRoom } from "../../api/rooms/types";
-import { getIconPathByFolderType } from "../../utils/common";
+import {
+ getIconPathByFolderType,
+ getLifetimePeriodTranslation,
+} from "../../utils/common";
import { iconSize32 } from "../../utils/image-helpers";
import { DEFAULT_FILE_EXTS } from "./FilesSelector.constants";
import { getTitleWithoutExtension } from "../../utils";
+import { TTranslation } from "../../types";
const isDisableFolder = (
folder: TFolder,
@@ -98,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);
@@ -113,14 +118,16 @@ export const convertFilesToItems: (
rootFolderType,
isDisabled: !filterParam,
fileExst,
+ viewUrl,
};
});
return items;
};
-export const convertRoomsToItems: (rooms: TRoom[]) => TSelectorItem[] = (
+export const convertRoomsToItems: (
rooms: TRoom[],
-) => {
+ t: TTranslation,
+) => TSelectorItem[] = (rooms: TRoom[], t: TTranslation) => {
const items = rooms.map((room) => {
const {
id,
@@ -133,12 +140,20 @@ export const convertRoomsToItems: (rooms: TRoom[]) => TSelectorItem[] = (
parentId,
rootFolderType,
shared,
+ lifetime,
} = room;
const icon = logo.medium || "";
const iconProp = icon ? { icon } : { color: logo.color as string };
+ const lifetimeTooltip = lifetime
+ ? t("Files:RoomFilesLifetime", {
+ days: String(lifetime.value),
+ period: getLifetimePeriodTranslation(lifetime.period, t),
+ })
+ : null;
+
return {
id,
label: title,
@@ -151,6 +166,7 @@ export const convertRoomsToItems: (rooms: TRoom[]) => TSelectorItem[] = (
isFolder: true,
roomType,
shared,
+ lifetimeTooltip,
...iconProp,
};
});
diff --git a/packages/shared/selectors/Files/hooks/useRoomsHelper.tsx b/packages/shared/selectors/Files/hooks/useRoomsHelper.tsx
index 6f1599da1b..cb22c2afff 100644
--- a/packages/shared/selectors/Files/hooks/useRoomsHelper.tsx
+++ b/packages/shared/selectors/Files/hooks/useRoomsHelper.tsx
@@ -142,7 +142,7 @@ const useRoomsHelper = ({
setIsBreadCrumbsLoading(false);
}
- const itemList: TSelectorItem[] = convertRoomsToItems(folders);
+ const itemList: TSelectorItem[] = convertRoomsToItems(folders, t);
setHasNextPage(count === PAGE_COUNT);
diff --git a/packages/shared/selectors/Files/index.tsx b/packages/shared/selectors/Files/index.tsx
index 8487ca92d0..773c474013 100644
--- a/packages/shared/selectors/Files/index.tsx
+++ b/packages/shared/selectors/Files/index.tsx
@@ -134,6 +134,7 @@ const FilesSelector = ({
title: string;
path?: string[];
fileExst?: string;
+ viewUrl?: string;
inPublic?: boolean;
} | null>(null);
const [total, setTotal] = React.useState(0);
@@ -347,6 +348,7 @@ const FilesSelector = ({
id: item.id,
title: item.label,
fileExst: item.fileExst,
+ viewUrl: item.viewUrl,
inPublic,
});
diff --git a/packages/shared/themes/base.ts b/packages/shared/themes/base.ts
index c01d018734..7d1a23952a 100644
--- a/packages/shared/themes/base.ts
+++ b/packages/shared/themes/base.ts
@@ -1410,7 +1410,7 @@ export const getBaseTheme = () => {
color: {
error: lightErrorStatus,
status: black,
- },
+ },
},
dropDown: {
@@ -2004,6 +2004,8 @@ export const getBaseTheme = () => {
background: white,
rootFolderTitleColor: "#A3A9AE",
boxShadow: "0px 8px 16px 0px #040F1B14",
+ lifetimeIconFill: "#f2675a",
+ lifetimeIconStroke: "#f2675a",
icon: {
fill: "#316DAA",
@@ -3236,15 +3238,15 @@ export const getBaseTheme = () => {
notifications: {
textDescriptionColor: "#A3A9AE",
},
- activeSessions: {
- color: "#333",
- borderColor: "#eceef1",
- tickIconColor: "#35AD17",
- removeIconColor: "#A3A9AE",
- sortHeaderColor: "#d0d5da",
+ activeSessions: {
+ color: "#333",
+ borderColor: "#eceef1",
+ tickIconColor: "#35AD17",
+ removeIconColor: "#A3A9AE",
+ sortHeaderColor: "#d0d5da",
tableCellColor: "#a3a9ae",
dividerColor: "#D0D5DA",
- },
+ },
},
formWrapper: {
diff --git a/packages/shared/themes/dark.ts b/packages/shared/themes/dark.ts
index 57ba769a9b..e08ca24e19 100644
--- a/packages/shared/themes/dark.ts
+++ b/packages/shared/themes/dark.ts
@@ -61,7 +61,6 @@ const {
strongBlue,
darkRed,
-
darkErrorStatus,
charlestonGreen,
outerSpace,
@@ -1380,7 +1379,7 @@ const Dark: TTheme = {
color: {
error: darkErrorStatus,
status: grayMaxLight,
- },
+ },
},
dropDown: {
@@ -1976,6 +1975,8 @@ const Dark: TTheme = {
background: black,
rootFolderTitleColor: "#ADADAD",
boxShadow: "0px 8px 16px 0px #040F1B29",
+ lifetimeIconFill: "none",
+ lifetimeIconStroke: "#657077",
icon: {
fill: "#E06A1B",
@@ -3204,15 +3205,15 @@ const Dark: TTheme = {
notifications: {
textDescriptionColor: "#858585",
},
- activeSessions: {
- color: "#eeeeee",
- borderColor: "#474747",
- tickIconColor: "#3BA420",
- removeIconColor: "#A3A9AE",
- sortHeaderColor: "#474747",
+ activeSessions: {
+ color: "#eeeeee",
+ borderColor: "#474747",
+ tickIconColor: "#3BA420",
+ removeIconColor: "#A3A9AE",
+ sortHeaderColor: "#474747",
tableCellColor: "#858585",
dividerColor: "#474747",
- },
+ },
},
formWrapper: {
diff --git a/packages/shared/utils/common.ts b/packages/shared/utils/common.ts
index 658059a19e..2e33226842 100644
--- a/packages/shared/utils/common.ts
+++ b/packages/shared/utils/common.ts
@@ -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";
@@ -298,9 +299,9 @@ export const getUserRole = (user: TUser) => {
user.access === ShareAccessRights.RoomManager ||
user.access === ShareAccessRights.Collaborator
)
- //TODO: Change to People Product Id const
+ // TODO: Change to People Product Id const
return "admin";
- //TODO: Need refactoring
+ // TODO: Need refactoring
if (user.isVisitor) return "user";
if (user.isCollaborator) return "collaborator";
if (user.isRoomAdmin) return "manager";
@@ -411,6 +412,24 @@ export function getProviderLabel(provider: string, t: (key: string) => string) {
return "";
}
}
+
+export const getLifetimePeriodTranslation = (
+ period: number,
+ t: TTranslation,
+) => {
+ switch (period) {
+ case 0:
+ return t("Common:Days").toLowerCase();
+ case 1:
+ return t("Common:Months").toLowerCase();
+ case 2:
+ return t("Common:Years").toLowerCase();
+
+ default:
+ return t("Common:Days").toLowerCase();
+ }
+};
+
export const isLanguageRtl = (lng: string) => {
if (!lng) return;
@@ -1118,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 {
+ 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,
+ );
+};
diff --git a/public/images/icons/16/export-room-index.react.svg b/public/images/icons/16/export-room-index.react.svg
new file mode 100644
index 0000000000..473b3ac917
--- /dev/null
+++ b/public/images/icons/16/export-room-index.react.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/public/images/lifetime-room.react.svg b/public/images/lifetime-room.react.svg
new file mode 100644
index 0000000000..c54fd0d9dc
--- /dev/null
+++ b/public/images/lifetime-room.react.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/public/images/lifetime.react.svg b/public/images/lifetime.react.svg
new file mode 100644
index 0000000000..e570f7665a
--- /dev/null
+++ b/public/images/lifetime.react.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/public/locales/en/Common.json b/public/locales/en/Common.json
index 49cf4684f2..5bf21014d1 100644
--- a/public/locales/en/Common.json
+++ b/public/locales/en/Common.json
@@ -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.",
@@ -370,6 +371,7 @@
"ReviewRoomTitle": "Review room",
"Role": "Role",
"Room": "Room",
+ "RoomName": "Room Name",
"RoomAdmin": "Room admin",
"RoomList": "Room list",
"Rooms": "Rooms",