Merge pull request #509 from ONLYOFFICE/feature/VDR-lifetime

Feature/vdr lifetime
This commit is contained in:
Alexey Safronov 2024-07-09 17:29:59 +04:00 committed by GitHub
commit 8f2f8defa5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 573 additions and 55 deletions

View File

@ -181,6 +181,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;
@ -115,8 +151,14 @@ export default function withQuickButtons(WrappedComponent) {
isPersonalRoom,
isArchiveFolder,
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}
@ -136,6 +178,8 @@ export default function withQuickButtons(WrappedComponent) {
onCopyPrimaryLink={this.onCopyPrimaryLink}
isArchiveFolder={isArchiveFolder}
currentDeviceType={currentDeviceType}
showLifetimeIcon={showLifetimeIcon}
expiredDate={expiredDate}
/>
);
@ -175,10 +219,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,
@ -192,6 +238,7 @@ export default function withQuickButtons(WrappedComponent) {
isArchiveFolder,
getPrimaryFileLink,
setShareChanged,
roomLifetime: infoPanelRoom?.lifetime,
};
},
)(observer(WithQuickButtons));

View File

@ -76,6 +76,7 @@ import LeaveRoomDialog from "../dialogs/LeaveRoomDialog";
import ChangeRoomOwnerPanel from "../panels/ChangeRoomOwnerPanel";
import { CreatedPDFFormDialog } from "../dialogs/CreatedPDFFormDialog";
import { PDFFormEditingDialog } from "../dialogs/PDFFormEditingDialog";
import LifetimeDialog from "../dialogs/LifetimeDialog";
import { SharePDFFormDialog } from "../dialogs/SharePDFFormDialog";
const Panels = (props) => {
@ -90,6 +91,7 @@ const Panels = (props) => {
deleteThirdPartyDialogVisible,
versionHistoryPanelVisible,
deleteDialogVisible,
lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,
@ -269,6 +271,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" />,
@ -378,6 +381,7 @@ export default inject(
connectDialogVisible,
deleteThirdPartyDialogVisible,
deleteDialogVisible,
lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,
@ -442,6 +446,7 @@ export default inject(
deleteThirdPartyDialogVisible,
versionHistoryPanelVisible,
deleteDialogVisible,
lifetimeDialogVisible,
downloadDialogVisible,
emptyTrashDialogVisible,
newFilesPanelVisible,

View File

@ -27,6 +27,7 @@
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";
@ -75,6 +76,7 @@ const EditRoomEvent = ({
defaultRoomsQuota,
isDefaultRoomsQuotaSet,
changeRoomLifetime,
}) => {
const { t } = useTranslation(["CreateEditRoomDialog", "Common", "Files"]);
@ -106,6 +108,7 @@ const EditRoomEvent = ({
},
roomOwner: item.createdBy,
indexing: item.indexing,
lifetime: item.lifetime,
...(isDefaultRoomsQuotaSet && {
quota: item.quotaLimit,
@ -140,6 +143,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 +180,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;
@ -238,7 +248,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();
@ -337,7 +351,8 @@ 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;
@ -381,6 +396,7 @@ export default inject(
updateInfoPanelSelection,
changeRoomOwner,
changeRoomLifetime,
};
},
)(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,
@ -65,6 +82,8 @@ const QuickButtons = (props) => {
isPersonalRoom,
isArchiveFolder,
currentDeviceType,
showLifetimeIcon,
expiredDate,
} = props;
const isMobile = currentDeviceType === DeviceType.mobile;
@ -136,8 +155,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 && (
<ColorTheme
themeId={ThemeId.IconButton}
@ -206,7 +253,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,7 @@
// International. See the License terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode
import React, { useState, useEffect, useRef, useCallback } from "react";
import isEqual from "lodash/isEqual";
import TagHandler from "./handlers/TagHandler";
import SetRoomParams from "./sub-components/SetRoomParams";
import DialogHeader from "./sub-components/DialogHeader";
@ -76,7 +76,8 @@ 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)
);
};

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

@ -67,7 +67,9 @@ const Block = ({
const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
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);
@ -76,6 +78,7 @@ const VirtualDataRoomBlock = ({ t, roomParams, setRoomParams }) => {
};
const onChangeFileLifetime = () => {
if (fileLifetimeChecked) setRoomParams({ ...roomParams, lifetime: null });
setFileLifetimeChecked(!fileLifetimeChecked);
};
@ -103,7 +106,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")}

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

@ -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";
}
@ -182,6 +184,7 @@ class DetailsHelper {
"Date modified",
"Last modified by",
"Creation date",
this.item.expired && "Lifetime ends",
"Versions",
"Comments",
]
@ -214,6 +217,8 @@ class DetailsHelper {
return this.t("LastModifiedBy");
case "Creation date":
return this.t("CreationDate");
case "Lifetime ends":
return this.t("LifetimeEnds");
case "Versions":
return this.t("InfoPanel:Versions");
@ -261,6 +266,8 @@ class DetailsHelper {
return this.getAuthorDecoration("updatedBy");
case "Creation date":
return this.getItemCreationDate();
case "Lifetime ends":
return this.getItemExpiredDate();
case "Versions":
return this.getItemVersions();
@ -338,6 +345,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";
@ -50,6 +51,7 @@ import {
getCategoryUrl,
} from "SRC_DIR/helpers/utils";
import TariffBar from "SRC_DIR/components/TariffBar";
import { getLifetimePeriodTranslation } from "@docspace/shared/utils/common";
const StyledContainer = styled.div`
width: 100%;
@ -145,6 +147,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) => {
@ -211,6 +228,8 @@ const SectionHeaderContent = (props) => {
isPublicRoom,
theme,
isPublicRoomType,
isVirtualDataRoomType,
moveToPublicRoom,
currentDeviceType,
isFrame,
@ -474,6 +493,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;
@ -481,7 +511,10 @@ const SectionHeaderContent = (props) => {
return (
<Consumer key="header">
{(context) => (
<StyledContainer isRecycleBinFolder={isRecycleBinFolder}>
<StyledContainer
isRecycleBinFolder={isRecycleBinFolder}
isVirtualDataRoomType={isVirtualDataRoomType}
>
{tableGroupMenuVisible ? (
<TableGroupMenu {...tableGroupMenuProps} withComboBox />
) : (
@ -534,9 +567,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}
@ -663,6 +695,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;
@ -771,6 +804,7 @@ export default inject(
moveToRoomsPage,
onClickBack,
isPublicRoomType,
isVirtualDataRoomType,
isPublicRoom,
moveToPublicRoom,

View File

@ -126,6 +126,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,

View File

@ -48,6 +48,7 @@ class DialogsStore {
connectDialogVisible = false;
thirdPartyMoveDialogVisible = false;
deleteDialogVisible = false;
lifetimeDialogVisible = false;
downloadDialogVisible = false;
emptyTrashDialogVisible = false;
newFilesPanelVisible = false;
@ -232,6 +233,10 @@ class DialogsStore {
this.deleteDialogVisible = deleteDialogVisible;
};
setLifetimeDialogVisible = (lifetimeDialogVisible) => {
this.lifetimeDialogVisible = lifetimeDialogVisible;
};
setEventDialogVisible = (eventDialogVisible) => {
this.eventDialogVisible = eventDialogVisible;
};

View File

@ -87,6 +87,7 @@ 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";
class FilesActionStore {
settingsStore;
@ -2718,6 +2719,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];

View File

@ -3236,6 +3236,7 @@ class FilesStore {
inRoom,
requestToken,
indexing,
lifetime,
lastOpened,
quotaLimit,
usedSpace,
@ -3243,6 +3244,7 @@ class FilesStore {
providerId,
startFilling,
draftLocation,
expired,
} = item;
const thirdPartyIcon = this.thirdPartyStore.getThirdPartyIcon(
@ -3405,6 +3407,7 @@ class FilesStore {
...pluginOptions,
inRoom,
indexing,
lifetime,
type,
hasDraft,
isForm,
@ -3417,6 +3420,7 @@ class FilesStore {
providerId,
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";
@ -141,6 +145,8 @@ class SelectedFolderStore {
parentRoomType: Nullable<FolderType> = null;
lifetime: TRoomLifetime | null = null;
constructor(settingsStore: SettingsStore) {
makeAutoObservable(this);
this.settingsStore = settingsStore;
@ -185,6 +191,7 @@ class SelectedFolderStore {
type: this.type,
isRootFolder: this.isRootFolder,
parentRoomType: this.parentRoomType,
lifetime: this.lifetime,
};
};
@ -230,6 +237,7 @@ class SelectedFolderStore {
this.type = null;
this.inRoom = false;
this.parentRoomType = null;
this.lifetime = null;
};
setParentId = (parentId: number) => {
@ -252,9 +260,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

@ -35,7 +35,7 @@ import {
toUrlParams,
} from "../../utils/common";
import RoomsFilter from "./filter";
import { TGetRooms } from "./types";
import { TGetRooms, TRoomLifetime } from "./types";
export async function getRooms(filter: RoomsFilter, signal?: AbortSignal) {
let params;
@ -478,3 +478,17 @@ 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);
}

View File

@ -54,6 +54,12 @@ export type TRoomSecurity = {
CopySharedLink: boolean;
};
export type TRoomLifetime = {
deletePermanently: boolean;
period: number;
value: number;
};
export type TRoom = {
parentId: number;
filesCount: number;
@ -79,6 +85,7 @@ export type TRoom = {
updatedBy: TCreatedBy;
isArchive?: boolean;
security: TRoomSecurity;
lifetime: TRoomLifetime;
};
export type TGetRooms = {

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

@ -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,
@ -118,9 +122,10 @@ export const convertFilesToItems: (
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 +138,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 +164,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

@ -2004,6 +2004,8 @@ export const getBaseTheme = () => {
background: white,
rootFolderTitleColor: "#A3A9AE",
boxShadow: "0px 8px 16px 0px #040F1B14",
lifetimeIconFill: "#f2675a",
lifetimeIconStroke: "#f2675a",
icon: {
fill: "#316DAA",

View File

@ -1976,6 +1976,8 @@ const Dark: TTheme = {
background: black,
rootFolderTitleColor: "#ADADAD",
boxShadow: "0px 8px 16px 0px #040F1B29",
lifetimeIconFill: "none",
lifetimeIconStroke: "#657077",
icon: {
fill: "#E06A1B",

View File

@ -411,6 +411,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;

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