Merge pull request #546 from ONLYOFFICE/feature/public-edit

Feature/public edit
This commit is contained in:
Alexey Safronov 2024-08-05 16:30:26 +04:00 committed by GitHub
commit 9b5cb6934e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 475 additions and 262 deletions

View File

@ -44,9 +44,6 @@ import { ComboBox, TOption } from "@docspace/shared/components/combobox";
import { TData } from "@docspace/shared/components/toast/Toast.type";
import { TTranslation } from "@docspace/shared/types";
import { TColorScheme, TTheme } from "@docspace/shared/themes";
import { SettingsStore } from "@docspace/shared/store/SettingsStore";
import { UserStore } from "@docspace/shared/store/UserStore";
import {
ModalDialog,
ModalDialogType,
@ -65,8 +62,6 @@ import { StyledModalDialog, StyledBody } from "./StyledEmbeddingPanel";
import { DisplayBlock } from "./sub-components/DisplayBlock";
import { CheckboxElement } from "./sub-components/CheckboxElement";
import PublicRoomStore from "../../../store/PublicRoomStore";
import DialogsStore from "../../../store/DialogsStore";
type LinkParamsLinkShareToType = {
denyDownload: boolean;

View File

@ -29,7 +29,7 @@ import { inject, observer } from "mobx-react";
import { withTranslation } from "react-i18next";
import { toastr } from "@docspace/shared/components/toast";
import { RoomsType, ShareAccessRights } from "@docspace/shared/enums";
import { RoomsType } from "@docspace/shared/enums";
import { LINKS_LIMIT_COUNT } from "@docspace/shared/constants";
import InfoPanelViewLoader from "@docspace/shared/skeletons/info-panel/body";
import MembersHelper from "../../helpers/MembersHelper";
@ -182,6 +182,8 @@ const Members = ({
key="general-link"
link={primaryLink}
setIsScrollLocked={setIsScrollLocked}
isShareLink
isPrimaryLink
/>,
);
}
@ -193,6 +195,7 @@ const Members = ({
link={link}
key={link?.sharedTo?.id}
setIsScrollLocked={setIsScrollLocked}
isShareLink
/>,
);
});
@ -202,6 +205,7 @@ const Members = ({
key="create-additional-link"
className="additional-link"
onClick={onAddNewLink}
isShareLink
>
<div className="create-link-icon">
<IconButton size={12} iconName={PlusIcon} isDisabled />

View File

@ -24,70 +24,77 @@
// 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 React, { useState } from "react";
import { observer, inject } from "mobx-react";
import { withTranslation } from "react-i18next";
import copy from "copy-to-clipboard";
import { Avatar } from "@docspace/shared/components/avatar";
import { Link } from "@docspace/shared/components/link";
import { Text } from "@docspace/shared/components/text";
import { IconButton } from "@docspace/shared/components/icon-button";
import { ContextMenuButton } from "@docspace/shared/components/context-menu-button";
import moment from "moment";
import LinkRowComponent from "@docspace/shared/components/share/sub-components/LinkRow";
import { toastr } from "@docspace/shared/components/toast";
import CopyReactSvgUrl from "PUBLIC_DIR/images/copy.react.svg?url";
import LinkReactSvgUrl from "PUBLIC_DIR/images/tablet-link.react.svg?url";
import SettingsReactSvgUrl from "PUBLIC_DIR/images/catalog.settings.react.svg?url";
import ShareReactSvgUrl from "PUBLIC_DIR/images/share.react.svg?url";
import CodeReactSvgUrl from "PUBLIC_DIR/images/code.react.svg?url";
import CopyToReactSvgUrl from "PUBLIC_DIR/images/copyTo.react.svg?url";
import OutlineReactSvgUrl from "PUBLIC_DIR/images/outline-true.react.svg?url";
import LockedReactSvgUrl from "PUBLIC_DIR/images/locked.react.svg?url";
import LoadedReactSvgUrl from "PUBLIC_DIR/images/loaded.react.svg?url";
import TrashReactSvgUrl from "PUBLIC_DIR/images/trash.react.svg?url";
import ClockReactSvg from "PUBLIC_DIR/images/clock.react.svg";
import moment from "moment-timezone";
import { RoomsType } from "@docspace/shared/enums";
import { TTranslation } from "@docspace/shared/types";
import { TFileLink } from "@docspace/shared/api/files/types";
import { useState } from "react";
import { TOption } from "@docspace/shared/components/combobox";
import { StyledLinkRow } from "./Styled";
type LinkRowProps = {
t: TTranslation;
link: TFileLink;
roomId: string | number;
setLinkParams: (linkParams: {
roomId: number | string;
isEdit?: boolean;
link: TFileLink;
isPublic?: boolean;
isFormRoom?: boolean;
}) => void;
setEditLinkPanelIsVisible: (value: boolean) => void;
setDeleteLinkDialogVisible: (value: boolean) => void;
setEmbeddingPanelData: (value: {
visible: boolean;
itemId?: string | number;
}) => void;
const LinkRow = (props) => {
isArchiveFolder: boolean;
setIsScrollLocked: (isScrollLocked: boolean) => void;
isPublicRoomType: boolean;
isFormRoom: boolean;
isPrimaryLink: boolean;
};
const LinkRow = (props: LinkRowProps) => {
const {
t,
link,
roomId,
setLinkParams,
setExternalLink,
editExternalLink,
setEditLinkPanelIsVisible,
setDeleteLinkDialogVisible,
setEmbeddingPanelData,
isArchiveFolder,
theme,
setIsScrollLocked,
isPublicRoomType,
isFormRoom,
...rest
isPrimaryLink,
editExternalLink,
setExternalLink,
} = props;
const [isLoading, setIsLoading] = useState(false);
const {
title,
shareLink,
password,
disabled,
expirationDate,
isExpired,
primary,
} = link.sharedTo;
const { shareLink, password, isExpired, primary } = link.sharedTo;
const isLocked = !!password;
const expiryDate = !!expirationDate;
const date = moment(expirationDate).tz(window.timezone).format("LLL");
const isDisabled = isExpired;
const tooltipContent = isExpired
? t("Translations:LinkHasExpiredAndHasBeenDisabled")
: t("Translations:PublicRoomLinkValidTime", { date });
const [loadingLinks, setLoadingLinks] = useState<(string | number)[]>([]);
const onCloseContextMenu = () => {
setIsScrollLocked(false);
};
const onEditLink = () => {
setEditLinkPanelIsVisible(true);
@ -101,33 +108,11 @@ const LinkRow = (props) => {
onCloseContextMenu();
};
// const onDisableLink = () => {
// if (isExpired) {
// setEditLinkPanelIsVisible(true);
// setLinkParams({ isEdit: true, link, roomId, isPublic: isPublicRoomType, isFormRoom });
// return;
// }
// setIsLoading(true);
// const newLink = JSON.parse(JSON.stringify(link));
// newLink.sharedTo.disabled = !newLink.sharedTo.disabled;
// editExternalLink(roomId, newLink)
// .then((link) => {
// setExternalLink(link);
// disabled
// ? toastr.success(t("Files:LinkEnabledSuccessfully"))
// : toastr.success(t("Files:LinkDisabledSuccessfully"));
// })
// .catch((err) => toastr.error(err?.message))
// .finally(() => setIsLoading(false));
// };
const onCopyPassword = () => {
copy(password);
toastr.success(t("Files:PasswordSuccessfullyCopied"));
if (password) {
copy(password);
toastr.success(t("Files:PasswordSuccessfullyCopied"));
}
};
const onEmbeddingClick = () => {
@ -152,12 +137,6 @@ const LinkRow = (props) => {
setIsScrollLocked(true);
};
const onCloseContextMenu = () => {
setIsScrollLocked(false);
};
const isDisabled = disabled || isExpired;
const getData = () => {
return [
{
@ -166,17 +145,6 @@ const LinkRow = (props) => {
icon: SettingsReactSvgUrl,
onClick: onEditLink,
},
// {
// key: "edit-link-separator",
// isSeparator: true,
// },
// {
// key: "share-key",
// label: t("Files:Share"),
// icon: ShareReactSvgUrl,
// // onClick: () => args.onClickLabel("label2"),
// },
!isDisabled && {
key: "copy-link-settings-key",
label: t("Files:CopySharedLink"),
@ -184,27 +152,21 @@ const LinkRow = (props) => {
onClick: onCopyExternalLink,
},
!isDisabled &&
isLocked && {
key: "copy-link-password-key",
label: t("Files:CopyLinkPassword"),
icon: LockedReactSvgUrl,
onClick: onCopyPassword,
},
!isDisabled && {
key: "embedding-settings-key",
label: t("Files:EmbeddingSettings"),
label: t("Files:Embed"),
icon: CodeReactSvgUrl,
onClick: onEmbeddingClick,
},
// disabled
// ? {
// key: "enable-link-key",
// label: t("Files:EnableLink"),
// icon: LoadedReactSvgUrl,
// onClick: onDisableLink,
// }
// : {
// key: "disable-link-key",
// label: t("Files:DisableLink"),
// icon: OutlineReactSvgUrl,
// onClick: onDisableLink,
// },
{
key: "delete-link-separator",
isSeparator: true,
@ -222,91 +184,66 @@ const LinkRow = (props) => {
];
};
const textColor = disabled ? theme.text.disableColor : theme.text.color;
const editExternalLinkAction = (newLink: TFileLink) => {
setLoadingLinks([newLink.sharedTo.id]);
editExternalLink(roomId, newLink)
.then((linkData: TFileLink) => {
setExternalLink(linkData);
setLinkParams({
link: linkData,
roomId,
isPublic: isPublicRoomType,
isFormRoom,
});
copy(link?.sharedTo?.shareLink);
toastr.success(t("Files:LinkEditedSuccessfully"));
})
.catch((err: Error) => toastr.error(err?.message))
.finally(() => setLoadingLinks([]));
};
const onAccessRightsSelect = (opt: TOption) => {
const newLink = { ...link };
if (opt.access) newLink.access = opt.access;
editExternalLinkAction(newLink);
};
const changeExpirationOption = async (
linkData: TFileLink,
expirationDate: moment.Moment | null,
) => {
const newLink = { ...link };
newLink.sharedTo.expirationDate = expirationDate
? moment(expirationDate)
: null;
editExternalLinkAction(newLink);
};
return (
<StyledLinkRow
{...rest}
<LinkRowComponent
loadingLinks={loadingLinks}
links={[link]}
getData={getData}
onOpenContextMenu={onOpenContextMenu}
onCloseContextMenu={onCloseContextMenu}
isRoomsLink
isPrimaryLink={isPrimaryLink}
onAccessRightsSelect={onAccessRightsSelect}
changeExpirationOption={changeExpirationOption}
isArchiveFolder={isArchiveFolder}
isExpired={isExpired}
>
<Avatar
size="min"
source={LinkReactSvgUrl}
roleIcon={
expiryDate ? (
<div className="clock-icon">
<ClockReactSvg />
</div>
) : null
}
withTooltip={expiryDate}
tooltipContent={tooltipContent}
/>
{isArchiveFolder ? (
<Text fontSize="14px" fontWeight={600} className="external-row-link">
{title}
</Text>
) : (
<Link
type="action"
fontSize="14px"
fontWeight={600}
onClick={onEditLink}
isDisabled={disabled}
color={textColor}
className="external-row-link"
>
{title}
</Link>
)}
{disabled && <Text color={textColor}>{t("Settings:Disabled")}</Text>}
<div className="external-row-icons">
{!disabled && !isExpired && !isArchiveFolder && (
<>
{isLocked && (
<IconButton
className="locked-icon"
size={16}
iconName={LockedReactSvgUrl}
onClick={onCopyPassword}
title={t("Files:CopyLinkPassword")}
/>
)}
<IconButton
className="copy-icon"
size={16}
iconName={CopyReactSvgUrl}
onClick={onCopyExternalLink}
title={t("Files:CopySharedLink")}
/>
</>
)}
{!isArchiveFolder && (
<ContextMenuButton
getData={getData}
isDisabled={isLoading}
title={t("Files:ShowLinkActions")}
directionY="both"
onClick={onOpenContextMenu}
onClose={onCloseContextMenu}
/>
)}
</div>
</StyledLinkRow>
/>
);
};
export default inject(
export default inject<TStore>(
({
settingsStore,
dialogsStore,
publicRoomStore,
treeFoldersStore,
infoPanelStore,
publicRoomStore,
}) => {
const { infoPanelSelection } = infoPanelStore;
const { theme } = settingsStore;
@ -317,23 +254,25 @@ export default inject(
setEmbeddingPanelData,
setLinkParams,
} = dialogsStore;
const { editExternalLink, setExternalLink } = publicRoomStore;
const { isArchiveFolderRoot } = treeFoldersStore;
const { id, roomType } = infoPanelSelection!;
const { editExternalLink, setExternalLink } = publicRoomStore;
return {
setLinkParams,
editExternalLink,
roomId: infoPanelSelection.id,
setExternalLink,
roomId: id,
setEditLinkPanelIsVisible,
setDeleteLinkDialogVisible,
setEmbeddingPanelData,
isArchiveFolder: isArchiveFolderRoot,
theme,
isPublicRoomType:
infoPanelSelection.roomType === RoomsType.PublicRoom ||
infoPanelSelection.roomType === RoomsType.FormRoom,
isFormRoom: infoPanelSelection?.roomType === RoomsType.FormRoom,
roomType === RoomsType.PublicRoom || roomType === RoomsType.FormRoom,
isFormRoom: roomType === RoomsType.FormRoom,
editExternalLink,
setExternalLink,
};
},
)(

View File

@ -78,6 +78,7 @@ const StyledList = styled(List)`
`;
const itemSize = 48;
const shareLinkItemSize = 68;
const MembersList = (props) => {
const {
@ -151,6 +152,16 @@ const MembersList = (props) => {
[isNextPageLoading, loadNextPage],
);
const getItemSize = ({ index }) => {
const elem = list[index];
if (elem?.props?.isShareLink) {
return shareLinkItemSize;
}
return itemSize;
};
const onScroll = (e) => {
const header = document.getElementById("members-list-header");
@ -221,7 +232,7 @@ const MembersList = (props) => {
onRowsRendered={onRowsRendered}
ref={registerChild}
rowCount={itemsCount}
rowHeight={itemSize}
rowHeight={getItemSize}
rowRenderer={renderRow}
width={width}
isScrolling={isScrolling}

View File

@ -78,7 +78,6 @@ class InfoPanelStore {
infoPanelSelection = null;
selectionHistory = null;
selectionHistory = null;
roomsView = infoMembers;
fileView = infoHistory;
@ -94,7 +93,6 @@ class InfoPanelStore {
publicRoomStore = null;
infoPanelMembers = null;
infoPanelSelection = null;
infoPanelRoom = null;
membersIsLoading = false;
isMembersPanelUpdating = false;

View File

@ -24,6 +24,7 @@
// 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 moment from "moment";
import { TCreatedBy, TPathParts } from "../../types";
import { TUser } from "../people/types";
import {
@ -395,9 +396,11 @@ export type TFileLink = {
requestToken: string;
shareLink: string;
title: string;
expirationDate?: string;
expirationDate?: moment.Moment | null;
internal?: boolean;
password?: string;
};
subjectType: number;
};
export type TFilesUsedSpace = {

View File

@ -27,7 +27,8 @@
/* eslint-disable @typescript-eslint/default-param-last */
import { AxiosRequestConfig } from "axios";
import { FolderType, MembersSubjectType } from "../../enums";
import moment from "moment";
import { FolderType, MembersSubjectType, ShareAccessRights } from "../../enums";
import { request } from "../client";
import {
checkFilterInstance,
@ -397,15 +398,15 @@ export const acceptInvitationByLink = async () => {
};
export function editExternalLink(
roomId,
linkId,
title,
access,
expirationDate,
linkType,
password,
disabled,
denyDownload,
roomId: number | string,
linkId: number | string,
title: string,
access: ShareAccessRights,
expirationDate: moment.Moment,
linkType: number,
password: string,
disabled: boolean,
denyDownload: boolean,
) {
const skipRedirect = true;

View File

@ -24,7 +24,7 @@
// 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 from "styled-components";
import styled, { css } from "styled-components";
import { Base } from "../../themes";
import { mobile } from "../../utils";
@ -36,6 +36,25 @@ const StyledWrapper = styled(ComboBox)`
padding-inline: 8px;
}
${({ type, theme }) =>
type === "onlyIcon" &&
css`
.combo-button {
padding-right: 4px;
}
.combo-button_selected-icon-container {
margin-right: 0px;
}
.combo-buttons_arrow-icon,
.combo-button_selected-icon-container {
svg path {
fill: ${theme.iconButton.color};
}
}
`}
@media ${mobile} {
.backdrop-active {
top: -64px;
@ -77,8 +96,16 @@ const StyledItemDescription = styled.div`
StyledItemDescription.defaultProps = { theme: Base };
const StyledItemIcon = styled.img`
const StyledItemIcon = styled.img<{ isShortenIcon?: boolean }>`
margin-inline-end: 8px;
${({ isShortenIcon }) =>
isShortenIcon &&
css`
padding-top: 2px;
width: 12px;
height: 12px;
`}
`;
const StyledItemContent = styled.div`

View File

@ -46,6 +46,7 @@ export const AccessRightSelectPure = ({
advancedOptions,
selectedOption,
className,
type,
...props
}: AccessRightSelectProps) => {
const [currentItem, setCurrentItem] = useState(selectedOption);
@ -76,7 +77,12 @@ export const AccessRightSelectPure = ({
onClick={() => onSelectCurrentItem(item)}
>
<StyledItem>
{item.icon && <StyledItemIcon src={item.icon} />}
{item.icon && (
<StyledItemIcon
src={item.icon}
isShortenIcon={type === "onlyIcon"}
/>
)}
<StyledItemContent>
<StyledItemTitle>
{item.label}
@ -108,6 +114,7 @@ export const AccessRightSelectPure = ({
return (
<StyledWrapper
className={className}
type={type}
advancedOptions={formattedOptions}
onSelect={onSelectCurrentItem}
options={[]}
@ -116,7 +123,7 @@ export const AccessRightSelectPure = ({
icon: currentItem?.icon,
default: true,
key: currentItem?.key,
label: currentItem?.label,
label: type === "onlyIcon" ? "" : currentItem?.label,
} as TOption
}
forceCloseClickOutside

View File

@ -44,6 +44,9 @@ type PropsFromCombobox = Pick<
| "withoutBackground"
| "withBackground"
| "withBlur"
| "type"
| "noBorder"
| "isDisabled"
>;
export type AccessRightSelectProps = PropsFromCombobox & {

View File

@ -77,7 +77,7 @@ export interface ContextMenuButtonProps {
/** Sets the number of columns */
columnCount?: number;
/** Sets the display type */
displayType: ContextMenuButtonDisplayType;
displayType?: ContextMenuButtonDisplayType;
/** Closing event */
onClose?: () => void;
/** Sets the drop down open with the portal */

View File

@ -116,6 +116,39 @@ export const getAccessOptions = (
return items;
};
export const getRoomAccessOptions = (t: TTranslation) => {
return [
{
access: ShareAccessRights.Editing,
description: t("Translations:RoleEditorDescription"),
key: "editing",
label: t("Common:Editor"),
icon: AccessEditReactSvgUrl,
},
{
access: ShareAccessRights.Review,
description: t("Translations:RoleReviewerDescription"),
key: "review",
label: t("Translations:RoleReviewer"),
icon: AccessReviewReactSvgUrl,
},
{
access: ShareAccessRights.Comment,
description: t("Translations:RoleCommentatorDescription"),
key: "commenting",
label: t("Commentator"),
icon: AccessCommentReactSvgUrl,
},
{
access: ShareAccessRights.ReadOnly,
description: t("Translations:RoleViewerDescription"),
key: "viewing",
label: t("JavascriptSdk:Viewer"),
icon: EyeReactSvgUrl,
},
];
};
export const getExpiredOptions = (
t: TTranslation,
setTwelveHours: VoidFunction,

View File

@ -24,7 +24,8 @@
// 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 from "styled-components";
import styled, { css } from "styled-components";
import { DropDown } from "../drop-down";
const StyledLinks = styled.div`
margin-top: 20px;
@ -49,11 +50,30 @@ const StyledLinks = styled.div`
}
`;
const StyledLinkRow = styled.div`
padding: 8px 0;
const StyledLinkRow = styled.div<{ isExpired?: boolean; isDisabled?: boolean }>`
display: flex;
gap: 8px;
align-items: center;
height: 68px;
opacity: ${({ isDisabled }) => (isDisabled ? 0.4 : 1)};
.avatar-wrapper,
.avatar-wrapper:hover {
svg {
path {
fill: ${({ theme }) => theme.infoPanel.avatarColor};
}
}
}
.avatar_role-wrapper {
svg {
path:nth-child(3) {
fill: ${({ theme }) => theme.backgroundColor};
}
}
}
.combo-box {
padding: 0;
@ -67,23 +87,41 @@ const StyledLinkRow = styled.div`
display: flex;
flex-direction: column;
gap: 0;
overflow: hidden;
}
.internal-combobox {
padding: 0px;
.combo-button-label {
font-size: 14px;
}
}
.internal-combobox_expiered {
.link-options_title {
font-size: 14px;
font-weight: 600;
line-height: 16px;
margin: 6px 8px;
color: ${({ theme }) => theme.infoPanel.members.linkAccessComboboxExpired};
${({ theme, isExpired }) =>
isExpired &&
css`
color: ${theme.infoPanel.members.linkAccessComboboxExpired};
`};
}
.expired-options {
padding: 0px;
.text {
color: ${({ theme }) => theme.infoPanel.links.color};
:hover {
color: ${({ theme }) => theme.infoPanel.links.color};
background: unset;
}
}
& > span > a {
padding: 0px !important;
}
@ -91,6 +129,7 @@ const StyledLinkRow = styled.div`
.expire-text {
margin-inline-start: 8px;
color: ${({ theme }) => theme.infoPanel.links.primaryColor};
}
.link-actions {
@ -98,6 +137,11 @@ const StyledLinkRow = styled.div`
gap: 16px;
align-items: center;
margin-inline-start: auto;
.link-row_copy-icon {
min-width: 16px;
min-height: 16px;
}
}
.loader {
@ -131,4 +175,10 @@ const StyledSquare = styled.div`
}
`;
export { StyledLinks, StyledLinkRow, StyledSquare };
const StyledDropDown = styled(DropDown)`
.share-link_calendar {
position: fixed;
}
`;
export { StyledLinks, StyledLinkRow, StyledSquare, StyledDropDown };

View File

@ -39,21 +39,50 @@ export type ShareCalendarProps = {
closeCalendar: (formattedDate: moment.Moment) => void;
calendarRef: React.RefObject<HTMLDivElement>;
locale: string;
bodyRef?: React.MutableRefObject<HTMLDivElement | null>;
useDropDown?: boolean;
};
export type TLink = TFileLink | { isLoaded: boolean };
export type LinkRowProps = {
onAddClick: () => Promise<void>;
links: TLink[] | null;
changeShareOption: (item: TOption, link: TFileLink) => Promise<void>;
changeAccessOption: (item: TOption, link: TFileLink) => Promise<void>;
changeExpirationOption: (
link: TFileLink,
expirationDate: moment.Moment | null,
) => Promise<void>;
availableExternalRights: TAvailableExternalRights;
loadingLinks: (string | number)[];
};
export type LinkRowProps =
| {
onAddClick: () => Promise<void>;
links: TLink[] | null;
changeShareOption: (item: TOption, link: TFileLink) => Promise<void>;
changeAccessOption: (item: TOption, link: TFileLink) => Promise<void>;
changeExpirationOption: (
link: TFileLink,
expirationDate: moment.Moment | null,
) => Promise<void>;
availableExternalRights: TAvailableExternalRights;
loadingLinks: (string | number)[];
isRoomsLink?: undefined;
isPrimaryLink?: undefined;
isArchiveFolder?: undefined;
getData: () => undefined;
onOpenContextMenu?: undefined;
onCloseContextMenu?: undefined;
onAccessRightsSelect?: undefined;
}
| {
onAddClick: () => Promise<void>;
links: TLink[] | null;
changeShareOption: (item: TOption, link: TFileLink) => Promise<void>;
changeAccessOption: (item: TOption, link: TFileLink) => Promise<void>;
changeExpirationOption: (
link: TFileLink,
expirationDate: moment.Moment | null,
) => Promise<void>;
availableExternalRights: TAvailableExternalRights;
loadingLinks: (string | number)[];
isRoomsLink?: boolean;
isPrimaryLink: boolean;
isArchiveFolder: boolean;
getData: () => ContextMenuModel[];
onOpenContextMenu: (e: React.MouseEvent) => void;
onCloseContextMenu: () => void;
onAccessRightsSelect: (option: TOption) => void;
};
export type ExpiredComboBoxProps = {
link: TFileLink;
@ -62,6 +91,9 @@ export type ExpiredComboBoxProps = {
expirationDate: moment.Moment | null,
) => Promise<void>;
isDisabled?: boolean;
isRoomsLink?: boolean;
changeAccessOption: (item: TOption, link: TFileLink) => Promise<void>;
accessOptions: TOption[];
};
export type ShareProps = {

View File

@ -244,11 +244,7 @@ const Share = (props: ShareProps) => {
if (item.access === ShareAccessRights.None) {
deleteLink(link.sharedTo.id);
if (link.sharedTo.primary) {
toastr.success(t("Common:GeneralAccessLinkRemove"));
} else {
toastr.success(t("Common:AdditionalLinkRemove"));
}
toastr.success(t("Common:LinkRemoved"));
} else {
updateLink(link, res);
if (item.access === ShareAccessRights.DenyAccess) {

View File

@ -37,11 +37,15 @@ import { getExpiredOptions } from "../Share.helpers";
import { ExpiredComboBoxProps } from "../Share.types";
import ShareCalendar from "./ShareCalendar";
import { ShareAccessRights } from "../../../enums";
const ExpiredComboBox = ({
link,
changeExpirationOption,
isDisabled,
isRoomsLink,
changeAccessOption,
accessOptions,
}: ExpiredComboBoxProps) => {
const { t, i18n } = useTranslation(["Common"]);
const calendarRef = useRef<HTMLDivElement | null>(null);
@ -107,8 +111,9 @@ const ExpiredComboBox = ({
return { date: calculatedDate + 1, label: t("Common:Days") };
};
const onRegenerateClick = () => {
setSevenDays();
const onRemoveLink = () => {
const opt = accessOptions.find((o) => o.access === ShareAccessRights.None);
if (opt) changeAccessOption(opt, link);
};
useEffect(() => {
@ -150,7 +155,7 @@ const ExpiredComboBox = ({
</Trans>
);
}
const date = t("Common:Unlimited");
const date = t("Common:Unlimited").toLowerCase();
return (
<Trans t={t} i18nKey="LinkIsValid" ns="Common">
@ -181,9 +186,9 @@ const ExpiredComboBox = ({
fontWeight={400}
fontSize="12px"
color="#4781D1"
onClick={onRegenerateClick}
onClick={onRemoveLink}
>
{t("Common:Regenerate")}
{t("Common:RemoveLink")}
</Link>
</Text>
) : (
@ -193,10 +198,12 @@ const ExpiredComboBox = ({
)}
{showCalendar && (
<ShareCalendar
bodyRef={bodyRef}
onDateSet={setDateFromCalendar}
calendarRef={calendarRef}
closeCalendar={onCalendarClose}
locale={i18n.language}
useDropDown={isRoomsLink}
/>
)}
</div>

View File

@ -30,6 +30,7 @@ import PlusIcon from "PUBLIC_DIR/images/plus.react.svg?url";
import UniverseIcon from "PUBLIC_DIR/images/universe.react.svg?url";
import PeopleIcon from "PUBLIC_DIR/images/people.react.svg?url";
import CopyIcon from "PUBLIC_DIR/images/copy.react.svg?url";
import LockedReactSvg from "PUBLIC_DIR/images/icons/12/locked.react.svg";
import { RowSkeleton } from "../../../skeletons/share";
import { TFileLink } from "../../../api/files/types";
@ -43,11 +44,18 @@ import { Loader, LoaderTypes } from "../../loader";
import { Text } from "../../text";
import { StyledLinkRow, StyledSquare } from "../Share.styled";
import { getShareOptions, getAccessOptions } from "../Share.helpers";
import {
getShareOptions,
getAccessOptions,
getRoomAccessOptions,
} from "../Share.helpers";
import { LinkRowProps } from "../Share.types";
import ExpiredComboBox from "./ExpiredComboBox";
import { AccessRightSelect } from "../../access-right-select";
import { ContextMenuButton } from "../../context-menu-button";
const LinkRow = ({
onAddClick,
links,
@ -56,11 +64,22 @@ const LinkRow = ({
changeExpirationOption,
availableExternalRights,
loadingLinks,
isRoomsLink,
isPrimaryLink,
isArchiveFolder,
getData,
onOpenContextMenu,
onCloseContextMenu,
onAccessRightsSelect,
}: LinkRowProps) => {
const { t } = useTranslation(["Common"]);
const { t } = useTranslation(["Common", "Translations"]);
const shareOptions = getShareOptions(t) as TOption[];
const accessOptions = getAccessOptions(t, availableExternalRights);
const accessOptions = availableExternalRights
? getAccessOptions(t, availableExternalRights)
: [];
const roomAccessOptions = isRoomsLink ? getRoomAccessOptions(t) : [];
const onCopyLink = (link: TFileLink) => {
copyShareLink(link.sharedTo.shareLink);
@ -88,14 +107,26 @@ const LinkRow = ({
(option) =>
option && "access" in option && option.access === link.access,
);
const roomSelectedOptions = roomAccessOptions.find(
(option) =>
option && "access" in option && option.access === link.access,
);
const avatar = shareOption?.key === "anyone" ? UniverseIcon : PeopleIcon;
const isExpiredLink = link.sharedTo.isExpired;
const isLocked = !!link.sharedTo.password;
const linkTitle = link.sharedTo.title;
const isLoaded = loadingLinks.includes(link.sharedTo.id);
return (
<StyledLinkRow key={`share-link-row-${index * 5}`}>
<StyledLinkRow
isExpired={isExpiredLink}
key={`share-link-row-${index * 5}`}
isDisabled={isArchiveFolder}
>
{isLoaded ? (
<Loader className="loader" size="20px" type={LoaderTypes.track} />
) : (
@ -103,10 +134,15 @@ const LinkRow = ({
size={AvatarSize.min}
role={AvatarRole.user}
source={avatar}
roleIcon={isLocked ? <LockedReactSvg /> : undefined}
/>
)}
<div className="link-options">
{!isExpiredLink ? (
{isRoomsLink ? (
<Text className="link-options_title" truncate>
{linkTitle}
</Text>
) : !isExpiredLink ? (
<ComboBox
className="internal-combobox"
directionY="both"
@ -122,39 +158,71 @@ const LinkRow = ({
isDisabled={isLoaded}
/>
) : (
<Text className="internal-combobox_expiered">
{shareOption?.label}
</Text>
<Text className="link-options_title">{shareOption?.label}</Text>
)}
{!isPrimaryLink && (
<ExpiredComboBox
link={link}
accessOptions={accessOptions}
changeExpirationOption={changeExpirationOption}
isDisabled={isLoaded || isArchiveFolder}
isRoomsLink={isRoomsLink}
changeAccessOption={changeAccessOption}
/>
)}
<ExpiredComboBox
link={link}
changeExpirationOption={changeExpirationOption}
isDisabled={isLoaded}
/>
</div>
<div className="link-actions">
<IconButton
size={16}
iconName={CopyIcon}
onClick={() => onCopyLink(link)}
title={t("Common:CreateAndCopy")}
isDisabled={isExpiredLink || isLoaded}
/>
<ComboBox
directionY="both"
options={accessOptions}
selectedOption={accessOption ?? ({} as TOption)}
onSelect={(item) => changeAccessOption(item, link)}
scaled={false}
scaledOptions={false}
showDisabledItems
size={ComboBoxSize.content}
fillIcon
modernView
type="onlyIcon"
isDisabled={isExpiredLink || isLoaded}
manualWidth="fit-content"
/>
{!isArchiveFolder && (
<IconButton
size={16}
className="link-row_copy-icon"
iconName={CopyIcon}
onClick={() => onCopyLink(link)}
title={t("Common:CreateAndCopy")}
isDisabled={isExpiredLink || isLoaded}
/>
)}
{isRoomsLink ? (
<>
<AccessRightSelect
selectedOption={roomSelectedOptions ?? ({} as TOption)}
onSelect={onAccessRightsSelect}
accessOptions={roomAccessOptions}
noBorder
directionX="right"
directionY="bottom"
type="onlyIcon"
manualWidth="300px"
isDisabled={isExpiredLink || isLoaded || isArchiveFolder}
/>
{!isArchiveFolder && (
<ContextMenuButton
getData={getData}
title={t("Files:ShowLinkActions")}
directionY="both"
onClick={onOpenContextMenu}
onClose={onCloseContextMenu}
isDisabled={isExpiredLink || isLoaded}
/>
)}
</>
) : (
<ComboBox
directionY="both"
options={accessOptions}
selectedOption={accessOption ?? ({} as TOption)}
onSelect={(item) => changeAccessOption(item, link)}
scaled={false}
scaledOptions={false}
showDisabledItems
size={ComboBoxSize.content}
fillIcon
modernView
type="onlyIcon"
isDisabled={isExpiredLink || isLoaded}
manualWidth="fit-content"
/>
)}
</div>
</StyledLinkRow>
);

View File

@ -31,6 +31,7 @@ import { isMobile } from "../../../utils/device";
import { Calendar } from "../../calendar";
import { ShareCalendarProps } from "../Share.types";
import { StyledDropDown } from "../Share.styled";
const StyledCalendar = styled(Calendar)`
position: absolute;
@ -50,12 +51,15 @@ const ShareCalendar = ({
closeCalendar,
calendarRef,
locale,
bodyRef,
useDropDown,
}: ShareCalendarProps) => {
const selectedDate = moment();
const maxDate = moment().add(10, "years");
return (
const calendarComponent = (
<StyledCalendar
className="share-link_calendar"
selectedDate={selectedDate}
setSelectedDate={onDateSet}
onChange={closeCalendar}
@ -66,6 +70,19 @@ const ShareCalendar = ({
maxDate={maxDate}
/>
);
return useDropDown ? (
<StyledDropDown
open
isDefaultMode
forwardedRef={bodyRef}
eventTypes={["mousedown"]}
>
{calendarComponent}
</StyledDropDown>
) : (
calendarComponent
);
};
export default ShareCalendar;

View File

@ -2085,8 +2085,10 @@ export const getBaseTheme = () => {
closeButtonBg: "transparent",
nameColor: "#858585",
avatarColor: "#555F65",
links: {
color: "#4781D1",
iconColor: "#3B72A7",
iconErrorColor: "#F24724",
primaryColor: "#555F65",

View File

@ -2057,8 +2057,10 @@ const Dark: TTheme = {
closeButtonBg: "#a2a2a2",
nameColor: "#A3A9AE",
avatarColor: "#A3A9AE",
links: {
color: "#4781D1",
iconColor: "#858585",
iconErrorColor: "#E06451",
primaryColor: "#ADADAD",

View File

@ -0,0 +1,15 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_622_21524)">
<mask id="path-1-outside-1_622_21524" maskUnits="userSpaceOnUse" x="0" y="0" width="12" height="14" fill="black">
<rect fill="white" width="12" height="14"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4C3 2.34315 4.34315 1 6 1C7.65685 1 9 2.34315 9 4V5C10.1046 5 11 5.89543 11 7V11C11 12.1046 10.1046 13 9 13H3C1.89543 13 1 12.1046 1 11V7C1 5.89543 1.89543 5 3 5V4ZM7 4V5H5V4C5 3.44772 5.44772 3 6 3C6.55228 3 7 3.44772 7 4ZM3 7V11H9V7H3ZM6 10C6.55228 10 7 9.55229 7 9C7 8.44772 6.55228 8 6 8C5.44772 8 5 8.44772 5 9C5 9.55229 5.44772 10 6 10Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4C3 2.34315 4.34315 1 6 1C7.65685 1 9 2.34315 9 4V5C10.1046 5 11 5.89543 11 7V11C11 12.1046 10.1046 13 9 13H3C1.89543 13 1 12.1046 1 11V7C1 5.89543 1.89543 5 3 5V4ZM7 4V5H5V4C5 3.44772 5.44772 3 6 3C6.55228 3 7 3.44772 7 4ZM3 7V11H9V7H3ZM6 10C6.55228 10 7 9.55229 7 9C7 8.44772 6.55228 8 6 8C5.44772 8 5 8.44772 5 9C5 9.55229 5.44772 10 6 10Z" fill="#4781D1"/>
<path d="M9 5H8C8 5.55228 8.44772 6 9 6V5ZM3 5V6C3.55228 6 4 5.55228 4 5H3ZM7 5V6C7.55228 6 8 5.55228 8 5H7ZM5 5H4C4 5.55228 4.44772 6 5 6V5ZM3 11H2C2 11.5523 2.44772 12 3 12V11ZM3 7V6C2.44772 6 2 6.44772 2 7H3ZM9 11V12C9.55228 12 10 11.5523 10 11H9ZM9 7H10C10 6.44772 9.55228 6 9 6V7ZM6 0C3.79086 0 2 1.79086 2 4H4C4 2.89543 4.89543 2 6 2V0ZM10 4C10 1.79086 8.20914 0 6 0V2C7.10457 2 8 2.89543 8 4H10ZM10 5V4H8V5H10ZM12 7C12 5.34315 10.6569 4 9 4V6C9.55228 6 10 6.44772 10 7H12ZM12 11V7H10V11H12ZM9 14C10.6569 14 12 12.6569 12 11H10C10 11.5523 9.55228 12 9 12V14ZM3 14H9V12H3V14ZM0 11C0 12.6569 1.34315 14 3 14V12C2.44772 12 2 11.5523 2 11H0ZM0 7V11H2V7H0ZM3 4C1.34315 4 0 5.34315 0 7H2C2 6.44772 2.44772 6 3 6V4ZM2 4V5H4V4H2ZM8 5V4H6V5H8ZM5 6H7V4H5V6ZM4 4V5H6V4H4ZM6 2C4.89543 2 4 2.89543 4 4H6V2ZM8 4C8 2.89543 7.10457 2 6 2V4H8ZM4 11V7H2V11H4ZM9 10H3V12H9V10ZM8 7V11H10V7H8ZM3 8H9V6H3V8ZM6 9V11C7.10457 11 8 10.1046 8 9H6ZM6 9H8C8 7.89543 7.10457 7 6 7V9ZM6 9V7C4.89543 7 4 7.89543 4 9H6ZM6 9H4C4 10.1046 4.89543 11 6 11V9Z" fill="white" mask="url(#path-1-outside-1_622_21524)"/>
</g>
<defs>
<clipPath id="clip0_622_21524">
<rect width="12" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -147,6 +147,7 @@
"EditButton": "Edit",
"Editing": "Editing",
"Editor": "Editor",
"Commentator": "Commentator",
"Email": "Email",
"EmptyDescription": "The list of users previously invited to {{productName}} or separate rooms will appear here. You will be able to invite these users for collaboration at any time.",
"EmptyEmail": "No email address parsed",
@ -386,6 +387,8 @@
"RecoverDescribeYourProblemPlaceholder": "Describe your problem",
"RecoverTitle": "Access recovery",
"Regenerate": "Regenerate",
"RemoveLink": "Remove link",
"LinkRemoved": "Link removed",
"RegistrationEmail": "Your registration email address",
"ReloadPage": "Reload page",
"Remember": "Remember me",