Merge branch 'feature/VDR-room' into feature/VDR-indexing

# Conflicts:
#	packages/client/src/HOCs/withQuickButtons.js
#	packages/client/src/components/FilesPanels/index.js
#	packages/client/src/components/QuickButtons.js
#	packages/client/src/pages/Home/Section/Header/index.js
#	packages/client/src/store/ContextOptionsStore.js
#	packages/client/src/store/FilesActionsStore.js
#	packages/client/src/store/SelectedFolderStore.ts
This commit is contained in:
Dmitry Sychugov 2024-07-11 12:35:06 +05:00
commit 93a04c0392
60 changed files with 2394 additions and 284 deletions

View File

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

View File

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

View File

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

View File

@ -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 = (
<QuickButtons
t={t}
@ -138,6 +180,8 @@ export default function withQuickButtons(WrappedComponent) {
isArchiveFolder={isArchiveFolder}
isIndexEditingMode={isIndexEditingMode}
currentDeviceType={currentDeviceType}
showLifetimeIcon={showLifetimeIcon}
expiredDate={expiredDate}
/>
);
@ -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));

View File

@ -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) => {
<VersionHistoryPanel key="version-history-panel" />
),
deleteDialogVisible && <DeleteDialog key="delete-dialog" />,
lifetimeDialogVisible && <LifetimeDialog key="delete-dialog" />,
emptyTrashDialogVisible && <EmptyTrashDialog key="empty-trash-dialog" />,
downloadDialogVisible && <DownloadDialog key="download-dialog" />,
@ -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,

View File

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

View File

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

View File

@ -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 = () => (
<Text fontSize="12px" fontWeight={400} noSelect>
{t("Files:FileWillBeDeleted", { date: expiredDate })}.
</Text>
);
return (
<div className="badges additional-badges badges__quickButtons">
<StyledQuickButtons className="badges additional-badges badges__quickButtons">
{showLifetimeIcon && (
<>
<ColorTheme
themeId={ThemeId.IconButton}
iconName={LifetimeReactSvgUrl}
className="badge file-lifetime icons-group"
size={sizeQuickButton}
isClickable
isDisabled={isDisabled}
data-tooltip-id="lifetimeTooltip"
color={theme.filesQuickButtons.lifeTimeColor}
/>
<Tooltip
id="lifetimeTooltip"
place="bottom"
getContent={getTooltipContent}
maxWidth="300px"
/>
</>
)}
{isAvailableLockFile && !isIndexEditingMode && (
<ColorTheme
themeId={ThemeId.IconButton}
@ -207,7 +254,7 @@ const QuickButtons = (props) => {
hoverColor={theme.filesQuickButtons.hoverColor}
/>
)} */}
</div>
</StyledQuickButtons>
);
};

View File

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

View File

@ -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
>
<ModalDialog.Header>
@ -182,4 +189,10 @@ const EditRoomDialog = ({
);
};
export default EditRoomDialog;
export default inject(({ createEditRoomStore }) => {
const { isEqualWatermarkChanges } = createEditRoomStore;
return {
isEqualWatermarkChanges,
};
})(observer(EditRoomDialog));

View File

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

View File

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

View File

@ -1,16 +1,17 @@
import { useState } from "react";
import { Trans } from "react-i18next";
import styled from "styled-components";
import { inject, observer } from "mobx-react";
import { Text } from "@docspace/shared/components/text";
import { ToggleButton } from "@docspace/shared/components/toggle-button";
import FileLifetime from "./FileLifetime";
import WatermarkBlock from "./Watermarks/WatermarkBlock";
const StyledVirtualDataRoomBlock = styled.div`
.virtual-data-room-block {
margin-bottom: 18px;
:last-child {
margin-bottom: -26px;
}
.virtual-data-room-block_header {
display: flex;
@ -26,6 +27,9 @@ const StyledVirtualDataRoomBlock = styled.div`
color: ${({ theme }) => theme.editLink.text.color};
}
.virtual-data-room-block_content {
margin-top: 16px;
}
}
`;
@ -64,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 (
<StyledVirtualDataRoomBlock>
<Block
@ -103,7 +105,11 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
isDisabled={false}
isChecked={fileLifetimeChecked}
>
<FileLifetime t={t} />
<FileLifetime
t={t}
roomParams={roomParams}
setRoomParams={setRoomParams}
/>
</Block>
<Block
headerText={t("RestrictCopyAndDownload")}
@ -117,13 +123,8 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
isDisabled={false}
isChecked={copyAndDownloadChecked}
></Block>
<Block
headerText={t("AddWatermarksToDocuments")}
bodyText={t("AddWatermarksToDocumentsDescription")}
onChange={onChangeAddWatermarksToDocuments}
isDisabled={false}
isChecked={watermarksChecked}
></Block>
<WatermarkBlock BlockComponent={Block} t={t} isEdit={isEdit} />
</StyledVirtualDataRoomBlock>
);
};

View File

@ -0,0 +1,334 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { Text } from "@docspace/shared/components/text";
import { ComboBox } from "@docspace/shared/components/combobox";
import { DropDownItem } from "@docspace/shared/components/drop-down-item";
import { FileInput } from "@docspace/shared/components/file-input";
import { imageProcessing } from "@docspace/shared/utils/common";
import { ButtonDelete } from "@docspace/shared/components/image-editor";
import { HelpButton } from "@docspace/shared/components/help-button";
import { StyledWatermark } from "./StyledComponent";
const scaleOptions = [
{ key: 100, label: "100" },
{ key: 200, label: "200" },
{ key: 300, label: "300" },
{ key: 400, label: "400" },
{ key: 500, label: "500" },
];
const rotateOptions = [
{ key: 0, label: "0" },
{ key: 30, label: "30" },
{ key: 45, label: "45" },
{ key: 60, label: "60" },
{ key: 90, label: "90" },
];
const getInitialScale = (scale, isEdit) => {
if (!isEdit || !scale) return scaleOptions[0];
return scaleOptions.find((item) => {
return item.key === scale;
});
};
const getInitialRotate = (rotate, isEdit) => {
if (!isEdit) return rotateOptions[0];
const item = rotateOptions.find((item) => {
return item.key === rotate;
});
return !item ? rotateOptions[0] : item;
};
const ImageWatermark = ({
isEdit,
setWatermarks,
initialWatermarksSettings,
imageUrl,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const initialInfo = useRef(null);
const previewRef = useRef(null);
if (initialInfo.current === null) {
initialInfo.current = {
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit),
scale: getInitialScale(initialWatermarksSettings?.imageScale, isEdit),
};
}
const initialInfoRef = initialInfo.current;
useEffect(() => {
const { enabled, isImage } = initialWatermarksSettings;
if (isEdit && enabled && isImage) {
setWatermarks(initialWatermarksSettings);
return;
}
setWatermarks({
rotate: initialInfoRef.rotate.key,
scale: initialInfoRef.scale.key,
additions: 0,
isImage: true,
enabled: true,
});
}, []);
useEffect(() => {
return () => {
URL.revokeObjectURL(previewRef.current);
previewRef.current = null;
};
}, []);
const [selectedRotate, setRotate] = useState(initialInfoRef.rotate);
const [selectedScale, setScale] = useState(initialInfoRef.scale);
const [selectedImageUrl, setImageUrl] = useState(imageUrl);
const onInput = (file) => {
imageProcessing(file)
.then((f) => {
if (f instanceof File) {
setWatermarks({ image: f });
const img = new Image();
previewRef.current = URL.createObjectURL(f);
img.src = previewRef.current;
img.onload = () => {
setImageUrl(previewRef.current);
};
}
})
.catch((error) => {
if (
error instanceof Error &&
error.message === "recursion depth exceeded"
) {
toastr.error(t("Common:SizeImageLarge"));
}
});
};
const onScaleChange = (item) => {
setScale(item);
setWatermarks({ imageScale: item.key });
};
const onRotateChange = (item) => {
setRotate(item);
setWatermarks({ rotate: item.key });
};
const onButtonClick = () => {
if (previewRef.current) {
URL.revokeObjectURL(previewRef.current);
previewRef.current = null;
}
setWatermarks({ image: null, imageUrl: null });
setImageUrl("");
};
const rotateItems = () => {
const items = rotateOptions.map((item) => {
return (
<DropDownItem
className="access-right-item"
key={item.key}
data-key={item.key}
onClick={() => onRotateChange(item)}
>
{item.label}&deg;
</DropDownItem>
);
});
return <div style={{ display: "contents" }}>{items}</div>;
};
const scaleItems = () => {
const items = scaleOptions.map((item) => {
return (
<DropDownItem
className="access-right-item"
key={item.key}
data-key={item.key}
onClick={() => onScaleChange(item)}
>
{item.label}&#37;
</DropDownItem>
);
});
return <div style={{ display: "contents" }}>{items}</div>;
};
// const onSelectFile = (fileInfo) => {
// setWatermarks({ image: fileInfo });
// };
console.log("selectedRotate", selectedRotate.key, selectedScale.key);
return (
<StyledWatermark
rotate={selectedRotate.key}
scale={selectedScale.key / 100}
mainHeight={50}
>
{!selectedImageUrl && (
<FileInput
accept={["image/png", "image/jpeg"]}
onInput={onInput}
scale
/>
)}
{/* <FilesSelectorInput
onSelectFile={onSelectFile}
filterParam={FilesSelectorFilterTypes.IMG}
isSelect
scale
/> */}
{selectedImageUrl && (
<div className="image-wrapper">
<div>
<div className="image-description">
<Text fontWeight={600} className="image-watermark_text">
{t("WatermarkPreview")}
</Text>
<HelpButton
tooltipContent={
<Text fontSize="12px">{t("WatermarkPreviewHelp")}</Text>
}
offsetRight={0}
className="settings_unavailable"
/>
</div>
<div className="image-watermark_wrapper">
<img
alt="logo"
src={selectedImageUrl}
className="header-logo-icon"
/>
</div>
<ButtonDelete t={t} onClick={onButtonClick} />
</div>
<div className="options-wrapper">
<div>
<Text fontWeight={600} lineHeight="20px">
{t("Scale")}
</Text>
<ComboBox
onSelect={onScaleChange}
scaled
scaledOptions
advancedOptions={scaleItems()}
options={[]}
selectedOption={{}}
>
<div>{selectedScale.label}&#37;</div>
</ComboBox>
</div>
<div>
<Text fontWeight={600} lineHeight="20px">
{t("Rotate")}
</Text>
<ComboBox
onSelect={onRotateChange}
scaled
scaledOptions
advancedOptions={rotateItems()}
options={[]}
selectedOption={{}}
advancedOptionsCount={rotateOptions.length}
fillIcon={false}
>
<div>{selectedRotate.label}&deg;</div>
</ComboBox>
</div>
</div>
</div>
)}
</StyledWatermark>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings, watermarksSettings } =
createEditRoomStore;
const { imageUrl } = watermarksSettings;
return {
setWatermarks,
initialWatermarksSettings,
imageUrl,
};
})(observer(ImageWatermark));

View File

@ -0,0 +1,90 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import styled, { css } from "styled-components";
const StyledWatermark = styled.div`
margin-top: 16px;
.watermark-title {
margin: 16px 0 8px 0;
}
.title-without-top {
margin-top: 0px;
}
.watermark-checkbox {
margin: 18px 0 0 0;
}
.options-wrapper {
display: grid;
grid-template-rows: 56px 56px;
gap: 16px;
}
.image-wrapper {
display: grid;
grid-template-columns: 216px auto;
gap: 16px;
.image-description {
display: flex;
gap: 8px;
align-items: baseline;
.image-watermark_text {
margin-bottom: 8px;
}
}
.image-watermark_wrapper {
width: 216px;
height: 216px;
border: 1px solid #eceef1;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
img {
width: 88%;
height: 88%;
transform: ${(props) =>
`rotate(${props.rotate}deg) scale(${props.scale})`};
opacity: 0.4;
margin: auto;
}
}
}
`;
const StyledBody = styled.div`
.types-content {
}
`;
export { StyledWatermark, StyledBody };

View File

@ -0,0 +1,240 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { TextInput } from "@docspace/shared/components/text-input";
import { Text } from "@docspace/shared/components/text";
import { ComboBox } from "@docspace/shared/components/combobox";
import { WatermarkAdditions } from "@docspace/shared/enums";
import { StyledWatermark } from "./StyledComponent";
import { Tabs, TabsTypes } from "@docspace/shared/components/tabs";
const tabsOptions = (t) => [
{
id: "UserName",
name: t("UserName"),
index: 0,
},
{
id: "UserEmail",
name: t("UserEmail"),
index: 1,
},
{
id: "UserIpAdress",
name: t("UserIPAddress"),
index: 2,
},
{
id: "CurrentDate",
name: t("Common:CurrentDate"),
index: 3,
},
{
id: "RoomName",
name: t("Common:RoomName"),
index: 4,
},
];
const getInitialState = (initialTab) => {
const state = {
UserName: false,
UserEmail: false,
UserIpAdress: false,
CurrentDate: false,
RoomName: false,
};
initialTab.map((item) => {
state[item.id] = true;
});
return state;
};
const getInitialText = (text, isEdit) => {
return isEdit && text ? text : "";
};
const getInitialTabs = (additions, isEdit, t) => {
const dataTabs = tabsOptions(t);
if (!isEdit || !additions) return [dataTabs[0]];
return dataTabs.filter((item) => additions & WatermarkAdditions[item.id]);
};
const rotateOptions = (t) => [
{ key: -45, label: t("Diagonal") },
{ key: 0, label: t("Horizontal") },
];
const getInitialRotate = (rotate, isEdit, t) => {
const dataRotate = rotateOptions(t);
if (!isEdit) return dataRotate[0];
const item = dataRotate.find((item) => {
return item.key === rotate;
});
return !item ? dataRotate[0] : item;
};
const ViewerInfoWatermark = ({
isEdit,
setWatermarks,
initialWatermarksSettings,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const elements = useRef(null);
const initialInfo = useRef(null);
if (initialInfo.current === null) {
initialInfo.current = {
dataRotate: rotateOptions(t),
dataTabs: tabsOptions(t),
tabs: getInitialTabs(initialWatermarksSettings?.additions, isEdit, t),
rotate: getInitialRotate(initialWatermarksSettings?.rotate, isEdit, t),
text: getInitialText(initialWatermarksSettings?.text, isEdit),
};
elements.current = getInitialState(initialInfo.current.tabs);
}
const initialInfoRef = initialInfo.current;
useEffect(() => {
const { enabled, isImage } = initialWatermarksSettings;
if (isEdit && enabled && !isImage) {
setWatermarks(initialWatermarksSettings);
return;
}
setWatermarks({
rotate: initialInfoRef.rotate.key,
additions: WatermarkAdditions.UserName,
isImage: false,
enabled: true,
image: "",
imageWidth: 0,
imageHeight: 0,
imageScale: 0,
});
}, []);
const [selectedPosition, setSelectedPosition] = useState(
initialInfoRef.rotate,
);
const [textValue, setTextValue] = useState(initialInfoRef.text);
const onSelect = (item) => {
let elementsData = elements.current;
let flagsCount = 0;
const key = item.id;
elementsData[key] = !elementsData[item.id];
for (const key in elementsData) {
const value = elementsData[key];
if (value) {
flagsCount += WatermarkAdditions[key];
}
}
setWatermarks({ additions: flagsCount });
};
const onPositionChange = (item) => {
setSelectedPosition(item);
setWatermarks({ rotate: item.key });
};
const onTextChange = (e) => {
const { value } = e.target;
setTextValue(value);
setWatermarks({ text: value });
};
return (
<StyledWatermark>
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
{t("AddWatermarkElements")}
</Text>
<Tabs
items={initialInfoRef.dataTabs}
selectedItems={initialInfoRef.tabs.map((item) => item.index)}
onSelect={onSelect}
type={TabsTypes.Secondary}
multiple
/>
<Text
className="watermark-title title-without-top"
fontWeight={600}
lineHeight="20px"
>
{t("AddStaticText")}
</Text>
<TextInput scale value={textValue} tabIndex={1} onChange={onTextChange} />
<Text className="watermark-title" fontWeight={600} lineHeight="20px">
{t("Position")}
</Text>
<ComboBox
selectedOption={selectedPosition}
options={initialInfoRef.dataRotate}
onSelect={onPositionChange}
scaled
scaledOptions
/>
</StyledWatermark>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
return {
setWatermarks,
initialWatermarksSettings,
};
})(observer(ViewerInfoWatermark));

View File

@ -0,0 +1,102 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState, useEffect } from "react";
import { inject, observer } from "mobx-react";
import Watermarks from "./index";
const WatermarkBlock = ({
BlockComponent,
setWatermarks,
isEdit = false,
isWatermarks = false,
resetWatermarks,
t,
}) => {
useEffect(() => {
return () => resetWatermarks();
}, []);
const [watermarksChecked, setWatermarksChecked] = useState(
isWatermarks && isEdit,
);
const onChangeAddWatermarksToDocuments = () => {
setWatermarksChecked(!watermarksChecked);
setWatermarks({ enabled: !watermarksChecked });
};
return (
<BlockComponent
headerText={t("AddWatermarksToDocuments")}
bodyText={t("AddWatermarksToDocumentsDescription")}
onChange={onChangeAddWatermarksToDocuments}
isDisabled={false}
isChecked={watermarksChecked}
>
<Watermarks isEdit={isEdit} />
</BlockComponent>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings, resetWatermarks } =
createEditRoomStore;
return {
setWatermarks,
isWatermarks: initialWatermarksSettings?.enabled,
resetWatermarks,
};
})(observer(WatermarkBlock));

View File

@ -0,0 +1,102 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { inject, observer } from "mobx-react";
import { RadioButtonGroup } from "@docspace/shared/components/radio-button-group";
import ViewerInfoWatermark from "./ViewerInfo";
import { StyledBody } from "./StyledComponent";
import ImageWatermark from "./ImageWatermark";
const imageWatermark = "image",
viewerInfoWatermark = "viewerInfo";
const options = (t) => [
{
label: t("ViewerInfo"),
value: viewerInfoWatermark,
},
{
label: t("Common:Image"),
value: imageWatermark,
},
];
const getOptionType = (additions, isEdit) => {
if (isEdit) {
return additions === 0 ? imageWatermark : viewerInfoWatermark;
}
return viewerInfoWatermark;
};
const Watermarks = ({ isEdit, setWatermarks, initialWatermarksSettings }) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common"]);
const [type, setType] = useState(
getOptionType(initialWatermarksSettings?.additions, isEdit),
);
const onSelectType = (e) => {
const { value } = e.target;
setType(value);
setWatermarks({
isImage: type === imageWatermark,
});
};
const typeOptions = options(t);
return (
<StyledBody>
<RadioButtonGroup
name="watermarks-radiobutton"
fontSize="13px"
fontWeight="400"
spacing="8px"
options={typeOptions}
selected={type}
onClick={onSelectType}
/>
{type === imageWatermark ? (
<ImageWatermark isEdit={isEdit} />
) : (
<ViewerInfoWatermark isEdit={isEdit} />
)}
</StyledBody>
);
};
export default inject(({ createEditRoomStore }) => {
const { setWatermarks, initialWatermarksSettings } = createEditRoomStore;
return {
setWatermarks,
initialWatermarksSettings,
};
})(observer(Watermarks));

View File

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

View File

@ -0,0 +1,46 @@
import { ModalDialog } from "@docspace/shared/components/modal-dialog";
import { tablet } from "@docspace/shared/utils";
import styled from "styled-components";
import { getCorrectFourValuesStyle } from "@docspace/shared/utils";
const StyledLifetimeDialog = styled(ModalDialog)`
.modal-dialog-content-body {
display: grid;
gap: 18px;
}
.modal-dialog-aside-header {
margin: ${({ theme }) =>
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 };

View File

@ -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 (
<StyledLifetimeDialog
isLoading={!tReady}
visible={visible}
onClose={onClose}
>
<ModalDialog.Header>{t("Common:Warning")}</ModalDialog.Header>
<ModalDialog.Body>
<div className="modal-dialog-content-body">
<Text fontWeight={600} fontSize="13px" noSelect>
{t("Files:LifetimeDialogDescriptionHeader")}
</Text>
<Text fontSize="13px" noSelect>
{t("Files:LifetimeDialogDescription")}
</Text>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<Button
id="delete-file-modal_submit"
key="OkButton"
label={t("Common:OKButton")}
size="normal"
primary
scale
onClick={onDeleteAction}
// isDisabled={!selection.length}
/>
<Button
id="delete-file-modal_cancel"
key="CancelButton"
label={t("Common:CancelButton")}
size="normal"
scale
onClick={onClose}
/>
</ModalDialog.Footer>
</StyledLifetimeDialog>
);
};
const LifetimeDialog = withTranslation(["Common", "Files"])(
LifetimeDialogComponent,
);
export default inject(({ dialogsStore }) => {
const { lifetimeDialogVisible: visible, setLifetimeDialogVisible } =
dialogsStore;
return {
visible,
setLifetimeDialogVisible,
};
})(observer(LifetimeDialog));

View File

@ -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 = (
<>
<Link
color="#5299E0"
fontSize="12px"
target={openOnNewPage ? LinkTarget.blank : LinkTarget.self}
href={fileUrl}
>
{fileName}
</Link>
&nbsp;
<Text as="span" fontSize="12px">
{t<string>("Files:FileExportedToMyDocuments")}
</Text>
</>
);
toastr.success(toastMessage);
};

View File

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

View File

@ -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 (
<Consumer key="header">
{(context) => (
<StyledContainer isRecycleBinFolder={isRecycleBinFolder}>
<StyledContainer
isRecycleBinFolder={isRecycleBinFolder}
isVirtualDataRoomType={isVirtualDataRoomType}
>
{tableGroupMenuVisible ? (
<TableGroupMenu {...tableGroupMenuProps} withComboBox />
) : 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,

View File

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

View File

@ -25,12 +25,15 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { makeAutoObservable } from "mobx";
import isEqual from "lodash/isEqual";
import { toastr } from "@docspace/shared/components/toast";
import { isDesktop } from "@docspace/shared/utils";
import FilesFilter from "@docspace/shared/api/files/filter";
import { getCategoryUrl } from "SRC_DIR/helpers/utils";
import { CategoryType } from "SRC_DIR/helpers/constants";
import { RoomsType } from "@docspace/shared/enums";
import { setWatermarkSettings } from "@docspace/shared/api/rooms";
class CreateEditRoomStore {
roomParams = null;
@ -46,6 +49,9 @@ class CreateEditRoomStore {
settingsStore = null;
infoPanelStore = null;
currentQuotaStore = null;
watermarksSettings = {};
initialWatermarksSettings = {};
isImageType = false;
constructor(
filesStore,
@ -91,6 +97,113 @@ class CreateEditRoomStore {
this.onClose = onClose;
};
setInitialWatermarks = (watermarksSettings) => {
this.initialWatermarksSettings = !watermarksSettings
? { enabled: false }
: watermarksSettings;
this.initialWatermarksSettings.isImage =
!!this.initialWatermarksSettings.imageUrl;
this.setWatermarks(this.initialWatermarksSettings);
};
setWatermarks = (object) => {
for (const [key, value] of Object.entries(object)) {
this.watermarksSettings[key] = value;
}
};
resetWatermarks = () => {
this.watermarksSettings = {};
this.initialWatermarksSettings = {};
};
isEqualWatermarkChanges = () => {
return isEqual(this.watermarksSettings, this.initialWatermarksSettings);
};
isNotWatermarkSet = () => {
if (
this.watermarksSettings.isImage &&
!this.watermarksSettings.image &&
!this.watermarksSettings.imageUrl
)
return true;
if (
!this.watermarksSettings.isImage &&
this.watermarksSettings.additions === 0
)
return true;
return false;
};
getWatermarkRequest = async (room) => {
if (!this.watermarksSettings.isImage) {
return setWatermarkSettings(room.id, {
enabled: this.watermarksSettings.enabled,
rotate: this.watermarksSettings.rotate,
text: this.watermarksSettings.text,
additions: this.watermarksSettings.additions,
});
}
const watermarkImage = this.watermarksSettings.image;
const watermarksSettings = this.watermarksSettings;
const getMeta = (url, onSetInfo) => {
//url for this.watermarksSettings.image.viewUrl
const img = new Image();
const imgUrl = url ?? URL.createObjectURL(watermarkImage);
img.src = imgUrl;
img.onload = () => {
URL.revokeObjectURL(imgUrl);
onSetInfo(null, img);
};
img.onerror = (err) => onSetInfo(err);
};
if (!watermarkImage && this.watermarksSettings.imageUrl) {
return setWatermarkSettings(room.id, {
enabled: watermarksSettings.enabled,
imageScale: watermarksSettings.imageScale,
rotate: watermarksSettings.rotate,
imageUrl: watermarksSettings.imageUrl,
// imageId: watermarksSettings.image.id,
imageWidth: watermarksSettings.imageWidth,
imageHeight: watermarksSettings.imageHeight,
});
}
const { uploadRoomLogo } = this.filesStore;
const uploadWatermarkData = new FormData();
uploadWatermarkData.append(0, watermarkImage);
const response = await uploadRoomLogo(uploadWatermarkData);
getMeta(null, (err, img) => {
if (err) {
toastr.error(err);
return;
}
return setWatermarkSettings(room.id, {
enabled: watermarksSettings.enabled,
imageScale: watermarksSettings.imageScale,
rotate: watermarksSettings.rotate,
imageUrl: response.data,
// imageId: watermarksSettings.image.id,
imageWidth: img.naturalWidth,
imageHeight: img.naturalHeight,
});
});
};
onCreateRoom = async (withConfirm = false, t) => {
const roomParams = this.roomParams;
@ -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 {

View File

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

View File

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

View File

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

View File

@ -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<FolderType> = 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) => {

View File

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

View File

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

View File

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

View File

@ -33,4 +33,5 @@ export const enum FloatingButtonIcons {
plus = "plus",
minus = "minus",
refresh = "refresh",
exportIndex = "exportIndex",
}

View File

@ -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: <ButtonMinusIcon />,
refresh: <RefreshIcon />,
duplicate: <ButtonDuplicateIcon />,
exportIndex: <ExportRoomIndexIcon />,
};
const FloatingButton = (props: FloatingButtonProps) => {

View File

@ -0,0 +1,123 @@
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
// (c) Copyright Ascensio System SIA 2009-2024
//
// This program is a free software product.
// You can redistribute it and/or modify it under the terms
// of the GNU Affero General Public License (AGPL) version 3 as published by the Free Software
// Foundation. In accordance with Section 7(a) of the GNU AGPL its Section 15 shall be amended
// to the effect that Ascensio System SIA expressly excludes the warranty of non-infringement of
// any third-party rights.
//
// This program is distributed WITHOUT ANY WARRANTY, without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For details, see
// the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html
//
// You can contact Ascensio System SIA at Lubanas st. 125a-25, Riga, Latvia, EU, LV-1021.
//
// The interactive user interfaces in modified source and object code versions of the Program must
// display Appropriate Legal Notices, as required under Section 5 of the GNU AGPL version 3.
//
// Pursuant to Section 7(b) of the License you must retain the original Product logo when
// distributing the program. Pursuant to Section 7(e) we decline to grant you any rights under
// trademark law for use of our trademarks.
//
// All the Product's GUI elements, including illustrations and icon sets, as well as technical writing
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import { ReactSVG } from "react-svg";
import styled from "styled-components";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import { TTranslation } from "../../../types";
const StyledButton = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 6px 0;
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.background};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.borderColor};
border-radius: 3px;
margin-bottom: 12px;
transition: all 0.2s ease;
&:hover {
background: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBackground};
border: 1px solid
${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton
.hoverBorderColor};
}
&-text {
user-select: none;
font-weight: 600;
line-height: 20px;
color: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.color};
}
svg {
path {
fill: ${(props) =>
props.theme.createEditRoomDialog.iconCropper.deleteButton.iconColor};
}
}
`;
const ButtonDelete = ({
onClick,
t,
}: {
onClick: (e: React.MouseEvent) => void;
t: TTranslation;
}) => {
return (
<StyledButton
className="icon_cropper-delete_button"
onClick={onClick}
title={t("Common:Delete")}
>
<ReactSVG src={TrashReactSvgUrl} />
<div className="icon_cropper-delete_button-text">
{t("Common:Delete")}
</div>
</StyledButton>
);
};
export default ButtonDelete;

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = () => (
<Text fontSize="12px" fontWeight={400} noSelect>
{titleIconTooltip}
</Text>
);
const navigationTitleNode = (
<div className="title-block">
{titleIcon && <ReactSVG className="title-icon" src={titleIcon} />}
<Text
{titleIcon && (
<ReactSVG
data-tooltip-id="iconTooltip"
className="title-icon"
src={titleIcon}
/>
)}
{titleIconTooltip && (
<Tooltip
id="iconTooltip"
place="bottom"
getContent={getContent}
maxWidth="300px"
/>
)}
<NavigationText
className="title-block-text"
title={title}
isOpen={isOpen}
isOpen={false}
isRootFolder={isRootFolder}
onClick={toggleDropBox}
isRootFolderTitle={false}
@ -189,7 +213,7 @@ const Navigation = ({
const navigationTitleContainerNode = showRootFolderNavigation ? (
<div className="title-container">
<Text
<NavigationText
className="room-title"
title={
rootRoomTitle || navigationItems[navigationItems.length - 2].title
@ -241,6 +265,7 @@ const Navigation = ({
currentDeviceType={currentDeviceType}
navigationTitleContainerNode={navigationTitleContainerNode}
onCloseDropBox={onCloseDropBox}
titleIconTooltip={titleIconTooltip}
/>
</>
)}

View File

@ -203,5 +203,6 @@ export interface INavigationProps {
onNavigationButtonClick?: () => void;
tariffBar: React.ReactElement;
showNavigationButton: boolean;
titleIconTooltip?: string;
onContextOptionsClick?: () => void;
}

View File

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

View File

@ -541,6 +541,7 @@ export type TSelectorItem = TSelectorItemType & {
isSelected?: boolean;
isDisabled?: boolean;
disabledText?: string;
lifetimeTooltip?: string | null;
};
export type Data = {

View File

@ -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 = () => (
<Text fontSize="12px" fontWeight={400} noSelect>
{lifetimeTooltip}
</Text>
);
return (
<StyledItem
key={`${label}-${avatar}-${role}`}
@ -215,16 +225,34 @@ const Item = React.memo(({ index, style, data }: ItemProps) => {
{renderCustomItem ? (
renderCustomItem(label, typeLabel, email, isGroup)
) : (
<Text
className="label label-disabled"
fontWeight={600}
fontSize="14px"
noSelect
truncate
dir="auto"
>
{label}
</Text>
<div className="selector-item_name">
<Text
className="label label-disabled"
fontWeight={600}
fontSize="14px"
noSelect
truncate
dir="auto"
>
{label}
</Text>
{lifetimeTooltip && (
<>
<ReactSVG
data-tooltip-id={`${item.id}_iconTooltip`}
className="title-icon"
src={LifetimeRoomIconUrl}
/>
<Tooltip
id={`${item.id}_iconTooltip`}
place="bottom"
getContent={getContent}
maxWidth="300px"
/>
</>
)}
</div>
)}
{isDisabled && disabledText ? (

View File

@ -24,7 +24,5 @@
// content are licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
export const INDEX_NOT_FOUND = -1;
export const OFFSET_RIGHT = 48;
export const OFFSET_LEFT = 48;

View File

@ -31,9 +31,14 @@ import { TabsTypes } from "./Tabs.enums";
export const StyledTabs = styled.div<{
stickyTop?: string;
multiple: boolean;
}>`
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) =>

View File

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

View File

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

View File

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

View File

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

View File

@ -142,7 +142,7 @@ const useRoomsHelper = ({
setIsBreadCrumbsLoading(false);
}
const itemList: TSelectorItem[] = convertRoomsToItems(folders);
const itemList: TSelectorItem[] = convertRoomsToItems(folders, t);
setHasNextPage(count === PAGE_COUNT);

View File

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

View File

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

View File

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

View File

@ -33,6 +33,7 @@ import moment from "moment-timezone";
import { isMobile } from "react-device-detect";
import { I18nextProviderProps } from "react-i18next";
import sjcl from "sjcl";
import resizeImage from "resize-image";
import { flagsIcons } from "@docspace/shared/utils/image-flags";
@ -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<unknown> {
const data = resizeImage.resize(
// @ts-expect-error canvas
canvas,
img.width / compressionRatio,
img.height / compressionRatio,
resizeImage.JPEG,
);
const newFile = await fetch(data)
.then((res) => res.blob())
.then((blob) => {
const f = new File([blob], "File name", {
type: "image/jpg",
});
return f;
});
// const stepMessage = `Step ${depth + 1}`;
// const sizeMessage = `size = ${file.size} bytes`;
// const compressionRatioMessage = `compressionRatio = ${compressionRatio}`;
// console.log(`${stepMessage} ${sizeMessage} ${compressionRatioMessage}`);
if (file.size < maxImageSize) {
return file;
}
if (depth > 5) {
// console.log("start");
throw new Error("recursion depth exceeded");
}
return new Promise((resolve) => {
// eslint-disable-next-line no-promise-executor-return
return resolve(newFile);
}).then(() => resizeRecursiveAsync(img, compressionRatio + 1, depth + 1));
}
return resizeRecursiveAsync(
{ width, height },
file.size > maxImageSize ? COMPRESSION_RATIO : NO_COMPRESSION_RATIO,
);
};

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8092 8L13.8092 12.3445L14.8555 11.3389L16 12.4388L13.5722 14.7722C13.4205 14.9181 13.2146 15 13 15C12.7854 15 12.5795 14.9181 12.4278 14.7722L10 12.4388L11.1445 11.3389L12.1907 12.3445L12.1907 8.00001L13.8092 8Z" fill="#657077"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 1.5C2 1.22386 1.77614 1 1.5 1H0.5C0.223858 1 0 1.22386 0 1.5V2.5C0 2.77614 0.223858 3 0.5 3H1.5C1.77614 3 2 2.77614 2 2.5V1.5ZM16 1.5C16 1.22386 15.7761 1 15.5 1H4.5C4.22386 1 4 1.22386 4 1.5V2.5C4 2.77614 4.22386 3 4.5 3L15.5 3C15.7761 3 16 2.77614 16 2.5V1.5ZM16 5.5C16 5.22386 15.7761 5 15.5 5H8.5C8.22386 5 8 5.22386 8 5.5V6.5C8 6.77614 8.22386 7 8.5 7H15.5C15.7761 7 16 6.77614 16 6.5V5.5ZM9 13H4.5C4.22386 13 4 13.2239 4 13.5V14.5C4 14.7761 4.22386 15 4.5 15H9V13ZM5 7C5.55228 7 6 6.55228 6 6C6 5.44772 5.55228 5 5 5C4.44772 5 4 5.44772 4 6C4 6.55228 4.44772 7 5 7ZM5 11C5.55228 11 6 10.5523 6 10C6 9.44771 5.55228 9 5 9C4.44772 9 4 9.44771 4 10C4 10.5523 4.44772 11 5 11ZM0.5 13C0.223858 13 0 13.2239 0 13.5V14.5C0 14.7761 0.223858 15 0.5 15H1.5C1.77614 15 2 14.7761 2 14.5V13.5C2 13.2239 1.77614 13 1.5 13H0.5Z" fill="#657077"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.29161 8.29084C3.35982 8.64692 3.4449 8.99697 3.55132 9.31623C3.75317 9.92178 4.14601 10.3844 4.6771 10.6278C5.18859 10.8623 5.73524 10.8507 6.19123 10.6987C6.64256 10.5482 7.07413 10.2374 7.33699 9.77739C7.61595 9.28921 7.66898 8.68902 7.41915 8.10608C6.78953 6.63698 6.88202 5.03002 7.37839 3.65122C7.60976 3.00852 7.9148 2.45209 8.24405 2.0037C8.40619 2.60299 8.64385 3.12067 8.94095 3.58119C9.47219 4.4046 10.1678 4.99418 10.7477 5.48566C10.783 5.51561 10.8179 5.54519 10.8524 5.57444C11.4796 6.10764 11.9845 6.56006 12.359 7.19042C12.7212 7.80008 13 8.64846 13 10C13 12.7614 10.7614 15 8 15C5.23858 15 3 12.7614 3 10C3 9.40715 3.11554 8.8279 3.29161 8.29084Z" stroke="#657077" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" width="6" height="2" rx="1" fill="#F2675A"/>
<circle cx="8" cy="9" r="6" stroke="#F2675A" stroke-width="2"/>
<path d="M8 5V9L10 11" stroke="#F2675A" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

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