Merge branch 'release/v1.2' of github.com:ONLYOFFICE/AppServer into release/v1.2

This commit is contained in:
Artem Tarasov 2022-05-12 17:43:09 +03:00
commit bbcbb79a74
222 changed files with 1821 additions and 981 deletions

View File

@ -345,6 +345,18 @@ namespace ASC.Core.Notify.Signalr
}
}
public void UpdateFile<T>(T fileId, string room, string data)
{
try
{
MakeRequest("update-file", new { room, fileId, data });
}
catch (Exception error)
{
ProcessError(error);
}
}
public void DeleteFile<T>(T fileId, string room)
{
try

View File

@ -101,7 +101,14 @@ namespace ASC.FederatedLogin
if (string.IsNullOrEmpty(json)) return null;
try
{
return JsonSerializer.Deserialize<OAuth20Token>(json);
var result = JsonSerializer.Deserialize<OAuth20Token>(json);
if (result.Timestamp == default)
{
result.Timestamp = DateTime.UtcNow;
}
return result;
}
catch (Exception)
{

View File

@ -16,6 +16,11 @@
res.end();
});
router.post("/update-file", (req, res) => {
files.updateFile(req.body);
res.end();
});
router.post("/delete-file", (req, res) => {
files.deleteFile(req.body);
res.end();

View File

@ -115,10 +115,15 @@
modifyFolder(room, "create", fileId, "file", data);
}
function updateFile({ fileId, room, data } = {}) {
logger.info(`update file ${fileId} in room ${room}`);
modifyFolder(room, "update", fileId, "file", data);
}
function deleteFile({ fileId, room } = {}) {
logger.info(`delete file ${fileId} in room ${room}`);
modifyFolder(room, "delete", fileId, "file");
}
return { startEdit, stopEdit, createFile, deleteFile };
return { startEdit, stopEdit, createFile, deleteFile, updateFile };
};

View File

@ -103,7 +103,7 @@
"url": "/socket.io",
"internal": "http://localhost:9899/"
},
"cultures": "de,en,fr,it,pt-BR,ru",
"cultures": "az,cs,el,es,fr,ja,lo,nl,pt,ro,sk,tr,vi,bg,de,en,fi,it,ko,lv,pl,pt-BR,ru,sl,uk,zh-CN",
"url-shortener": {
"value": "/sh/",
"internal": "http://localhost:9999/"

View File

@ -24,7 +24,7 @@ map $request_uri $header_x_frame_options {
map $request_uri $cache_control {
default "no-cache, no-store, must-revalidate";
~*\/(api\/2\.0.*|storage|login\.ashx|products\/.+\/httphandlers\/filehandler\.ashx|ChunkedUploader.ashx|apisystem|sh|remoteEntry\.js|debuginfo\.md) "no-cache, no-store, must-revalidate";
~*\/(api\/2\.0.*|storage|login\.ashx|products\/.+\/httphandlers\/filehandler\.ashx|ChunkedUploader.ashx|ThirdPartyAppHandler|apisystem|sh|remoteEntry\.js|debuginfo\.md) "no-cache, no-store, must-revalidate";
~*\/(locales.*\.json) "public, no-transform";
~*\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|md|css|js)$ "public, no-transform";
}
@ -204,6 +204,11 @@ server {
location /backupFileUpload.ashx {
proxy_pass http://localhost:5012;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /ThirdPartyApp {
proxy_pass http://localhost:5007;
proxy_set_header X-REWRITER-URL $X_REWRITER_URL;
}
location /products {

View File

@ -4,7 +4,7 @@ import Avatar from "@appserver/components/avatar";
import Text from "@appserver/components/text";
import Checkbox from "@appserver/components/checkbox";
import Loader from "@appserver/components/loader";
import Loaders from "@appserver/common/components/Loaders";
const Option = ({
style,
isMultiSelect,
@ -16,7 +16,7 @@ const Option = ({
onOptionChange,
onLinkClick,
isLoader,
loadingLabel,
countLoaderRows,
}) => {
const onOptionChangeAction = React.useCallback(() => {
onOptionChange && onOptionChange(index, isChecked);
@ -27,19 +27,9 @@ const Option = ({
}, [onLinkClick, index]);
return isLoader ? (
<div style={style} className="row-option">
<div key="loader">
<Loader
type="oval"
size="16px"
style={{
display: "inline",
marginRight: "10px",
}}
/>
<Text as="span" noSelect={true}>
{loadingLabel}
</Text>
<div style={style}>
<div key="loader" className="option-loader">
<Loaders.ListLoader withoutFirstRectangle count={countLoaderRows} />
</div>
</div>
) : isMultiSelect ? (

View File

@ -8,7 +8,6 @@ import Option from "./Option";
const OptionList = ({
listOptionsRef,
loadingLabel,
options,
isOptionChecked,
isMultiSelect,
@ -17,13 +16,22 @@ const OptionList = ({
isItemLoaded,
itemCount,
loadMoreItems,
countLoad,
}) => {
const renderOption = React.useCallback(
({ index, style }) => {
const isLoaded = isItemLoaded(index);
if (!isLoaded) {
return <Option isLoader={true} loadingLabel={loadingLabel} />;
if (countLoad >= 1) {
return (
<div style={style}>
<Option isLoader={true} countLoaderRows={2} />
</div>
);
}
return <Option isLoader={true} />;
}
const option = options[index];
@ -43,7 +51,6 @@ const OptionList = ({
},
[
options,
loadingLabel,
isMultiSelect,
isItemLoaded,

View File

@ -37,6 +37,7 @@ const getCurrentGroup = (items) => {
return currentGroup;
};
let countLoad;
const Selector = (props) => {
const {
groups,
@ -72,6 +73,9 @@ const Selector = (props) => {
resetCache();
}, [searchValue, currentGroup, hasNextPage]);
useEffect(() => {
countLoad = 0;
}, []);
const resetCache = useCallback(() => {
if (listOptionsRef && listOptionsRef.current) {
listOptionsRef.current.resetloadMoreItemsCache(true);
@ -148,6 +152,7 @@ const Selector = (props) => {
const onSearchChange = useCallback(
(value) => {
countLoad = 0;
setSearchValue(value);
onSearchChanged && onSearchChanged(value);
},
@ -155,6 +160,7 @@ const Selector = (props) => {
);
const onSearchReset = useCallback(() => {
countLoad = 0;
onSearchChanged && onSearchChange("");
}, [onSearchChanged]);
@ -258,7 +264,7 @@ const Selector = (props) => {
searchValue: searchValue,
currentGroup: currentGroup ? currentGroup.key : null,
};
countLoad++;
loadNextPage && loadNextPage(options);
},
[isNextPageLoading, searchValue, currentGroup, options]
@ -295,6 +301,8 @@ const Selector = (props) => {
);
const onArrowClickAction = useCallback(() => {
countLoad = 0;
if (groupHeader && groups.length !== 1) {
setGroupHeader(null);
@ -308,7 +316,7 @@ const Selector = (props) => {
const renderGroupsList = useCallback(() => {
if (groupList.length === 0) {
return <Option isLoader={true} loadingLabel={loadingLabel} />;
return <Option isLoader={true} />;
}
return (
@ -318,7 +326,7 @@ const Selector = (props) => {
onGroupClick={onGroupClick}
/>
);
}, [isMultiSelect, groupList, onGroupClick, loadingLabel]);
}, [isMultiSelect, groupList, onGroupClick]);
const itemCount = hasNextPage ? options.length + 1 : options.length;
const hasSelected = selectedOptionList.length > 0;
@ -373,7 +381,6 @@ const Selector = (props) => {
) : (
<OptionList
listOptionsRef={listOptionsRef}
loadingLabel={loadingLabel}
options={options}
itemCount={itemCount}
isMultiSelect={isMultiSelect}
@ -382,6 +389,7 @@ const Selector = (props) => {
isItemLoaded={isItemLoaded}
isOptionChecked={isOptionChecked}
loadMoreItems={loadMoreItems}
countLoad={countLoad}
/>
)}
</>

View File

@ -109,7 +109,11 @@ const StyledSelector = styled.div`
right: 10px !important;
}
}
.option-loader {
width: 100%;
height: 100%;
margin-top: 16px;
}
.row-option {
box-sizing: border-box;
height: 48px;
@ -163,7 +167,8 @@ const StyledSelector = styled.div`
}
.option-separator {
height: 1px;
background: #dfe2e3;
background: ${(props) =>
props.theme.advancedSelector.selectedBackgroundColor};
margin: 8px 16px;
}

View File

@ -143,18 +143,10 @@ const StyledArticleHeader = styled.div`
justify-content: flex-start;
align-items: center;
.loader {
padding-top: 2px;
}
@media ${tablet} {
padding: 16px 16px 17px;
margin: 0;
justify-content: ${(props) => (props.showText ? "flex-start" : "center")};
.loader {
padding-top: 5px;
padding-bottom: 7px;
}
}
@media ${mobile} {
@ -168,10 +160,6 @@ const StyledArticleHeader = styled.div`
padding: 16px 16px 17px;
justify-content: ${(props) => (props.showText ? "flex-start" : "center")};
margin: 0;
.loader {
padding-top: 5px;
padding-bottom: 7px;
}
`}
${isMobileOnly &&
@ -181,6 +169,8 @@ const StyledArticleHeader = styled.div`
padding: 12px 16px 12px !important;
margin-bottom: 16px !important;
`}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
StyledArticleHeader.defaultProps = { theme: Base };

View File

@ -20,6 +20,7 @@ const ArticleHeader = ({
isLoaded,
tReady,
setIsLoadedArticleHeader,
isBurgerLoading,
...rest
}) => {
const location = useLocation();
@ -34,19 +35,19 @@ const ArticleHeader = ({
if (isLoadedSetting) setIsLoadedArticleHeader(isLoadedSetting);
}, [isLoadedSetting]);
const heightLoader = isTabletUtils() || isTablet ? "20px" : "32px";
const showLoader = commonSettings ? !isLoadedPage : false;
return showLoader ? (
<StyledArticleHeader>
<Loaders.ArticleHeader height={heightLoader} className="loader" />
</StyledArticleHeader>
) : (
const isTabletView = isTabletUtils() || isTablet;
return (
<StyledArticleHeader showText={showText} {...rest}>
<StyledIconBox name="article-burger">
<StyledMenuIcon onClick={onClick} />
</StyledIconBox>
{isTabletView && (isBurgerLoading || showLoader) ? (
<Loaders.ArticleHeader height="20px" />
) : (
<StyledIconBox name="article-burger">
<StyledMenuIcon onClick={onClick} />
</StyledIconBox>
)}
<StyledHeading showText={showText} size="large">
{children}
@ -63,11 +64,13 @@ ArticleHeader.propTypes = {
ArticleHeader.displayName = "Header";
export default inject(({ common }) => {
export default inject(({ common, auth }) => {
const { isLoaded, setIsLoadedArticleHeader } = common;
const { settingsStore } = auth;
const { isBurgerLoading } = settingsStore;
return {
isLoaded,
setIsLoadedArticleHeader,
isBurgerLoading,
};
})(observer(ArticleHeader));

View File

@ -293,8 +293,9 @@ const SortButton = ({
<>
<Backdrop
visible={isOpen}
withBackground={true}
withBackground={false}
onClick={toggleCombobox}
withoutBlur={true}
/>
<StyledSortButton
viewAs={viewAs}

View File

@ -50,6 +50,8 @@ const StyledButton = styled.div`
width: 200px;
}
`}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
StyledButton.defaultProps = { theme: Base };

View File

@ -160,6 +160,7 @@ const StyledFilterBlockItemTag = styled.div`
cursor: pointer;
${(props) => props.isSelected && selectedItemTag}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
StyledFilterBlockItemTag.defaultProps = { theme: Base };

View File

@ -13,6 +13,7 @@ const StyledContainer = styled.div`
@media ${tablet} {
width: ${(props) => (props.showText ? "240px" : "52px")};
padding: ${(props) => (props.showText ? "0 16px" : "10px 16px")};
box-sizing: border-box;
}
${isMobile &&
@ -20,11 +21,13 @@ const StyledContainer = styled.div`
max-width: ${(props) => (props.showText ? "240px" : "52px")};
width: ${(props) => (props.showText ? "240px" : "52px")};
padding: ${(props) => (props.showText ? "0 16px" : "10px 16px")};
box-sizing: border-box;
`}
@media ${mobile} {
width: 100%;
padding: 0 16px;
box-sizing: border-box;
}
`;

View File

@ -16,6 +16,7 @@ const StyledContainer = styled.div`
@media ${tablet} {
width: ${(props) => (props.showText ? "240px" : "52px")};
padding: 0 16px;
box-sizing: border-box;
}
${isMobile &&
@ -23,11 +24,13 @@ const StyledContainer = styled.div`
max-width: ${(props) => (props.showText ? "240px" : "52px")};
width: ${(props) => (props.showText ? "240px" : "52px")};
padding: ${(props) => (props.showText ? "0 16px" : "10px 16px")};
box-sizing: border-box;
`}
@media ${mobile} {
width: 100%;
padding: 0 16px;
box-sizing: border-box;
}
`;

View File

@ -8,7 +8,7 @@ const ListLoader = ({ count, ...props }) => {
for (var i = 0; i < count; i++) {
items.push(<ListItemLoader key={`list_loader_${i}`} {...props} />);
}
return <StyledList>{items}</StyledList>;
return <StyledList className="list-loader-wrapper">{items}</StyledList>;
};
ListLoader.propTypes = {

View File

@ -1,28 +1,31 @@
import React from "react";
import PropTypes from "prop-types";
import Loaders from "@appserver/common/components/Loaders";
import StyledContainer from "./StyledNavigation";
import ArrowButton from "./sub-components/arrow-btn";
import Text from "./sub-components/text";
import ControlButtons from "./sub-components/control-btn";
import DropBox from "./sub-components/drop-box";
import { isMobileOnly } from "react-device-detect";
import { Consumer } from "@appserver/components/utils/context";
import DomHelpers from "@appserver/components/utils/domHelpers";
import Backdrop from "@appserver/components/backdrop";
import { isMobile, isMobileOnly } from "react-device-detect";
import {
isMobile as isMobileUtils,
isTablet as isTabletUtils,
isDesktop as isDesktopUtils,
} from "@appserver/components/utils/device";
import ToggleInfoPanelButton from "./sub-components/toggle-infopanel-btn";
const Navigation = ({
tReady,
showText,
isRootFolder,
title,
canCreate,
isDesktop,
isTabletView,
personal,
onClickFolder,
@ -47,6 +50,10 @@ const Navigation = ({
const dropBoxRef = React.useRef(null);
const containerRef = React.useRef(null);
const isDesktop =
(!isMobile && !isTabletUtils() && !isMobileUtils()) ||
(isDesktopUtils() && !isMobile);
const onMissClick = (e) => {
e.preventDefault;
const path = e.path || (e.composedPath && e.composedPath());
@ -110,16 +117,16 @@ const Navigation = ({
<>
{isOpen && (
<>
{isMobileOnly && (
<Backdrop
isAside={true}
visible={isOpen}
withBackground={true}
zIndex={400}
/>
)}
<Backdrop
visible={isOpen}
withBackground={false}
withoutBlur={true}
zIndex={400}
/>
<DropBox
{...rest}
isDesktop={isDesktop}
ref={dropBoxRef}
maxHeight={maxHeight}
dropBoxWidth={dropBoxWidth}
@ -146,9 +153,9 @@ const Navigation = ({
isRootFolder={isRootFolder}
canCreate={canCreate}
title={title}
isDesktop={isDesktop}
isTabletView={isTabletView}
isRecycleBinFolder={isRecycleBinFolder}
isDesktop={isDesktop}
>
<ArrowButton
isRootFolder={isRootFolder}
@ -171,8 +178,16 @@ const Navigation = ({
clearTrash={clearTrash}
toggleInfoPanel={toggleInfoPanel}
isInfoPanelVisible={isInfoPanelVisible}
isDesktop={isDesktop}
/>
</StyledContainer>
{isDesktop && (
<ToggleInfoPanelButton
isRootFolder={isRootFolder}
toggleInfoPanel={toggleInfoPanel}
isInfoPanelVisible={isInfoPanelVisible}
/>
)}
</>
)}
</Consumer>

View File

@ -1,9 +1,16 @@
import styled, { css } from "styled-components";
import { isMobileOnly } from "react-device-detect";
import { isMobile, isMobileOnly } from "react-device-detect";
import { tablet, mobile } from "@appserver/components/utils/device";
const StyledContainer = styled.div`
width: 100% !important;
${(props) =>
!props.isDropBox &&
props.isDesktop &&
css`
width: fit-content;
max-width: calc(100% - 72px);
`}
display: grid;
align-items: center;
grid-template-columns: ${(props) =>
@ -29,6 +36,14 @@ const StyledContainer = styled.div`
padding: ${(props) => (props.isDropBox ? "14px 0 5px" : "14px 0 15px")};
}
${isMobile &&
css`
width: 100%;
grid-template-columns: ${(props) =>
props.isRootFolder ? "auto 1fr" : "29px 1fr auto"};
padding: ${(props) => (props.isDropBox ? "14px 0 5px" : "14px 0 15px")};
`}
@media ${mobile} {
padding: ${(props) =>
props.isDropBox ? "10px 0 5px" : "10px 0 11px"} !important;

View File

@ -6,27 +6,26 @@ import IconButton from "@appserver/components/icon-button";
import { isMobile } from "react-device-detect";
import { tablet } from "@appserver/components/utils/device";
import { Base } from "@appserver/components/themes";
import ToggleInfoPanelButton from "./toggle-infopanel-btn";
const StyledContainer = styled.div`
margin-left: 20px;
display: flex;
align-items: center;
height: 32px;
.add-button {
margin-right: 16px;
min-width: 15px;
${(props) =>
!props.isDropBox &&
css`
@media ${tablet} {
display: none;
}
`}
@media ${tablet} {
display: none;
}
${isMobile &&
css`
${(props) => !props.isDropBox && "display: none"};
display: none;
`}
}
@ -117,6 +116,7 @@ const ControlButtons = ({
)}
{!personal && (
<ContextMenuButton
zIndex={402}
className="option-button"
directionX="right"
iconName="images/vertical-dots.react.svg"
@ -127,20 +127,11 @@ const ControlButtons = ({
/>
)}
{!isDesktop && (
<StyledInfoPanelToggleWrapper
<ToggleInfoPanelButton
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={toggleInfoPanelAction}
/>
</div>
</StyledInfoPanelToggleWrapper>
toggleInfoPanel={toggleInfoPanelAction}
/>
)}
</>
) : canCreate ? (
@ -157,20 +148,13 @@ const ControlButtons = ({
isDisabled={false}
/>
)}
<StyledInfoPanelToggleWrapper
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={toggleInfoPanelAction}
/>
</div>
</StyledInfoPanelToggleWrapper>
{!isDesktop && (
<ToggleInfoPanelButton
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
toggleInfoPanel={toggleInfoPanelAction}
/>
)}
</>
) : isRecycleBinFolder && !isEmptyFilesList ? (
<>
@ -181,37 +165,23 @@ const ControlButtons = ({
onClick={clearTrash}
className="trash-button"
/>
<StyledInfoPanelToggleWrapper
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={toggleInfoPanelAction}
/>
</div>
</StyledInfoPanelToggleWrapper>
{!isDesktop && (
<ToggleInfoPanelButton
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
toggleInfoPanel={toggleInfoPanelAction}
/>
)}
</>
) : (
<>
<StyledInfoPanelToggleWrapper
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={toggleInfoPanelAction}
/>
</div>
</StyledInfoPanelToggleWrapper>
{!isDesktop && (
<ToggleInfoPanelButton
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
toggleInfoPanel={toggleInfoPanelAction}
/>
)}
</>
)}
</StyledContainer>

View File

@ -14,10 +14,8 @@ import StyledContainer from "../StyledNavigation";
import { isMobile, isMobileOnly } from "react-device-detect";
import {
tablet,
mobile,
isMobile as isMobileUtils,
isTablet as isTabletUtils,
isDesktop as isDesktopUtils,
} from "@appserver/components/utils/device";
import { Base } from "@appserver/components/themes";
@ -29,7 +27,7 @@ const StyledBox = styled.div`
padding: ${isMobile ? "0 16px " : "0 20px"};
${(props) => !props.isDesktop && `width: ${props.dropBoxWidth}px;`};
${(props) => `width: ${props.dropBoxWidth}px;`};
height: ${(props) => (props.height ? `${props.height}px` : "fit-content")};
max-height: calc(100vh - 48px);
@ -92,14 +90,13 @@ const DropBox = React.forwardRef(
isInfoPanelVisible,
maxHeight,
isOpen,
isDesktop,
},
ref
) => {
const [dropBoxHeight, setDropBoxHeight] = React.useState(0);
const countItems = navigationItems.length;
const isDesktop = !isMobile || isDesktopUtils();
const getItemSize = (index) => {
if (index === countItems - 1) return 51;
return isMobile || isMobileUtils() || isTabletUtils() ? 36 : 30;

View File

@ -4,32 +4,37 @@ import PropTypes from "prop-types";
import ExpanderDownIcon from "@appserver/components/public/static/images/expander-down.react.svg";
import commonIconsStyles from "@appserver/components/utils/common-icons-style";
import Headline from "@appserver/common/components/Headline";
import Heading from "@appserver/components/heading";
import { tablet } from "@appserver/components/utils/device";
import { isMobile } from "react-device-detect";
import { Base } from "@appserver/components/themes";
const StyledTextContainer = styled.div`
width: fit-content;
position: relative;
display: grid;
grid-template-columns: ${(props) =>
props.isRootFolder ? "auto" : "auto 12px"};
display: flex;
align-items: center;
flex-direction: row;
position: relative;
overflow: hidden;
${(props) => !props.isRootFolder && "cursor: pointer"};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
const StyledHeadline = styled(Headline)`
width: 100%;
const StyledHeading = styled(Heading)`
font-weight: 700;
font-size: ${isMobile ? "21px !important" : "18px"};
line-height: ${isMobile ? "28px !important" : "24px"};
margin: 0;
@media ${tablet} {
font-size: 21px;
line-height: 28px;
@ -54,7 +59,7 @@ const StyledExpanderDownIconRotate = styled(ExpanderDownIcon)`
min-width: 8px !important;
width: 8px !important;
min-height: 18px !important;
padding: 0 4px 0 1px;
padding: 0 4px 0 2px;
transform: rotate(-180deg);
path {
@ -73,9 +78,9 @@ const Text = ({ title, isRootFolder, isOpen, onClick, ...rest }) => {
onClick={onClick}
{...rest}
>
<StyledHeadline type="content" truncate={true}>
<StyledHeading type="content" truncate={true}>
{title}
</StyledHeadline>
</StyledHeading>
{!isRootFolder ? (
isOpen ? (
<StyledExpanderDownIconRotate />

View File

@ -0,0 +1,106 @@
import React from "react";
import styled, { css } from "styled-components";
import ContextMenuButton from "@appserver/components/context-menu-button";
import IconButton from "@appserver/components/icon-button";
import { isMobile } from "react-device-detect";
import { tablet } from "@appserver/components/utils/device";
import { Base } from "@appserver/components/themes";
const StyledContainer = styled.div`
margin-left: 20px;
display: flex;
align-items: center;
height: 32px;
.add-button {
margin-right: 16px;
min-width: 15px;
@media ${tablet} {
display: none;
}
${isMobile &&
css`
display: none;
`}
}
.option-button {
margin-right: 16px;
min-width: 15px;
}
.trash-button {
margin-right: 16px;
min-width: 15px;
}
`;
const StyledInfoPanelToggleWrapper = styled.div`
display: ${(props) => (props.isInfoPanelVisible ? "none" : "flex")};
align-items: center;
align-self: center;
justify-content: center;
margin-left: auto;
margin-bottom: 1px;
@media ${tablet} {
margin-left: ${(props) => (props.isRootFolder ? "auto" : "0")};
}
${isMobile &&
css`
margin-left: ${(props) => (props.isRootFolder ? "auto" : "0")};
`}
.info-panel-toggle-bg {
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: ${(props) =>
props.isInfoPanelVisible
? props.theme.infoPanel.sectionHeaderToggleBgActive
: props.theme.infoPanel.sectionHeaderToggleBg};
path {
fill: ${(props) =>
props.isInfoPanelVisible
? props.theme.infoPanel.sectionHeaderToggleIconActive
: props.theme.infoPanel.sectionHeaderToggleIcon};
}
}
`;
StyledInfoPanelToggleWrapper.defaultProps = { theme: Base };
const ToggleInfoPanelButton = ({
isRootFolder,
isInfoPanelVisible,
toggleInfoPanel,
}) => {
return (
<StyledInfoPanelToggleWrapper
isRootFolder={isRootFolder}
isInfoPanelVisible={isInfoPanelVisible}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={toggleInfoPanel}
/>
</div>
</StyledInfoPanelToggleWrapper>
);
};
export default ToggleInfoPanelButton;

View File

@ -40,13 +40,8 @@ const StyledMainBar = styled.div`
box-sizing: border-box;
margin-left: -20px;
/* width: calc(100vw - 256px);
max-width: calc(100vw - 256px); */
width: ${(props) =>
props.infoPanelIsVisible ? "calc(100vw - 657px)" : "calc(100vw - 256px)"};
max-width: ${(props) =>
props.infoPanelIsVisible ? "calc(100vw - 657px)" : "calc(100vw - 256px)"};
width: calc(100% + 20px);
#bar-banner {
margin-bottom: -3px;
@ -58,19 +53,13 @@ const StyledMainBar = styled.div`
}
@media ${tablet} {
width: ${(props) =>
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
max-width: ${(props) =>
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"};
width: calc(100% + 16px);
margin-left: -16px;
}
${isMobile &&
css`
width: ${(props) =>
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
max-width: ${(props) =>
props.showText ? "calc(100vw - 240px)" : "calc(100vw - 52px)"} !important;
width: calc(100% + 32px) !important;
margin-left: -16px;
`}

View File

@ -10,12 +10,12 @@ import styled from "styled-components";
const StyledInfoPanelHeader = styled.div`
width: 100%;
max-width: 100%;
height: 54px;
min-height: 54px;
box-sizing: border-box;
height: 52px;
min-height: 52px;
display: flex;
justify-content: space-between;
align-items: center;
align-self: center;
border-bottom: ${(props) => `1px solid ${props.theme.infoPanel.borderColor}`};
.header-text {
@ -23,7 +23,30 @@ const StyledInfoPanelHeader = styled.div`
}
`;
const SubInfoPanelHeader = ({ children, onHeaderCrossClick }) => {
const StyledInfoPanelToggleWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding-right: 20px;
.info-panel-toggle-bg {
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: ${(props) =>
props.theme.infoPanel.sectionHeaderToggleBgActive};
path {
fill: ${(props) => props.theme.infoPanel.sectionHeaderToggleIconActive};
}
}
`;
StyledInfoPanelToggleWrapper.defaultProps = { theme: Base };
const SubInfoPanelHeader = ({ children, closeInfoPanel }) => {
const content = children?.props?.children;
return (
@ -31,6 +54,20 @@ const SubInfoPanelHeader = ({ children, onHeaderCrossClick }) => {
<Text className="header-text" fontSize="21px" fontWeight="700">
{content}
</Text>
<StyledInfoPanelToggleWrapper
isRootFolder={true}
isInfoPanelVisible={true}
>
<div className="info-panel-toggle-bg">
<IconButton
className="info-panel-toggle"
iconName="images/panel.react.svg"
size="16"
isFill={true}
onClick={closeInfoPanel}
/>
</div>
</StyledInfoPanelToggleWrapper>
</StyledInfoPanelHeader>
);
};
@ -50,9 +87,9 @@ SubInfoPanelHeader.defaultProps = { theme: Base };
SubInfoPanelHeader.displayName = "SubInfoPanelHeader";
export default inject(({ infoPanelStore }) => {
let onHeaderCrossClick = () => {};
let closeInfoPanel = () => {};
if (infoPanelStore) {
onHeaderCrossClick = infoPanelStore.onHeaderCrossClick;
closeInfoPanel = () => infoPanelStore.setIsVisible(false);
}
return { onHeaderCrossClick };
return { closeInfoPanel };
})(observer(SubInfoPanelHeader));

View File

@ -17,7 +17,7 @@ const tabletProps = css`
position: sticky;
top: 0;
background: ${(props) => props.theme.section.header.background};
z-index: 20;
z-index: 202;
${isMobileOnly &&
css`

View File

@ -24,6 +24,10 @@ const StyledSectionHeader = styled.div`
width: 100%;
max-width: 100%;
.header-container {
display: flex;
}
@media ${tablet} {
padding-right: 16px;
margin-right: 0px;

View File

@ -19,6 +19,7 @@ const themes = {
class SettingsStore {
isLoading = false;
isLoaded = false;
isBurgerLoading = false;
checkedMaintenance = false;
maintenanceExist = false;
@ -122,6 +123,7 @@ class SettingsStore {
setTenantStatus = (tenantStatus) => {
this.tenantStatus = tenantStatus;
};
get urlAuthKeys() {
const splitted = this.culture.split("-");
const lang = splitted.length > 0 ? splitted[0] : "en";
@ -472,6 +474,10 @@ class SettingsStore {
setTenantAlias = (tenantAlias) => {
this.tenantAlias = tenantAlias;
};
setIsBurgerLoading = (isBurgerLoading) => {
this.isBurgerLoading = isBurgerLoading;
};
}
export default SettingsStore;

View File

@ -38,8 +38,7 @@ const StyledAside = styled(Container)`
@media ${tablet} {
max-width: calc(100% - 69px);
transform: translateX(
${(props) =>
props.visible ? "0" : props.scale ? "100%" : "calc(100% - 69px)"}
${(props) => (props.visible ? "0" : props.scale ? "100%" : "480px")}
);
}
@ -47,8 +46,7 @@ const StyledAside = styled(Container)`
css`
max-width: calc(100% - 69px);
transform: translateX(
${(props) =>
props.visible ? "0" : props.scale ? "100%" : "calc(100% - 69px)"}
${(props) => (props.visible ? "0" : props.scale ? "100%" : "480px")}
);
`}

View File

@ -30,7 +30,7 @@ class Backdrop extends React.Component {
}
checkingExistBackdrop = () => {
const { visible, isAside, withBackground, isContextMenu } = this.props;
const { visible, isAside, withBackground, withoutBlur } = this.props;
if (visible) {
const isTablet = window.innerWidth < 1024;
const backdrops = document.querySelectorAll(".backdrop-active");
@ -39,7 +39,7 @@ class Backdrop extends React.Component {
backdrops.length < 1 || (isAside && backdrops.length <= 1);
let needBackground =
needBackdrop && ((isTablet && !isContextMenu) || withBackground);
needBackdrop && ((isTablet && !withoutBlur) || withBackground);
if (isAside && needBackdrop) needBackground = true;
@ -117,7 +117,7 @@ Backdrop.propTypes = {
/** Must be true if used with Aside component */
isAside: PropTypes.bool,
/** Must be true if used with Context menu */
isContextMenu: PropTypes.bool,
withoutBlur: PropTypes.bool,
};
Backdrop.defaultProps = {
@ -126,7 +126,7 @@ Backdrop.defaultProps = {
withBackground: false,
isAside: false,
isModalDialog: false,
isContextMenu: false,
withoutBlur: false,
};
export default Backdrop;

View File

@ -236,10 +236,13 @@ const StyledCatalogItemSibling = styled.div`
background-color: ${(props) =>
props.isActive && props.theme.catalogItem.sibling.active.background};
&:hover {
background-color: ${(props) =>
props.theme.catalogItem.sibling.hover.background};
}
${!isMobile &&
css`
&:hover {
background-color: ${(props) =>
props.theme.catalogItem.sibling.hover.background};
}
`}
@media ${tablet} {
min-height: ${(props) => props.theme.catalogItem.container.tablet.height};
@ -250,10 +253,6 @@ const StyledCatalogItemSibling = styled.div`
css`
min-height: ${(props) => props.theme.catalogItem.container.tablet.height};
max-height: ${(props) => props.theme.catalogItem.container.tablet.height};
&:hover {
background-color: transparent;
}
`}
${(props) => props.isDragging && draggingSiblingCss}
@ -302,6 +301,8 @@ const StyledCatalogItemContainer = styled.div`
props.isEndOfBlock &&
props.theme.catalogItem.container.tablet.marginBottom};
`}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
StyledCatalogItemContainer.defaultProps = { theme: Base };

View File

@ -354,7 +354,7 @@ class ContextMenu extends Component {
<Backdrop
visible={this.state.visible}
withBackground={false}
isContextMenu={true}
withoutBlur={true}
/>
)}
<Portal element={element} appendTo={this.props.appendTo} />

View File

@ -195,7 +195,9 @@ const StyledContextMenu = styled.div`
height: 16px;
width: 16px;
}
path {
path,
circle,
rect {
fill: ${(props) => props.theme.dropDownItem.icon.color};
}

View File

@ -35,3 +35,4 @@ When using component, it should be noted that parent must have CSS property _pos
| `style` | `obj`, `array` | - | - | - | Accepts css style |
| `withBackdrop` | `bool` | - | - | `true` | Used to display backdrop |
| `showDisabledItems` | `bool` | - | - | `false` | Display disabled items or not |
| `withBlur` | `bool` | - | - | `false` | Enable blur for backdrop |

View File

@ -294,13 +294,18 @@ class DropDownContainer extends React.Component {
this.props.clickOutsideAction({}, !this.props.open);
};
render() {
const { withBackdrop = true, open, theme } = this.props;
const { withBackdrop = true, withBlur = false, open } = this.props;
const eventTypesProp = isMobile ? { eventTypes: ["touchend"] } : {};
return (
<>
{withBackdrop ? (
<Backdrop visible={open} zIndex={199} onClick={this.toggleDropDown} />
<Backdrop
visible={open}
zIndex={199}
onClick={this.toggleDropDown}
withoutBlur={!withBlur}
/>
) : null}
<EnhancedComponent
{...eventTypesProp}
@ -355,6 +360,8 @@ DropDownContainer.propTypes = {
smallSectionWidth: PropTypes.bool,
/** It is necessary when we explicitly set the direction, disables check position */
fixedDirection: PropTypes.bool,
/**Enable blur for backdrop */
withBlur: PropTypes.bool,
};
DropDownContainer.defaultProps = {

View File

@ -7,6 +7,8 @@ import {
EmptyContentImage,
} from "./styled-empty-screen-container";
import { isMobile } from "react-device-detect";
const EmptyScreenContainer = (props) => {
const {
imageSrc,
@ -15,12 +17,15 @@ const EmptyScreenContainer = (props) => {
subheadingText,
descriptionText,
buttons,
imageStyle,
buttonStyle,
} = props;
return (
<EmptyContentBody {...props}>
<EmptyContentImage
imageSrc={imageSrc}
imageAlt={imageAlt}
style={!isMobile ? imageStyle : {}}
className="ec-image"
/>
@ -28,7 +33,7 @@ const EmptyScreenContainer = (props) => {
<Text
as="span"
fontSize="19px"
fontWeight="600"
fontWeight="700"
className="ec-header"
noSelect
>
@ -54,7 +59,11 @@ const EmptyScreenContainer = (props) => {
</Text>
)}
{buttons && <div className="ec-buttons">{buttons}</div>}
{buttons && (
<div className="ec-buttons" style={buttonStyle}>
{buttons}
</div>
)}
</EmptyContentBody>
);
};

View File

@ -1,5 +1,5 @@
import styled from "styled-components";
import { mobile } from "../utils/device";
import { mobile, tablet } from "../utils/device";
import NoUserSelect from "../utils/commonStyles";
const EmptyContentBody = styled.div`
@ -7,6 +7,10 @@ const EmptyContentBody = styled.div`
padding: 64px 0;
grid-template-columns: 150px 1fr;
@media ${tablet} {
grid-template-columns: none;
}
display: grid;
grid-template-areas:
"img headerText"
@ -20,7 +24,7 @@ const EmptyContentBody = styled.div`
grid-template-rows: max-content;
.ec-image {
grid-area: img;
margin: 0 0 0 auto;
margin: 16px 0 0 auto;
${NoUserSelect}
}
@ -76,6 +80,7 @@ const EmptyContentBody = styled.div`
.ec-header,
.ec-subheading,
.ec-desc,
.ec-image,
.ec-buttons {
padding-left: 16px;
}

View File

@ -223,8 +223,12 @@ const MainButtonMobile = (props) => {
return (
<>
<Backdrop zIndex={200} visible={isOpen} onClick={outsideClick} />
<div ref={ref} className={className} style={{ zIndex: "201", ...style }}>
<Backdrop zIndex={210} visible={isOpen} onClick={outsideClick} />
<div
ref={ref}
className={className}
style={{ zIndex: `${isOpen ? "211" : "201"}`, ...style }}
>
<StyledFloatingButton
icon={isOpen ? "minus" : "plus"}
isOpen={isOpen}

View File

@ -31,6 +31,11 @@ const Content = styled.div`
border-radius: ${(props) =>
props.theme.modalDialog.content.modalBorderRadius};
.modal-dialog-aside-header {
margin: 0 -16px;
padding: 0 16px;
}
.heading {
max-width: ${(props) => props.theme.modalDialog.content.heading.maxWidth};
margin: ${(props) => props.theme.modalDialog.content.heading.margin};

View File

@ -35,7 +35,7 @@
"react-onclickoutside": "^6.11.2",
"react-svg": "^12.1.0",
"react-text-mask": "^5.4.3",
"react-toastify": "^6.2.0",
"react-toastify": "^7.0.0",
"react-tooltip": "^4.2.21",
"react-transition-group": "^4.4.1",
"react-window": "^1.8.6",

View File

@ -1,6 +1,6 @@
import React from "react";
import PropTypes from "prop-types";
import { isMobile } from "@appserver/components/utils/device";
import StyledScrollbar from "./styled-scrollbar";
const Scrollbar = React.forwardRef((props, ref) => {
const scrollbarType = {
@ -40,7 +40,7 @@ const Scrollbar = React.forwardRef((props, ref) => {
borderRadius: "inherit",
},
view: {
paddingRight: "16px",
paddingRight: isMobile() ? "8px" : "16px",
outline: "none",
WebkitOverflowScrolling: "auto",
},

View File

@ -36,6 +36,8 @@ const StyledButton = styled.div`
!props.isDisabled &&
`background-color: ${props.theme.selectorAddButton.activeBackground};`}
}
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
`;
StyledButton.defaultProps = { theme: Base };

View File

@ -133,7 +133,8 @@ const StyledTableGroupMenu = styled.div`
StyledTableGroupMenu.defaultProps = { theme: Base };
const StyledInfoPanelToggleWrapper = styled.div`
display: flex;
display: ${(props) => (props.isInfoPanelVisible ? "none" : "flex")};
align-items: center;
align-self: center;
justify-content: center;
@ -146,11 +147,15 @@ const StyledInfoPanelToggleWrapper = styled.div`
margin: 0 16px 0 auto;
}
margin-top: 1px;
.info-panel-toggle-bg {
height: 32px;
width: 32px;
display: flex;
align-items: center;
align-self: center;
justify-content: center;
border-radius: 50%;
background-color: ${(props) =>

View File

@ -2226,6 +2226,12 @@ const Base = {
border: "1px solid #d1d1d1",
},
connectedClouds: {
color: "#657077",
borderBottom: `1px solid #eceef1`,
borderRight: `1px solid #d0d5da`,
},
filesModalDialog: {
border: `1px solid lightgray`,
},

View File

@ -1809,7 +1809,7 @@ const Dark = {
},
control: {
background: "#a3a3a3",
fill: "#333333",
fill: "#ffffff",
},
headerBurgerColor: "#606060",
@ -2235,6 +2235,12 @@ const Dark = {
border: "1px solid #474747",
},
connectedClouds: {
color: "#eeeeee",
borderBottom: `1px solid #474747`,
borderRight: `1px solid #474747`,
},
filesModalDialog: {
border: `1px solid #474747`,
},

View File

@ -50,6 +50,7 @@ const StyledToastContainer = styled(ToastContainer)`
}
50% {
visibility: hidden;
transform: translate3d(0, 0, 0);
}
}

View File

@ -0,0 +1,24 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_22900_255424)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M80 6.667C80 4.45786 78.2091 2.66699 76 2.66699H48.5424H17.5003C17.2343 2.66699 16.9792 2.77296 16.7916 2.96145L2.958 16.8562C2.77142 17.0436 2.66666 17.2973 2.66666 17.5617V93.3337C2.66666 95.5428 4.45752 97.3337 6.66666 97.3337H76C78.2091 97.3337 80 95.5428 80 93.3337V62.0003V37.667V13.3337V6.667Z" fill="white"/>
<path d="M79.667 55.6668L79.667 93.0002C79.667 95.2093 77.8761 97.0002 75.667 97.0002L6.33366 97.0002C4.12452 97.0002 2.33366 95.2093 2.33366 93.0002L2.33366 17.7477C2.33366 17.4825 2.43901 17.2281 2.62655 17.0406L17.0408 2.62639C17.2283 2.43885 17.4827 2.3335 17.7479 2.3335L75.667 2.3335C77.8761 2.3335 79.667 4.12435 79.667 6.33349V14.1668" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.9795 2.31344C17.1225 2.17044 17.3375 2.12766 17.5243 2.20505C17.7112 2.28244 17.833 2.46476 17.833 2.66699V14.3337C17.833 16.2667 16.266 17.8337 14.333 17.8337H2.66634C2.46411 17.8337 2.28179 17.7118 2.2044 17.525C2.12701 17.3382 2.16979 17.1231 2.31279 16.9801L16.9795 2.31344Z" fill="url(#paint0_linear_22900_255424)" stroke="#333333" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.473 24.3359C29.473 24.0598 29.2491 23.8359 28.973 23.8359H23.7933C23.5172 23.8359 23.2933 24.0598 23.2933 24.3359C23.2933 24.6121 23.5172 24.8359 23.7933 24.8359H28.973C29.2491 24.8359 29.473 24.6121 29.473 24.3359ZM52.167 44.3316C52.167 44.0554 51.9431 43.8316 51.667 43.8316H37.0003C36.7242 43.8316 36.5003 44.0554 36.5003 44.3316C36.5003 44.6077 36.7242 44.8316 37.0003 44.8316H51.667C51.9431 44.8316 52.167 44.6077 52.167 44.3316ZM30.8337 44.3316C30.8337 44.0554 30.6098 43.8316 30.3337 43.8316H15.667C15.3909 43.8316 15.167 44.0554 15.167 44.3316C15.167 44.6077 15.3909 44.8316 15.667 44.8316H30.3337C30.6098 44.8316 30.8337 44.6077 30.8337 44.3316ZM56.0003 53.8324C56.2765 53.8324 56.5003 54.0562 56.5003 54.3324C56.5003 54.6085 56.2765 54.8324 56.0003 54.8324H15.3337C15.0575 54.8324 14.8337 54.6085 14.8337 54.3324C14.8337 54.0562 15.0575 53.8324 15.3337 53.8324H56.0003ZM44.8337 34.3367C44.8337 34.0606 44.6098 33.8367 44.3337 33.8367H15.6696C15.3935 33.8367 15.1696 34.0606 15.1696 34.3367C15.1696 34.6129 15.3935 34.8367 15.6696 34.8367H44.3337C44.6098 34.8367 44.8337 34.6129 44.8337 34.3367ZM34.3337 63.8327C34.6098 63.8327 34.8337 64.0565 34.8337 64.3327C34.8337 64.6088 34.6098 64.8327 34.3337 64.8327H15.3046C15.0285 64.8327 14.8046 64.6088 14.8046 64.3327C14.8046 64.0565 15.0285 63.8327 15.3046 63.8327H34.3337ZM42.8337 84.3285C42.8337 84.0523 42.6098 83.8285 42.3337 83.8285H15.0519C14.7758 83.8285 14.5519 84.0523 14.5519 84.3285C14.5519 84.6046 14.7758 84.8285 15.0519 84.8285H42.3337C42.6098 84.8285 42.8337 84.6046 42.8337 84.3285ZM57.667 73.8336C57.9431 73.8336 58.167 74.0575 58.167 74.3336C58.167 74.6097 57.9431 74.8336 57.667 74.8336H15.4776C15.2015 74.8336 14.9776 74.6097 14.9776 74.3336C14.9776 74.0575 15.2015 73.8336 15.4776 73.8336H57.667Z" fill="#333333"/>
<path d="M56.667 64.3325H41.9713" stroke="#333333" stroke-linecap="round"/>
<path d="M61.333 14H34.6663" stroke="url(#paint1_linear_22900_255424)" stroke-width="2" stroke-linecap="round"/>
<path d="M74.6085 50.54L74.5008 50.6763V50.85V77.1281C74.5008 77.157 74.4918 77.2141 74.4613 77.2879C74.4375 77.3456 74.4067 77.3987 74.3749 77.4408L66.9144 71.576C66.9141 71.5757 66.9137 71.5755 66.9134 71.5753C66.7972 71.4832 66.6775 71.4112 66.5833 71.356C66.5656 71.3456 66.549 71.3359 66.5334 71.3268C66.4569 71.2822 66.4043 71.2515 66.3567 71.218C66.3312 71.2 66.3147 71.1863 66.3041 71.1762C66.3001 71.1724 66.2972 71.1693 66.2952 71.1671C66.2952 71.1658 66.2951 71.1643 66.2951 71.1625V50.8441V50.6704L66.1874 50.5341L46.9854 26.2369C46.9854 26.2369 46.9854 26.2368 46.9853 26.2368C46.3784 25.4682 45.8421 24.2693 45.8331 23.3161C45.8287 22.8516 45.9486 22.502 46.1805 22.267C46.4112 22.0332 46.8314 21.8335 47.5962 21.8335H48.5404H93.4417C94.1199 21.8335 94.4692 22.0319 94.6573 22.2574C94.8569 22.4967 94.9573 22.8646 94.9353 23.3542C94.8907 24.3447 94.3548 25.5474 93.8219 26.2258C93.8218 26.2259 93.8217 26.226 93.8216 26.2261L74.6085 50.54ZM66.2914 71.1624C66.2914 71.1624 66.2914 71.1625 66.2914 71.1625L66.2914 71.1624Z" fill="#BDECFF" stroke="#333333"/>
</g>
<defs>
<linearGradient id="paint0_linear_22900_255424" x1="17.333" y1="2.09408" x2="2.66631" y2="17.3337" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8E3D"/>
<stop offset="1" stop-color="#FF6F3D"/>
</linearGradient>
<linearGradient id="paint1_linear_22900_255424" x1="61.333" y1="13.9609" x2="61.2608" y2="15.9598" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF8E3D"/>
<stop offset="1" stop-color="#FF6F3D"/>
</linearGradient>
<clipPath id="clip0_22900_255424">
<rect width="100" height="100" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -17,6 +17,8 @@
"EmptyFilterDescriptionText": "No files or folders match this filter. Try a different one or clear filter to view all files. ",
"EmptyFilterSubheadingText": "No files to be displayed for this filter here",
"EmptyFolderHeader": "No files in this folder",
"EmptyScreenFolder": "No docs here yet",
"EmptyFolderDecription": "Drop files here or create new ones.",
"EmptyRecycleBin": "Empty Trash",
"FavoritesEmptyContainerDescription": "To mark files as favorites or remove them from this list, use the context menu.",
"FileRemoved": "File moved to Trash",
@ -43,6 +45,7 @@
"NewPresentation": "New presentation",
"NewSpreadsheet": "New spreadsheet",
"NoSubfolders": "No subfolders",
"NotFoundFilterTitle": "Nothing found",
"Open": "Open",
"OpenLocation": "Open location",
"Presentation": "Presentation",
@ -74,6 +77,7 @@
"TooltipElementsCopyMessage": "Copy {{element}} elements",
"TooltipElementsMoveMessage": "Move {{element}} elements",
"TrashEmptyContainerDescription": "Trash is where all the deleted files are moved. You can restore or delete them permanently by emptying Trash. ",
"TrashEmptyDescription": "All deleted files are moved to 'Trash'. Restore files deleted by mistake or delete them permanently. Please note, that the files deleted from the 'Trash' cannot be restored any longer.",
"UnblockVersion": "Unblock/Check-in",
"UploadToFolder": "Upload to folder",
"ViewList": "List",

View File

@ -1,5 +1,4 @@
{
"Info": "Info",
"ViewDetails": "View Details",
"ItemsSelected": "Items selected",
"SystemProperties": "System properties",
@ -7,9 +6,7 @@
"Members": "members",
"OpenSharingSettings": "Open sharing settings",
"OFORMsDescription": "Fill out the form online and get a simple Design Project Proposal ready, or just download the fillable template in the desirable format: DOCXF, OFORM, or PDF. <1> Propose a project or a series of projects to an freelance designer team. Outline project and task structure, payments, and terms.</1>",
"Location": "Location",
"FileExtension": "File extension",
"LastModifiedBy": "Last modified by",
"Versions": "Versions",
"Comments": "Comments"
"Versions": "Versions"
}

View File

@ -3,9 +3,11 @@
"Common": "Common",
"CommonSettings": "Common settings",
"ConnectAccounts": "Connect Accounts",
"ConnectEmpty": "There's nothing here",
"ConnectAccountsSubTitle": "No connected accounts",
"ConnectAdminDescription": "For successful connection, enter the necessary data on <1>this page</1>.",
"ConnectDescription": "You can connect the following accounts to the ONLYOFFICE Documents. The documents from these accounts will be available for editing in 'My Documents' section. ",
"ConnectDescriptionText": "You haven't connected any third-party clouds yet.",
"ConnectedCloud": "Connected cloud",
"ConnectMakeShared": "Make shared and put into the 'Common' folder",
"ConnextOtherAccount": "Other account",

View File

@ -1,5 +1,4 @@
{
"AccessRights": "Access rights",
"AddGroupsForSharingButton": "Add groups",
"AddShareMessage": "Add message",
"Comment": "Comment",

View File

@ -1,15 +1,11 @@
{
"Info": "Информация",
"ViewDetails": "Просмотреть подробную информацию",
"ItemsSelected": "Выбрано элементов",
"SystemProperties": "Системные свойства",
"WhoHasAccess": "У кого есть доступ",
"Members": "участников",
"OpenSharingSettings": "Открыть настройки общего доступа",
"Location": "Местоположение",
"FileExtension": "Расширение файла",
"LastModifiedBy": "Автор последнего корректива",
"Versions": "Версии",
"Comments": "Комментарии"
"Versions": "Версии"
}

View File

@ -1,5 +1,4 @@
{
"AccessRights": "Права доступа",
"AddGroupsForSharingButton": "Добавить группы",
"AddShareMessage": "Добавить сообщение",
"Comment": "Комментирование",

View File

@ -10,7 +10,14 @@ const isEditor = pathname.indexOf("doceditor") !== -1;
let loadTimeout = null;
const withLoader = (WrappedComponent) => (Loader) => {
const withLoader = (props) => {
const { tReady, firstLoad, isLoaded, isLoading, viewAs } = props;
const {
tReady,
firstLoad,
isLoaded,
isLoading,
viewAs,
setIsBurgerLoading,
} = props;
const [inLoad, setInLoad] = useState(false);
const cleanTimer = () => {
@ -36,6 +43,14 @@ const withLoader = (WrappedComponent) => (Loader) => {
};
}, [isLoading]);
useEffect(() => {
if ((!isEditor && firstLoad) || !isLoaded || (isMobile && inLoad)) {
setIsBurgerLoading(true);
} else {
setIsBurgerLoading(false);
}
}, [isEditor, firstLoad, isLoaded, isMobile, inLoad]);
return (!isEditor && firstLoad) ||
!isLoaded ||
(isMobile && inLoad) ||
@ -54,11 +69,14 @@ const withLoader = (WrappedComponent) => (Loader) => {
return inject(({ auth, filesStore }) => {
const { firstLoad, isLoading, viewAs } = filesStore;
const { settingsStore } = auth;
const { setIsBurgerLoading } = settingsStore;
return {
firstLoad,
isLoaded: auth.isLoaded,
isLoading,
viewAs,
setIsBurgerLoading,
};
})(observer(withLoader));
};

View File

@ -219,7 +219,11 @@ const EditingWrapperComponent = (props) => {
const onFocus = (e) => e.target.select();
const onBlur = (e) => {
if (e.relatedTarget && e.relatedTarget.classList.contains("edit-button"))
if (
(e.relatedTarget && e.relatedTarget.classList.contains("edit-button")) ||
OkIconIsHovered ||
CancelIconIsHovered
)
return false;
!passwordEntryProcess && onClickUpdateItem(e, false);

View File

@ -55,14 +55,20 @@ const EmptyFoldersContainer = (props) => {
subheadingText,
descriptionText,
buttons,
style,
imageStyle,
buttonStyle,
} = props;
return (
<EmptyFolderWrapper>
<EmptyScreenContainer
className="empty-folder_container"
style={style}
imageStyle={imageStyle}
imageSrc={imageSrc}
imageAlt={imageAlt}
buttonStyle={buttonStyle}
headerText={headerText}
subheadingText={subheadingText}
descriptionText={descriptionText}

View File

@ -31,21 +31,20 @@ const EmptyFilterContainer = ({
className="empty-folder_container-icon"
size="12"
onClick={onResetFilter}
iconName="/static/images/cross.react.svg"
iconName="/static/images/clear.empty.filter.svg"
isFill
/>
<Link onClick={onResetFilter} {...linkStyles}>
{t("Common:ClearButton")}
{t("Common:ClearFilter")}
</Link>
</div>
);
return (
<EmptyContainer
headerText={t("Filter")}
subheadingText={subheadingText}
headerText={t("NotFoundFilterTitle")}
descriptionText={descriptionText}
imageSrc="images/empty_screen_filter.png"
imageSrc="images/empty_screen_filter_alt.svg"
buttons={buttons}
/>
);

View File

@ -75,8 +75,10 @@ const EmptyFolderContainer = ({
return (
<EmptyContainer
headerText={t("EmptyFolderHeader")}
imageSrc="/static/images/empty_screen.png"
headerText={t("EmptyScreenFolder")}
style={{ gridColumnGap: "39px" }}
descriptionText={t("EmptyFolderDecription")}
imageSrc="/static/images/empty_screen_alt.svg"
buttons={buttons}
/>
);

View File

@ -6,12 +6,14 @@ import EmptyContainer from "./EmptyContainer";
import Link from "@appserver/components/link";
import Text from "@appserver/components/text";
import Box from "@appserver/components/box";
import Loaders from "@appserver/common/components/Loaders";
const RootFolderContainer = (props) => {
const {
t,
theme,
isPrivacyFolder,
isRecycleBinFolder,
isDesktop,
isEncryptionSupport,
organizationName,
@ -24,12 +26,15 @@ const RootFolderContainer = (props) => {
setIsLoading,
rootFolderType,
linkStyles,
isLoading,
viewAs,
} = props;
const subheadingText = t("SubheadingEmptyText");
const myDescription = t("MyEmptyContainerDescription");
const shareDescription = t("SharedEmptyContainerDescription");
const commonDescription = t("CommonEmptyContainerDescription");
const trashDescription = t("TrashEmptyContainerDescription");
const trashHeader = t("EmptyScreenFolder");
const trashDescription = t("TrashEmptyDescription");
const favoritesDescription = t("FavoritesEmptyContainerDescription");
const recentDescription = t("RecentEmptyContainerDescription");
@ -42,6 +47,8 @@ const RootFolderContainer = (props) => {
t("PrivateRoomDescriptionUnbreakable"),
];
const [showLoader, setShowLoader] = React.useState(false);
const onGoToMyDocuments = () => {
const newFilter = filter.clone();
setIsLoading(true);
@ -70,6 +77,7 @@ const RootFolderContainer = (props) => {
case FolderType.Favorites:
return {
descriptionText: favoritesDescription,
imageStyle: { margin: "0px 0 0 auto" },
imageSrc: "images/empty_screen_favorites.png",
};
case FolderType.Recent:
@ -85,10 +93,12 @@ const RootFolderContainer = (props) => {
};
case FolderType.TRASH:
return {
headerText: trashHeader,
descriptionText: trashDescription,
style: { gridColumnGap: "39px", gridTemplateColumns: "150px" },
imageSrc: theme.isBase
? "images/empty_screen_trash.png"
: "images/empty_screen_trash_dark.png",
? "images/empty_screen_trash_alt.png"
: "images/empty_screen_trash_alt.png",
buttons: trashButtons,
};
default:
@ -186,15 +196,34 @@ const RootFolderContainer = (props) => {
);
const headerText = isPrivacyFolder ? privateRoomHeader : title;
const subheadingTextProp = isPrivacyFolder ? {} : { subheadingText };
const subheadingTextProp =
isPrivacyFolder || isRecycleBinFolder ? {} : { subheadingText };
const emptyFolderProps = getEmptyFolderProps();
React.useEffect(() => {
if (isLoading) {
setShowLoader(isLoading);
} else {
setTimeout(() => setShowLoader(isLoading), 300);
}
}, [isLoading]);
return (
<EmptyContainer
headerText={headerText}
{...subheadingTextProp}
{...emptyFolderProps}
/>
<>
{showLoader ? (
viewAs === "tile" ? (
<Loaders.Tiles />
) : (
<Loaders.Rows />
)
) : (
<EmptyContainer
headerText={headerText}
{...subheadingTextProp}
{...emptyFolderProps}
/>
)}
</>
);
};
@ -211,14 +240,21 @@ export default inject(
filter,
fetchFiles,
privacyInstructions,
isLoading,
setIsLoading,
viewAs,
} = filesStore;
const { title, rootFolderType } = selectedFolderStore;
const { isPrivacyFolder, myFolderId } = treeFoldersStore;
const {
isPrivacyFolder,
myFolderId,
isRecycleBinFolder,
} = treeFoldersStore;
return {
theme,
isPrivacyFolder,
isRecycleBinFolder,
isDesktop: isDesktopClient,
isEncryptionSupport,
organizationName,
@ -227,8 +263,10 @@ export default inject(
myFolderId,
filter,
fetchFiles,
isLoading,
setIsLoading,
rootFolderType,
viewAs,
};
}
)(withTranslation("Home")(observer(RootFolderContainer)));

View File

@ -22,6 +22,7 @@ const backgroundDragColor = "#EFEFB2";
const backgroundDragEnterColor = "#F8F7BF";
const StyledTreeMenu = styled(TreeMenu)`
width: 100%;
.rc-tree-node-content-wrapper {
background: ${(props) => !props.dragging && "none !important"};
}

View File

@ -234,7 +234,7 @@ class SelectFileDialog extends React.Component {
const buttonName = creationButtonPrimary
? t("Common:Create")
: t("Common:SaveButton");
const name = dialogName ? dialogName : t("Common:SelectFile");
const name = dialogName ? dialogName : t("SelectFile");
// console.log("Render file-component");
return displayType === "aside" ? (

View File

@ -176,7 +176,7 @@ const ExternalLink = ({
</div>
<div className="external-link__access-rights">
<Text className="external-link__access-rights_text">
{t("AccessRights")}:
{t("Common:AccessRights")}:
</Text>
<AccessComboBox
t={t}

View File

@ -1,11 +1,9 @@
import React, { useEffect, useState } from "react";
import { FileType } from "@appserver/common/constants";
import { LANGUAGE } from "@appserver/common/constants";
import { sleep } from "@appserver/common/utils";
import Link from "@appserver/components/link";
import Text from "@appserver/components/text";
import Tooltip from "@appserver/components/tooltip";
import React, { useEffect, useState } from "react";
import { ReactSVG } from "react-svg";
import {
StyledAccess,
StyledAccessItem,
@ -24,7 +22,7 @@ const SingleItem = (props) => {
selectedItem,
onSelectItem,
setSharingPanelVisible,
getFolderInfo,
//getFolderInfo,
getIcon,
getFolderIcon,
getShareUsers,
@ -33,10 +31,8 @@ const SingleItem = (props) => {
dontShowAccess,
personal,
createThumbnail,
getFileInfo,
} = props;
let updateSubscription = true;
const [item, setItem] = useState({
id: "",
isFolder: false,
@ -170,7 +166,7 @@ const SingleItem = (props) => {
},
{
id: "Comments",
title: t("Comments"),
title: t("Common:Comments"),
content: styledText(item.comment),
}
);
@ -199,8 +195,6 @@ const SingleItem = (props) => {
};
const loadAsyncData = async (displayedItem, selectedItem) => {
if (!updateSubscription) return;
if (
!selectedItem.thumbnailUrl &&
!selectedItem.isFolder &&
@ -211,18 +205,6 @@ const SingleItem = (props) => {
selectedItem.fileType === FileType.Document)
) {
await createThumbnail(selectedItem.id);
await sleep(5000);
const newFileInfo = await getFileInfo(selectedItem.id);
if (newFileInfo.thumbnailUrl) {
displayedItem.thumbnailUrl = newFileInfo.thumbnailUrl;
setItem({
...displayedItem,
});
}
}
// const updateLoadedItemProperties = async (displayedItem, selectedItem) => {
@ -248,7 +230,7 @@ const SingleItem = (props) => {
// dip.id === "Location"
// ? {
// id: "Location",
// title: t("Location"),
// title: t("Common:Location"),
// content: (
// <Link
// className="property-content"
@ -327,25 +309,32 @@ const SingleItem = (props) => {
};
useEffect(() => {
if (selectedItem.id !== item.id && updateSubscription)
updateItemsInfo(selectedItem);
return () => (updateSubscription = false);
updateItemsInfo(selectedItem);
}, [selectedItem]);
return (
<>
<StyledTitle>
<ReactSVG className="icon" src={item.iconUrl} />
<img className="icon" src={item.iconUrl} alt="thumbnail-icon" />
<Text className="text">{item.title}</Text>
</StyledTitle>
{item.thumbnailUrl ? (
{selectedItem?.thumbnailUrl ? (
<StyledThumbnail>
<img src={item.thumbnailUrl} alt="" />
<img
src={selectedItem.thumbnailUrl}
alt="thumbnail-image"
height={260}
width={360}
/>
</StyledThumbnail>
) : (
<div className="no-thumbnail-img-wrapper">
<ReactSVG className="no-thumbnail-img" src={item.thumbnailUrl} />
<img
className="no-thumbnail-img"
src={item.thumbnailUrl}
alt="thumbnail-icon-big"
/>
</div>
)}

View File

@ -26,7 +26,6 @@ const InfoPanelBodyContent = ({
gallerySelected,
personal,
createThumbnail,
getFileInfo,
}) => {
const singleItem = (item) => {
const dontShowLocation = item.isFolder && item.parentId === 0;
@ -52,7 +51,6 @@ const InfoPanelBodyContent = ({
dontShowAccess={dontShowAccess}
personal={personal}
createThumbnail={createThumbnail}
getFileInfo={getFileInfo}
/>
);
};
@ -104,7 +102,6 @@ export default inject(
getShareUsers,
gallerySelected,
createThumbnail,
getFileInfo,
} = filesStore;
const { getIcon, getFolderIcon } = settingsStore;
const { onSelectItem } = filesActionsStore;
@ -137,7 +134,6 @@ export default inject(
gallerySelected,
personal,
createThumbnail,
getFileInfo,
};
}
)(

View File

@ -2,7 +2,7 @@ import React from "react";
import { withTranslation } from "react-i18next";
const InfoPanelHeaderContent = ({ t }) => {
return <>{t("Info")}</>;
return <>{t("Common:Info")}</>;
};
export default withTranslation(["InfoPanel"])(InfoPanelHeaderContent);
export default withTranslation(["InfoPanel", "Common"])(InfoPanelHeaderContent);

View File

@ -47,7 +47,7 @@ const SectionBodyContent = (props) => {
customScrollElm && customScrollElm.scrollTo(0, 0);
}
!isMobile && window.addEventListener("mousedown", onMouseDown);
window.addEventListener("mousedown", onMouseDown);
startDrag && window.addEventListener("mouseup", onMouseUp);
startDrag && document.addEventListener("mousemove", onMouseMove);

View File

@ -18,6 +18,8 @@ import { connectedCloudsTypeTitleTranslation } from "../../../../helpers/utils";
import Loaders from "@appserver/common/components/Loaders";
import { tablet } from "@appserver/components/utils/device";
import { ReactSVG } from "react-svg";
import { isMobile } from "react-device-detect";
import { Base } from "@appserver/components/themes";
const linkStyles = {
isHovered: true,
@ -28,8 +30,8 @@ const linkStyles = {
};
const StyledHeader = styled.div`
display: flex;
border-bottom: 1px solid #eceef1;
display: ${isMobile ? "none" : "flex"};
border-bottom: ${(props) => props.theme.connectedClouds.borderBottom};
padding-bottom: 12px;
@media ${tablet} {
@ -52,7 +54,7 @@ const StyledHeader = styled.div`
height: 10px;
margin: 4px 8px 0 0;
z-index: 1;
border-right: 1px solid #d0d5da;
border-right: ${(props) => props.theme.connectedClouds.borderRight};
}
.cloud-settings-header_connection {
@ -61,6 +63,8 @@ const StyledHeader = styled.div`
}
`;
StyledHeader.defaultProps = { theme: Base };
const StyledRow = styled(Row)`
.cloud-settings-row-content {
display: grid;
@ -195,7 +199,7 @@ class ConnectClouds extends React.Component {
className="cloud-settings-clouds"
fontSize="12px"
fontWeight={600}
color="#657077"
color={theme.connectedClouds.color}
>
{t("Clouds")}
</Text>
@ -264,9 +268,11 @@ class ConnectClouds extends React.Component {
</>
) : (
<EmptyFolderContainer
headerText={t("ConnectAccounts")}
subheadingText={t("ConnectAccountsSubTitle")}
imageSrc="/static/images/empty_screen.png"
headerText={t("ConnectEmpty")}
descriptionText={t("ConnectDescriptionText")}
style={{ gridColumnGap: "39px" }}
buttonStyle={{ marginTop: "16px" }}
imageSrc="/static/images/empty_screen_alt.svg"
buttons={
<div className="empty-folder_container-links empty-connect_container-links">
<img
@ -277,7 +283,7 @@ class ConnectClouds extends React.Component {
/>
<Box className="flex-wrapper_container">
<Link onClick={this.onShowThirdPartyDialog} {...linkStyles}>
{t("Translations:AddAccount")}
{t("Common:Connect")}
</Link>
</Box>
</div>
@ -326,7 +332,7 @@ export default inject(
};
}
)(
withTranslation(["Settings", "Translations"])(
withTranslation(["Settings", "Translations", "Common"])(
observer(withRouter(ConnectClouds))
)
);

View File

@ -51,7 +51,7 @@ class ContextOptionsStore {
onOpenFolder = (item) => {
const { id, folderId, fileExst } = item;
const locationId = !fileExst ? id : folderId;
this.filesActionsStore.openLocationAction(locationId, !fileExst);
this.filesActionsStore.openLocationAction(locationId);
};
onClickLinkFillForm = (item) => {
@ -89,7 +89,7 @@ class ContextOptionsStore {
onOpenLocation = (item) => {
const { parentId, folderId, fileExst } = item;
const locationId = !fileExst ? parentId : folderId;
this.filesActionsStore.openLocationAction(locationId, !fileExst);
this.filesActionsStore.openLocationAction(locationId);
};
onOwnerChange = () => {

View File

@ -612,21 +612,15 @@ class FilesActionStore {
}
};
openLocationAction = (locationId, isFolder) => {
openLocationAction = (locationId) => {
const { createNewExpandedKeys, setExpandedKeys } = this.treeFoldersStore;
const locationFilter = isFolder ? this.filesStore.filter : null;
this.filesStore.setBufferSelection(null);
return this.filesStore
.fetchFiles(locationId, locationFilter)
.then((data) => {
const pathParts = data.selectedFolder.pathParts;
const newExpandedKeys = createNewExpandedKeys(pathParts);
setExpandedKeys(newExpandedKeys);
});
/*.then(() =>
//isFolder ? null : this.selectRowAction(!checked, item)
);*/
return this.filesStore.fetchFiles(locationId, null).then((data) => {
const pathParts = data.selectedFolder.pathParts;
const newExpandedKeys = createNewExpandedKeys(pathParts);
setExpandedKeys(newExpandedKeys);
});
};
setThirdpartyInfo = (providerKey) => {

View File

@ -114,6 +114,37 @@ class FilesStore {
this.setFiles(newFiles);
}
break;
case "update":
if (opt?.type == "file" && opt?.data) {
const file = JSON.parse(opt?.data);
if (!file || !file.id) return;
this.setFile(file);
if (this.selection) {
const foundIndex = this.selection?.findIndex(
(x) => x.id === file.id
);
if (foundIndex > -1) {
runInAction(() => {
this.selection[foundIndex] = file;
});
}
}
if (this.bufferSelection) {
const foundIndex = this.bufferSelection?.findIndex(
(x) => x.id === file.id
);
if (foundIndex > -1) {
runInAction(() => {
this.bufferSelection[foundIndex] = file;
});
}
}
}
break;
case "delete":
if (opt?.type == "file" && opt?.id) {
const foundIndex = this.files.findIndex((x) => x.id === opt?.id);

View File

@ -7,10 +7,6 @@ class InfoPanelStore {
makeAutoObservable(this);
}
onHeaderCrossClick = () => {
this.isVisible = false;
};
toggleIsVisible = () => {
this.isVisible = !this.isVisible;
};

View File

@ -82,6 +82,7 @@ namespace ASC.Web.Files.Services.WCFService
{
private static readonly FileEntrySerializer serializer = new FileEntrySerializer();
private readonly OFormRequestManager _oFormRequestManager;
private readonly ThirdPartySelector _thirdPartySelector;
private Global Global { get; }
private GlobalStore GlobalStore { get; }
@ -171,7 +172,8 @@ namespace ASC.Web.Files.Services.WCFService
ICacheNotify<ThumbnailRequest> thumbnailNotify,
EntryStatusManager entryStatusManager,
CompressToArchive compressToArchive,
OFormRequestManager oFormRequestManager)
OFormRequestManager oFormRequestManager,
ThirdPartySelector thirdPartySelector)
{
Global = global;
GlobalStore = globalStore;
@ -217,6 +219,7 @@ namespace ASC.Web.Files.Services.WCFService
EntryStatusManager = entryStatusManager;
CompressToArchive = compressToArchive;
_oFormRequestManager = oFormRequestManager;
_thirdPartySelector = thirdPartySelector;
}
public async Task<Folder<T>> GetFolderAsync(T folderId)
@ -838,7 +841,7 @@ namespace ASC.Web.Files.Services.WCFService
{
ErrorIf(FileTracker.IsEditing(fileId), FilesCommonResource.ErrorMassage_SecurityException_EditFileTwice);
app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app == null)
{
await EntryManager.TrackEditingAsync(fileId, Guid.Empty, AuthContext.CurrentAccount.ID, doc, true);
@ -850,7 +853,7 @@ namespace ASC.Web.Files.Services.WCFService
(File<string> File, Configuration<string> Configuration) fileOptions;
app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app == null)
{
fileOptions = await DocumentServiceHelper.GetParamsAsync(fileId.ToString(), -1, doc, true, true, false);

View File

@ -40,34 +40,32 @@ using ASC.Web.Studio.Utility;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace ASC.Web.Files.HttpHandlers
{
public class ThirdPartyAppHandler
{
private RequestDelegate Next { get; }
private IServiceProvider ServiceProvider { get; }
private RequestDelegate Next { get; }
public static string HandlerPath = "~/ThirdPartyApp";
public ThirdPartyAppHandler(RequestDelegate next, IServiceProvider serviceProvider)
public ThirdPartyAppHandler(RequestDelegate next)
{
Next = next;
ServiceProvider = serviceProvider;
}
public async Task Invoke(HttpContext context)
public async Task Invoke(HttpContext context, ThirdPartyAppHandlerService thirdPartyAppHandlerService)
{
using var scope = ServiceProvider.CreateScope();
var thirdPartyAppHandlerService = scope.ServiceProvider.GetService<ThirdPartyAppHandlerService>();
await thirdPartyAppHandlerService.InvokeAsync(context);
await Next.Invoke(context);
//await Next.Invoke(context);
}
}
[Scope]
public class ThirdPartyAppHandlerService
{
{
private readonly ThirdPartySelector _thirdPartySelector;
private AuthContext AuthContext { get; }
private CommonLinkUtility CommonLinkUtility { get; }
private ILog Log { get; set; }
@ -78,12 +76,14 @@ namespace ASC.Web.Files.HttpHandlers
IOptionsMonitor<ILog> optionsMonitor,
AuthContext authContext,
BaseCommonLinkUtility baseCommonLinkUtility,
CommonLinkUtility commonLinkUtility)
CommonLinkUtility commonLinkUtility,
ThirdPartySelector thirdPartySelector)
{
AuthContext = authContext;
CommonLinkUtility = commonLinkUtility;
CommonLinkUtility = commonLinkUtility;
_thirdPartySelector = thirdPartySelector;
Log = optionsMonitor.CurrentValue;
HandlerPath = baseCommonLinkUtility.ToAbsolute("~/thirdpartyapp");
HandlerPath = baseCommonLinkUtility.ToAbsolute(ThirdPartyAppHandler.HandlerPath);
}
public async Task InvokeAsync(HttpContext context)
@ -94,7 +94,7 @@ namespace ASC.Web.Files.HttpHandlers
try
{
var app = ThirdPartySelector.GetApp(context.Request.Query[ThirdPartySelector.AppAttr]);
var app = _thirdPartySelector.GetApp(context.Request.Query[ThirdPartySelector.AppAttr]);
Log.Debug("ThirdPartyApp: app - " + app);
if (await app.RequestAsync(context))

View File

@ -31,7 +31,6 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Web;
using ASC.Common;
@ -70,9 +69,9 @@ namespace ASC.Web.Files.Services.DocumentService
{
internal static readonly Dictionary<FileType, string> DocType = new Dictionary<FileType, string>
{
{ FileType.Document, "text" },
{ FileType.Spreadsheet, "spreadsheet" },
{ FileType.Presentation, "presentation" }
{ FileType.Document, "word" },
{ FileType.Spreadsheet, "cell" },
{ FileType.Presentation, "slide" }
};
private FileType _fileTypeCache = FileType.Unknown;
@ -214,14 +213,22 @@ namespace ASC.Web.Files.Services.DocumentService
UserManager = userManager;
}
private bool? _favorite;
private bool _favoriteIsSet;
public bool? Favorite
{
get
{
if (_favoriteIsSet) return _favorite;
if (!SecurityContext.IsAuthenticated || UserManager.GetUsers(SecurityContext.CurrentAccount.ID).IsVisitor(UserManager)) return null;
if (File.Encrypted) return null;
return File.IsFavorite;
}
set
{
_favoriteIsSet = true;
_favorite = value;
}
}
public string Folder
@ -657,7 +664,8 @@ namespace ASC.Web.Files.Services.DocumentService
PathProvider pathProvider,
CustomerConfig<T> customerConfig,
LogoConfig<T> logoConfig,
FileSharing fileSharing)
FileSharing fileSharing,
ThirdPartySelector thirdPartySelector)
{
CoreBaseSettings = coreBaseSettings;
SettingsManager = settingsManager;
@ -671,9 +679,11 @@ namespace ASC.Web.Files.Services.DocumentService
Customer = customerConfig;
Logo = logoConfig;
FileSharing = fileSharing;
_thirdPartySelector = thirdPartySelector;
}
private Configuration<T> _configuration;
private readonly ThirdPartySelector _thirdPartySelector;
internal void SetConfiguration(Configuration<T> configuration)
{
@ -682,7 +692,8 @@ namespace ASC.Web.Files.Services.DocumentService
Logo.SetConfiguration(_configuration);
}
//private string _gobackUrl;
[JsonIgnore]
public string GobackUrl;
public bool IsRetina { get; set; } = false;
@ -716,7 +727,7 @@ namespace ASC.Web.Files.Services.DocumentService
{
return FileUtility.CanForcesave
&& !_configuration.Document.Info.GetFile().ProviderEntry
&& ThirdPartySelector.GetAppByFileId(_configuration.Document.Info.GetFile().ID.ToString()) == null
&& _thirdPartySelector.GetAppByFileId(_configuration.Document.Info.GetFile().ID.ToString()) == null
&& FilesSettingsHelper.Forcesave;
}
}
@ -727,13 +738,13 @@ namespace ASC.Web.Files.Services.DocumentService
{
if (_configuration.EditorType == EditorType.Embedded || _configuration.EditorType == EditorType.External) return null;
if (!AuthContext.IsAuthenticated) return null;
//if (_gobackUrl != null)
//{
// return new GobackConfig
// {
// Url = _gobackUrl,
// };
//}
if (GobackUrl != null)
{
return new GobackConfig
{
Url = GobackUrl,
};
}
var folderDao = DaoFactory.GetFolderDao<T>();
try

View File

@ -164,7 +164,9 @@ namespace ASC.Web.Files.Services.DocumentService
[Scope]
public class DocumentServiceTrackerHelper
{
{
private readonly ThirdPartySelector _thirdPartySelector;
private SecurityContext SecurityContext { get; }
private UserManager UserManager { get; }
private TenantManager TenantManager { get; }
@ -206,7 +208,8 @@ namespace ASC.Web.Files.Services.DocumentService
NotifyClient notifyClient,
MailMergeTaskRunner mailMergeTaskRunner,
FileTrackerHelper fileTracker,
IHttpClientFactory clientFactory)
IHttpClientFactory clientFactory,
ThirdPartySelector thirdPartySelector)
{
SecurityContext = securityContext;
UserManager = userManager;
@ -228,6 +231,7 @@ namespace ASC.Web.Files.Services.DocumentService
FileTracker = fileTracker;
Logger = options.CurrentValue;
ClientFactory = clientFactory;
_thirdPartySelector = thirdPartySelector;
}
public string GetCallbackUrl<T>(T fileId)
@ -275,7 +279,7 @@ namespace ASC.Web.Files.Services.DocumentService
private async Task ProcessEditAsync<T>(T fileId, TrackerData fileData)
{
if (ThirdPartySelector.GetAppByFileId(fileId.ToString()) != null)
if (_thirdPartySelector.GetAppByFileId(fileId.ToString()) != null)
{
return;
}
@ -284,7 +288,7 @@ namespace ASC.Web.Files.Services.DocumentService
var usersDrop = new List<string>();
string docKey;
var app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
var app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app == null)
{
File<T> fileStable;
@ -356,7 +360,7 @@ namespace ASC.Web.Files.Services.DocumentService
userId = Guid.Empty;
}
var app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
var app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app == null)
{
File<T> fileStable;

View File

@ -30,13 +30,12 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web;
using ASC.Common;
using ASC.Common.Caching;
using ASC.Common.Logging;
using ASC.Common.Web;
@ -73,7 +72,8 @@ using Newtonsoft.Json.Linq;
using SecurityContext = ASC.Core.SecurityContext;
namespace ASC.Web.Files.ThirdPartyApp
{
{
[Scope]
public class BoxApp : Consumer, IThirdPartyApp, IOAuthProvider
{
public const string AppAttr = "box";
@ -329,43 +329,35 @@ namespace ASC.Web.Files.ThirdPartyApp
var httpClient = _clientFactory.CreateClient();
var request = new HttpRequestMessage();
request.RequestUri = new Uri(BoxUrlUpload.Replace("{fileId}", fileId));
using (var tmpStream = new MemoryStream())
{
var boundary = DateTime.UtcNow.Ticks.ToString("x");
var metadata = $"Content-Disposition: form-data; name=\"filename\"; filename=\"{title}\"\r\nContent-Type: application/octet-stream\r\n\r\n";
var metadataPart = $"--{boundary}\r\n{metadata}";
var bytes = Encoding.UTF8.GetBytes(metadataPart);
await tmpStream.WriteAsync(bytes, 0, bytes.Length);
if (stream != null)
{
await stream.CopyToAsync(tmpStream);
}
else
{
var downloadRequest = new HttpRequestMessage();
downloadRequest.RequestUri = new Uri(downloadUrl);
using var response = await httpClient.SendAsync(request);
using var downloadStream = new ResponseStream(response);
await downloadStream.CopyToAsync(tmpStream);
}
var mediaPartEnd = $"\r\n--{boundary}--\r\n";
bytes = Encoding.UTF8.GetBytes(mediaPartEnd);
await tmpStream.WriteAsync(bytes, 0, bytes.Length);
request.Method = HttpMethod.Post;
request.Headers.Add("Authorization", "Bearer " + token);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data; boundary=" + boundary);
Logger.Debug("BoxApp: save file totalSize - " + tmpStream.Length);
tmpStream.Seek(0, SeekOrigin.Begin);
request.Content = new StreamContent(tmpStream);
}
request.RequestUri = new Uri(BoxUrlUpload.Replace("{fileId}", fileId));
StreamContent streamContent;
using var multipartFormContent = new MultipartFormDataContent();
if (stream != null)
{
streamContent = new StreamContent(stream);
}
else
{
var downloadRequest = new HttpRequestMessage();
downloadRequest.RequestUri = new Uri(downloadUrl);
var response = await httpClient.SendAsync(downloadRequest);
var downloadStream = new ResponseStream(response);
streamContent = new StreamContent(downloadStream);
}
streamContent.Headers.TryAddWithoutValidation("Content-Type", MimeMapping.GetMimeMapping(title));
multipartFormContent.Add(streamContent, name: "filename", fileName: title);
request.Content = multipartFormContent;
request.Method = HttpMethod.Post;
request.Headers.Add("Authorization", "Bearer " + token);
//request.Content.Headers.ContentType = new MediaTypeHeaderValue("multipart/form-data; boundary=" + boundary);
//Logger.Debug("BoxApp: save file totalSize - " + tmpStream.Length);
try
{
using var response = await httpClient.SendAsync(request);

View File

@ -31,12 +31,13 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web;
using ASC.Common;
using ASC.Common.Caching;
using ASC.Common.Logging;
using ASC.Common.Web;
@ -75,7 +76,8 @@ using MimeMapping = ASC.Common.Web.MimeMapping;
using SecurityContext = ASC.Core.SecurityContext;
namespace ASC.Web.Files.ThirdPartyApp
{
{
[Scope]
public class GoogleDriveApp : Consumer, IThirdPartyApp, IOAuthProvider
{
public const string AppAttr = "gdrive";
@ -123,7 +125,7 @@ namespace ASC.Web.Files.ThirdPartyApp
private IServiceProvider ServiceProvider { get; }
private readonly RequestHelper _requestHelper;
private readonly ThirdPartySelector _thirdPartySelector;
private readonly IHttpClientFactory _clientFactory;
private readonly OAuth20TokenHelper _oAuth20TokenHelper;
@ -166,7 +168,8 @@ namespace ASC.Web.Files.ThirdPartyApp
ConsumerFactory consumerFactory,
IHttpClientFactory clientFactory,
OAuth20TokenHelper oAuth20TokenHelper,
RequestHelper requestHelper,
RequestHelper requestHelper,
ThirdPartySelector thirdPartySelector,
string name, int order, Dictionary<string, string> additional)
: base(tenantManager, coreBaseSettings, coreSettings, configuration, cache, consumerFactory, name, order, additional)
{
@ -198,6 +201,7 @@ namespace ASC.Web.Files.ThirdPartyApp
_clientFactory = clientFactory;
_oAuth20TokenHelper = oAuth20TokenHelper;
_requestHelper = requestHelper;
_thirdPartySelector = thirdPartySelector;
}
public async Task<bool> RequestAsync(HttpContext context)
@ -338,7 +342,7 @@ namespace ASC.Web.Files.ThirdPartyApp
request.RequestUri = new Uri(GoogleLoginProvider.GoogleUrlFileUpload + "/{fileId}?uploadType=media".Replace("{fileId}", fileId));
request.Method = HttpMethod.Patch;
request.Headers.Add("Authorization", "Bearer " + token);
request.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(currentType));
if (stream != null)
{
@ -346,14 +350,16 @@ namespace ASC.Web.Files.ThirdPartyApp
}
else
{
using var response = await httpClient.SendAsync(request);
using var downloadStream = new ResponseStream(response);
var response = await httpClient.GetAsync(downloadUrl);
var downloadStream = new ResponseStream(response);
request.Content = new StreamContent(downloadStream);
}
}
request.Content.Headers.ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(currentType));
try
{
{
httpClient = _clientFactory.CreateClient();
using var response = await httpClient.SendAsync(request);
using var responseStream = await response.Content.ReadAsStreamAsync();
string result = null;
@ -470,15 +476,21 @@ namespace ASC.Web.Files.ThirdPartyApp
{
Logger.Debug("GoogleDriveApp: file must be converted");
if (FilesSettingsHelper.ConvertNotify)
{
//context.Response.Redirect(App.Location + "?" + FilesLinkUtility.FileId + "=" + HttpUtility.UrlEncode(fileId), true);
{
context.Response.Redirect(
BaseCommonLinkUtility.ToAbsolute(ThirdPartyAppHandler.HandlerPath)
+ "?" + FilesLinkUtility.Action + "=convert"
+ "&" + FilesLinkUtility.FileId + "=" + HttpUtility.UrlEncode(fileId)
+ "&" + ThirdPartySelector.AppAttr + "=" + AppAttr,
false);
return;
}
fileId = await CreateConvertedFileAsync(driveFile, token);
}
context.Response.Redirect(FilesLinkUtility.GetFileWebEditorUrl(ThirdPartySelector.BuildAppFileId(AppAttr, fileId)), true);
context.Response.Redirect(FilesLinkUtility.GetFileWebEditorUrl(ThirdPartySelector.BuildAppFileId(AppAttr, fileId)), false);
await context.Response.CompleteAsync();
return;
}
Logger.Error("GoogleDriveApp: Action not identified");
@ -528,10 +540,13 @@ namespace ASC.Web.Files.ThirdPartyApp
{
Logger.Error("GoogleDriveApp: downloadUrl is null");
throw new Exception("downloadUrl is null");
}
}
var contentLength = jsonFile.Value<string>("size");
Logger.Debug("GoogleDriveApp: get file stream contentLength - " + contentLength);
context.Response.Headers.Add("Content-Length", contentLength);
Logger.Debug("GoogleDriveApp: get file stream downloadUrl - " + downloadUrl);
var request = new HttpRequestMessage();
request.RequestUri = new Uri(downloadUrl);
request.Method = HttpMethod.Get;
@ -541,10 +556,6 @@ namespace ASC.Web.Files.ThirdPartyApp
using var response = await httpClient.SendAsync(request);
using var stream = new ResponseStream(response);
await stream.CopyToAsync(context.Response.Body);
var contentLength = jsonFile.Value<string>("size");
Logger.Debug("GoogleDriveApp: get file stream contentLength - " + contentLength);
context.Response.Headers.Add("Content-Length", contentLength);
}
catch (Exception ex)
{
@ -663,7 +674,7 @@ namespace ASC.Web.Files.ThirdPartyApp
LoginProfile loginProfile = null;
try
{
loginProfile = GoogleLoginProvider.Instance.GetLoginProfile(token.GetRefreshedToken(TokenHelper, _oAuth20TokenHelper));
loginProfile = GoogleLoginProvider.Instance.GetLoginProfile(token.GetRefreshedToken(TokenHelper, _oAuth20TokenHelper, _thirdPartySelector));
}
catch (Exception ex)
{
@ -761,36 +772,28 @@ namespace ASC.Web.Files.ThirdPartyApp
var request = new HttpRequestMessage();
request.RequestUri = new Uri(GoogleLoginProvider.GoogleUrlFileUpload + "?uploadType=multipart");
using (var tmpStream = new MemoryStream())
{
var boundary = DateTime.UtcNow.Ticks.ToString("x");
var folderdata = string.IsNullOrEmpty(folderId) ? "" : $",\"parents\":[\"{folderId}\"]";
var metadata = "{{\"name\":\"" + fileName + "\"" + folderdata + "}}";
var metadataPart = $"\r\n--{boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n{metadata}";
var bytes = Encoding.UTF8.GetBytes(metadataPart);
await tmpStream.WriteAsync(bytes, 0, bytes.Length);
var mediaPartStart = $"\r\n--{boundary}\r\nContent-Type: {MimeMapping.GetMimeMapping(fileName)}\r\n\r\n";
bytes = Encoding.UTF8.GetBytes(mediaPartStart);
await tmpStream.WriteAsync(bytes, 0, bytes.Length);
await content.CopyToAsync(tmpStream);
var mediaPartEnd = $"\r\n--{boundary}--\r\n";
bytes = Encoding.UTF8.GetBytes(mediaPartEnd);
await tmpStream.WriteAsync(bytes, 0, bytes.Length);
request.Method = HttpMethod.Post;
request.Headers.Add("Authorization", "Bearer " + token);
request.Content.Headers.ContentType = new MediaTypeHeaderValue("multipart/related; boundary=" + boundary);
Logger.Debug("GoogleDriveApp: create file totalSize - " + tmpStream.Length);
request.Content = new StreamContent(tmpStream);
}
var boundary = DateTime.UtcNow.Ticks.ToString("x");
request.Method = HttpMethod.Post;
request.Headers.Add("Authorization", "Bearer " + token);
var stringContent = new { name = fileName, parents = new List<string>() };
if (!string.IsNullOrEmpty(folderId))
{
stringContent.parents.Add(folderId);
}
var streamContent = new StreamContent(content);
streamContent.Headers.TryAddWithoutValidation("Content-Type", MimeMapping.GetMimeMapping(fileName));
var multipartContent = new MultipartContent("related", boundary);
multipartContent.Add(JsonContent.Create(stringContent));
multipartContent.Add(streamContent);
request.Content = multipartContent;
//Logger.Debug("GoogleDriveApp: create file totalSize - " + tmpStream.Length);
try
{
using var response = await httpClient.SendAsync(request);

View File

@ -26,8 +26,10 @@
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Threading.Tasks;
using ASC.Common;
using ASC.Core.Common.Configuration;
using ASC.Files.Core;
using Microsoft.AspNetCore.Http;
@ -45,11 +47,18 @@ namespace ASC.Web.Files.ThirdPartyApp
string GetFileStreamUrl(File<string> file);
Task SaveFileAsync(string fileId, string fileType, string downloadUrl, Stream stream);
}
public static class ThirdPartySelector
[Scope(Additional = typeof(ThirdPartySelectorExtension))]
public class ThirdPartySelector
{
public const string AppAttr = "app";
public static readonly Regex AppRegex = new Regex("^" + AppAttr + @"-(\S+)\|(\S+)$", RegexOptions.Singleline | RegexOptions.Compiled);
public static readonly Regex AppRegex = new Regex("^" + AppAttr + @"-(\S+)\|(\S+)$", RegexOptions.Singleline | RegexOptions.Compiled);
private readonly ConsumerFactory _consumerFactory;
public ThirdPartySelector(ConsumerFactory consumerFactory)
{
_consumerFactory = consumerFactory;
}
public static string BuildAppFileId(string app, object fileId)
{
@ -61,7 +70,7 @@ namespace ASC.Web.Files.ThirdPartyApp
return AppRegex.Match(appFileId).Groups[2].Value;
}
public static IThirdPartyApp GetAppByFileId(string fileId)
public IThirdPartyApp GetAppByFileId(string fileId)
{
if (string.IsNullOrEmpty(fileId)) return null;
var match = AppRegex.Match(fileId);
@ -70,14 +79,23 @@ namespace ASC.Web.Files.ThirdPartyApp
: null;
}
public static IThirdPartyApp GetApp(string app)
public IThirdPartyApp GetApp(string app)
{
return app switch
{
GoogleDriveApp.AppAttr => new GoogleDriveApp(),
BoxApp.AppAttr => new BoxApp(),
_ => new GoogleDriveApp(),
GoogleDriveApp.AppAttr => _consumerFactory.Get<GoogleDriveApp>(),
BoxApp.AppAttr => _consumerFactory.Get<BoxApp>(),
_ => _consumerFactory.Get<GoogleDriveApp>(),
};
}
}
public class ThirdPartySelectorExtension
{
public static void Register(DIHelper services)
{
services.TryAdd<GoogleDriveApp>();
services.TryAdd<BoxApp>();
}
}
}

View File

@ -53,11 +53,11 @@ namespace ASC.Web.Files.ThirdPartyApp
App = app;
}
public string GetRefreshedToken(TokenHelper tokenHelper, OAuth20TokenHelper oAuth20TokenHelper)
public string GetRefreshedToken(TokenHelper tokenHelper, OAuth20TokenHelper oAuth20TokenHelper, ThirdPartySelector thirdPartySelector)
{
if (IsExpired)
{
var app = ThirdPartySelector.GetApp(App);
var app = thirdPartySelector.GetApp(App);
try
{
tokenHelper.Logger.Debug("Refresh token for app: " + App);

View File

@ -60,19 +60,22 @@ namespace ASC.Web.Files.Utils
{
[Scope]
public class LockerManager
{
{
private readonly ThirdPartySelector _thirdPartySelector;
private AuthContext AuthContext { get; }
private IDaoFactory DaoFactory { get; }
public LockerManager(AuthContext authContext, IDaoFactory daoFactory)
public LockerManager(AuthContext authContext, IDaoFactory daoFactory, ThirdPartySelector thirdPartySelector)
{
AuthContext = authContext;
DaoFactory = daoFactory;
DaoFactory = daoFactory;
_thirdPartySelector = thirdPartySelector;
}
public bool FileLockedForMe<T>(T fileId, Guid userId = default)
{
var app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
var app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app != null)
{
return false;
@ -86,7 +89,7 @@ namespace ASC.Web.Files.Utils
public async Task<bool> FileLockedForMeAsync<T>(T fileId, Guid userId = default)
{
var app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
var app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app != null)
{
return false;
@ -260,7 +263,8 @@ namespace ASC.Web.Files.Utils
[Scope]
public class EntryManager
{
private const string UPDATE_LIST = "filesUpdateList";
private const string UPDATE_LIST = "filesUpdateList";
private readonly ThirdPartySelector _thirdPartySelector;
private ICache Cache { get; set; }
private FileTrackerHelper FileTracker { get; }
@ -315,7 +319,8 @@ namespace ASC.Web.Files.Utils
ICache cache,
FileTrackerHelper fileTracker,
EntryStatusManager entryStatusManager,
IHttpClientFactory clientFactory)
IHttpClientFactory clientFactory,
ThirdPartySelector thirdPartySelector)
{
DaoFactory = daoFactory;
FileSecurity = fileSecurity;
@ -342,7 +347,8 @@ namespace ASC.Web.Files.Utils
Cache = cache;
FileTracker = fileTracker;
EntryStatusManager = entryStatusManager;
ClientFactory = clientFactory;
ClientFactory = clientFactory;
_thirdPartySelector = thirdPartySelector;
}
public async Task<(IEnumerable<FileEntry> Entries, int Total)> GetEntriesAsync<T>(Folder<T> parent, int from, int count, FilterType filter, bool subjectGroup, Guid subjectId, string searchText, bool searchInContent, bool withSubfolders, OrderBy orderBy)
@ -1008,7 +1014,7 @@ namespace ASC.Web.Files.Utils
? FileUtility.GetFileExtension(downloadUri)
: fileExtension;
var app = ThirdPartySelector.GetAppByFileId(fileId.ToString());
var app = _thirdPartySelector.GetAppByFileId(fileId.ToString());
if (app != null)
{
await app.SaveFileAsync(fileId.ToString(), newExtension, downloadUri, stream);

View File

@ -88,6 +88,22 @@ namespace ASC.Web.Files.Utils
_signalrServiceClient.CreateFile(file.ID, room, data);
}
public async Task UpdateFileAsync<T>(File<T> file)
{
var room = GetFolderRoom(file.FolderID);
var serializerSettings = new JsonSerializerOptions()
{
WriteIndented = false,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
serializerSettings.Converters.Add(new ApiDateTimeConverter());
serializerSettings.Converters.Add(new FileEntryWrapperConverter());
var data = JsonSerializer.Serialize(await FilesWrapperHelper.GetAsync(file), serializerSettings);
_signalrServiceClient.UpdateFile(file.ID, room, data);
}
public void DeleteFile<T>(File<T> file)
{
var room = GetFolderRoom(file.FolderID);

View File

@ -43,6 +43,7 @@ using ASC.FederatedLogin.Helpers;
using ASC.FederatedLogin.LoginProviders;
using ASC.Files.Core;
using ASC.Files.Core.Model;
using ASC.Files.Core.Security;
using ASC.Files.Helpers;
using ASC.Files.Model;
using ASC.MessagingSystem;
@ -55,6 +56,7 @@ using ASC.Web.Files.Helpers;
using ASC.Web.Files.Services.DocumentService;
using ASC.Web.Files.Services.WCFService;
using ASC.Web.Files.Services.WCFService.FileOperations;
using ASC.Web.Files.ThirdPartyApp;
using ASC.Web.Files.Utils;
using ASC.Web.Studio.Core;
using ASC.Web.Studio.Utility;
@ -77,6 +79,8 @@ namespace ASC.Api.Documents
{
private readonly FileStorageService<string> FileStorageService;
private readonly RequestHelper _requestHelper;
private readonly ThirdPartySelector _thirdPartySelector;
private readonly DocumentServiceHelper _documentServiceHelper;
private FilesControllerHelper<string> FilesControllerHelperString { get; }
private FilesControllerHelper<int> FilesControllerHelperInt { get; }
@ -131,6 +135,8 @@ namespace ASC.Api.Documents
FileUtility fileUtility,
ConsumerFactory consumerFactory,
RequestHelper requestHelper,
ThirdPartySelector thirdPartySelector,
DocumentServiceHelper documentServiceHelper,
IServiceProvider serviceProvider)
{
FilesControllerHelperString = filesControllerHelperString;
@ -156,7 +162,9 @@ namespace ASC.Api.Documents
ProductEntryPoint = productEntryPoint;
TenantManager = tenantManager;
FileUtility = fileUtility;
this._requestHelper = requestHelper;
_requestHelper = requestHelper;
_thirdPartySelector = thirdPartySelector;
_documentServiceHelper = documentServiceHelper;
ServiceProvider = serviceProvider;
}
@ -687,6 +695,22 @@ namespace ASC.Api.Documents
return FilesControllerHelperString.OpenEditAsync(fileId, version, doc, view);
}
[AllowAnonymous]
[Read("file/app-{fileId}/openedit", Check = false)]
public async Task<Configuration<string>> OpenEditThirdPartyAsync(string fileId, int version, string doc, bool view)
{
fileId = "app-" + fileId;
var app = _thirdPartySelector.GetAppByFileId(fileId?.ToString());
bool editable;
var file = app.GetFile(fileId?.ToString(), out editable);
var docParams = await _documentServiceHelper.GetParamsAsync(file, true, editable ? FileShare.ReadWrite : FileShare.Read, false, editable, editable, editable, false);
var configuration = docParams.Configuration;
configuration.Document.Url = app.GetFileStreamUrl(file);
configuration.Document.Info.Favorite = null;
configuration.EditorConfig.Customization.GobackUrl = string.Empty;
return FilesControllerHelperString.OpenEditAsync(configuration);
}
[AllowAnonymous]
[Read("file/{fileId:int}/openedit", Check = false)]
public Task<Configuration<int>> OpenEditAsync(int fileId, int version, string doc, bool view)
@ -1141,6 +1165,16 @@ namespace ASC.Api.Documents
return FilesControllerHelperString.GetFileInfoAsync(fileId, version);
}
[Read("file/app-{fileId}", order: int.MaxValue, DisableFormat = true)]
public async Task<FileEntryWrapper> GetFileInfoThirdPartyAsync(string fileId, int version = -1)
{
fileId = "app-" + fileId;
var app = _thirdPartySelector.GetAppByFileId(fileId?.ToString());
var file = app.GetFile(fileId?.ToString(), out var editable);
var docParams = await _documentServiceHelper.GetParamsAsync(file, true, editable ? FileShare.ReadWrite : FileShare.Read, false, editable, editable, editable, false);
return await FilesControllerHelperString.GetFileEntryWrapperAsync(docParams.File);
}
[Read("file/{fileId:int}")]
public Task<FileWrapper<int>> GetFileInfoAsync(int fileId, int version = -1)
{

View File

@ -248,8 +248,11 @@ namespace ASC.Files.Helpers
public async Task<Configuration<T>> OpenEditAsync(T fileId, int version, string doc, bool view)
{
var docParams = await DocumentServiceHelper.GetParamsAsync(fileId, version, doc, true, !view, true);
var configuration = docParams.Configuration;
return OpenEditAsync(docParams.Configuration);
}
public Configuration<T> OpenEditAsync(Configuration<T> configuration)
{
configuration.EditorType = EditorType.External;
if (configuration.EditorConfig.ModeWrite)
{

View File

@ -1,3 +1,4 @@
using System;
using System.Text;
using System.Text.Json.Serialization;
@ -58,28 +59,28 @@ namespace ASC.Files
base.Configure(app, env);
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("httphandlers/filehandler.ashx"),
context => context.Request.Path.ToString().EndsWith("httphandlers/filehandler.ashx", StringComparison.OrdinalIgnoreCase),
appBranch =>
{
appBranch.UseFileHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("ChunkedUploader.ashx"),
context => context.Request.Path.ToString().EndsWith("ChunkedUploader.ashx", StringComparison.OrdinalIgnoreCase),
appBranch =>
{
appBranch.UseChunkedUploaderHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("ThirdPartyAppHandler.ashx"),
context => context.Request.Path.ToString().EndsWith("ThirdPartyApp", StringComparison.OrdinalIgnoreCase),
appBranch =>
{
appBranch.UseThirdPartyAppHandler();
});
app.MapWhen(
context => context.Request.Path.ToString().EndsWith("DocuSignHandler.ashx"),
context => context.Request.Path.ToString().EndsWith("httphandlers/DocuSignHandler.ashx", StringComparison.OrdinalIgnoreCase),
appBranch =>
{
appBranch.UseDocuSignHandler();

View File

@ -20,7 +20,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ASC.Common;
@ -31,14 +30,13 @@ using ASC.Files.Core;
using ASC.Web.Core.Files;
using ASC.Web.Core.Users;
using ASC.Web.Files.Classes;
using ASC.Web.Files.Core;
using ASC.Web.Files.Services.DocumentService;
using ASC.Web.Files.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Png;
namespace ASC.Files.ThumbnailBuilder
{
@ -93,7 +91,9 @@ namespace ASC.Files.ThumbnailBuilder
private DocumentServiceHelper DocumentServiceHelper { get; }
private Global Global { get; }
private PathProvider PathProvider { get; }
private IHttpClientFactory ClientFactory { get; }
private IHttpClientFactory ClientFactory { get; }
public SocketManager SocketManager { get; }
public Builder(
ThumbnailSettings settings,
@ -104,7 +104,8 @@ namespace ASC.Files.ThumbnailBuilder
Global global,
PathProvider pathProvider,
IOptionsMonitor<ILog> log,
IHttpClientFactory clientFactory)
IHttpClientFactory clientFactory,
SocketManager socketManager)
{
this.config = settings;
TenantManager = tenantManager;
@ -114,7 +115,8 @@ namespace ASC.Files.ThumbnailBuilder
Global = global;
PathProvider = pathProvider;
logger = log.Get("ASC.Files.ThumbnailBuilder");
ClientFactory = clientFactory;
ClientFactory = clientFactory;
SocketManager = socketManager;
}
internal async Task BuildThumbnail(FileData<T> fileData)
@ -172,13 +174,17 @@ namespace ASC.Files.ThumbnailBuilder
}
if (IsImage(file))
{
await CropImage(fileDao, file);
{
await CropImage(fileDao, file);
}
else
{
await MakeThumbnail(fileDao, file);
}
}
var newFile = await fileDao.GetFileStableAsync(file.ID);
await SocketManager.UpdateFileAsync(newFile);
}
catch (Exception exception)
{
@ -239,7 +245,7 @@ namespace ASC.Files.ThumbnailBuilder
attempt++;
}
Thread.Sleep(config.AttemptWaitInterval);
await Task.Delay(config.AttemptWaitInterval);
}
while (string.IsNullOrEmpty(thumbnailUrl));
@ -297,7 +303,7 @@ namespace ASC.Files.ThumbnailBuilder
var httpClient = ClientFactory.CreateClient();
using var response = httpClient.Send(request);
using (var stream = new ResponseStream(response))
using (var stream = await response.Content.ReadAsStreamAsync())
{
await Crop(fileDao, file, stream);
}
@ -313,10 +319,10 @@ namespace ASC.Files.ThumbnailBuilder
private async Task CropImage(IFileDao<T> fileDao, File<T> file)
{
logger.DebugFormat("CropImage: FileId: {0}.", file.ID);
logger.DebugFormat("CropImage: FileId: {0}.", file.ID);
using (var stream = await fileDao.GetFileStreamAsync(file))
{
{
await Crop(fileDao, file, stream);
}
@ -325,13 +331,13 @@ namespace ASC.Files.ThumbnailBuilder
private async Task Crop(IFileDao<T> fileDao, File<T> file, Stream stream)
{
using (var sourceImg = Image.Load(stream))
using (var sourceImg = await Image.LoadAsync(stream))
{
using (var targetImg = GetImageThumbnail(sourceImg))
{
using (var targetStream = new MemoryStream())
{
targetImg.Save(targetStream, PngFormat.Instance);
await targetImg.SaveAsJpegAsync(targetStream);
//targetImg.Save(targetStream, JpegFormat.Instance);
await fileDao.SaveThumbnailAsync(file, targetStream);
}
@ -341,13 +347,13 @@ namespace ASC.Files.ThumbnailBuilder
}
private Image GetImageThumbnail(Image sourceBitmap)
{
//bad for small or disproportionate images
{
//bad for small or disproportionate images
//return sourceBitmap.GetThumbnailImage(config.ThumbnaillWidth, config.ThumbnaillHeight, () => false, IntPtr.Zero);
var targetSize = new Size(Math.Min(sourceBitmap.Width, config.ThumbnaillWidth), Math.Min(sourceBitmap.Height, config.ThumbnaillHeight));
var point = new Point(0, 0);
var size = targetSize;
var size = targetSize;
if (sourceBitmap.Width > config.ThumbnaillWidth && sourceBitmap.Height > config.ThumbnaillHeight)
{
@ -366,7 +372,7 @@ namespace ASC.Files.ThumbnailBuilder
if (sourceBitmap.Width > sourceBitmap.Height)
{
point.X = (sourceBitmap.Width - size.Width) / 2;
}
}
var targetThumbnailSettings = new UserPhotoThumbnailSettings(point, size);

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Üzvlərin əlavə edilməsi",
"Birthdate": "Doğum tarixi",
"Comments": "Şərhlər",
"DeleteSelfProfile": "Profilin silinməsi",
"DisabledEmployeeStatus": "Söndürülüb",
"DisableUserButton": "Bloklamaq",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Qadın",
"InviteLinkTitle": "Dəvət üçün link",
"LDAPLbl": "LDAP",
"Location": "Yer",
"MaleSexStatus": "Kişi",
"maxSizeFileError": "Faylın həcmi, maksimum izin verilən həcmdən böyükdür",
"MaxSizeLabel": "(JPG və ya PNG, ən çox 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "İstifadəçinin statusu müvəffəqiyyətlə dəyişdirilmişdir",
"SuccessDeletePersonalData": "Şəxsi məlumatlar müvəffəqiyyətlə silindi",
"SuccessSentInvitation": "Dəvətnamə müvəffəqiyyətlə göndərildi"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Добави членове",
"Birthdate": "Дата на раждане",
"Comments": "Коментари",
"DeleteSelfProfile": "Изтрий профил",
"DisabledEmployeeStatus": "Деактивиран",
"DisableUserButton": "Деактивирай",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Жена",
"InviteLinkTitle": "Връзка за покана",
"LDAPLbl": "LDAP",
"Location": "Местоположение",
"MaleSexStatus": "Мъж",
"maxSizeFileError": "Максималният размер на файла е надвишен",
"MaxSizeLabel": "(JPG или PNG, макс. 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Потребителският статус беше променен успешно",
"SuccessDeletePersonalData": "Личните данни бяха изтрити успешно",
"SuccessSentInvitation": "Поканата беше изпратена успешно"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Přidat členy",
"Birthdate": "Datum narození",
"Comments": "Komentáře",
"DeleteSelfProfile": "Smazat profil",
"DisabledEmployeeStatus": "Deaktivováno",
"DisableUserButton": "Deaktivovat",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Žena",
"InviteLinkTitle": "Odkaz s pozvánkou",
"LDAPLbl": "LDAP",
"Location": "Umístění",
"MaleSexStatus": "Muž",
"maxSizeFileError": "Maximální velikost souboru překročena",
"MaxSizeLabel": "(JPG nebo PNG, max. 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Stav uživatele byl úspěšně změněn",
"SuccessDeletePersonalData": "Osobní údaje byly úspěšně odstraněny",
"SuccessSentInvitation": "Pozvánka byla úspěšně odeslána"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Mitglieder hinzufügen",
"Birthdate": "Geburtsdatum",
"Comments": "Kommentare",
"DeleteSelfProfile": "Profil löschen",
"DisabledEmployeeStatus": "Deaktiviert",
"DisableUserButton": "Deaktivieren",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Weiblich",
"InviteLinkTitle": "Einladungslink",
"LDAPLbl": "LDAP",
"Location": "Standort",
"MaleSexStatus": "Männlich",
"maxSizeFileError": "Maximale Dateigröße überschritten",
"MaxSizeLabel": "(JPG oder PNG, max 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Der Benutzerstatus war erfolgreich geändert",
"SuccessDeletePersonalData": "Alle persönlichen Daten wurden erfolgreich gelöscht",
"SuccessSentInvitation": "Die Einladung wurde erfolgreich gesendet"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Προσθήκη μελών",
"Birthdate": "Ημερομηνία γέννησης",
"Comments": "Σχόλια",
"DeleteSelfProfile": "Διαγραφή προφίλ",
"DisabledEmployeeStatus": "Απενεργοποιημένο",
"DisableUserButton": "Απενεργοποίηση",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Θήλυ",
"InviteLinkTitle": "Σύνδεσμος πρόσκλησης",
"LDAPLbl": "LDAP",
"Location": "Τοποθεσία",
"MaleSexStatus": "Άρρεν",
"maxSizeFileError": "Υπέρβαση μέγιστου μεγέθους αρχείου",
"MaxSizeLabel": "(JPG ή PNG, μέγιστο 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Η κατάσταση του χρήστη άλλαξε επιτυχώς",
"SuccessDeletePersonalData": "Τα προσωπικά δεδομένα έχουν διαγραφεί επιτυχώς",
"SuccessSentInvitation": "Η πρόσκληση στάλθηκε με επιτυχία"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Add members",
"Birthdate": "Date of birth",
"Comments": "Comments",
"DeleteSelfProfile": "Delete profile",
"DisabledEmployeeStatus": "Disabled",
"DisableUserButton": "Disable",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Female",
"InviteLinkTitle": "Invitation link",
"LDAPLbl": "LDAP",
"Location": "Location",
"MaleSexStatus": "Male",
"maxSizeFileError": "Maximum file size exceeded",
"MaxSizeLabel": "(JPG or PNG, max 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "The user status was successfully changed",
"SuccessDeletePersonalData": "Personal data has been successfully deleted",
"SuccessSentInvitation": "The invitation was successfully sent"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Añadir miembros",
"Birthdate": "Fecha de nacimiento",
"Comments": "Comentarios",
"DeleteSelfProfile": "Borrar perfil",
"DisabledEmployeeStatus": "Deshabilitado",
"DisableUserButton": "Deshabilitar",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Mujer",
"InviteLinkTitle": "Enlace de invitación",
"LDAPLbl": "LDAP",
"Location": "Ubicación",
"MaleSexStatus": "Hombre",
"maxSizeFileError": "Se ha excedido el tamaño máximo del archivo",
"MaxSizeLabel": "(JPG o PNG, máx. 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "El estado de usuario se ha cambiado correctamente",
"SuccessDeletePersonalData": "Los datos personales se han eliminado correctamente",
"SuccessSentInvitation": "La invitación se ha enviado correctamente"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Lisää jäseniä",
"Birthdate": "Syntymäaika",
"Comments": "Kommentit",
"DeleteSelfProfile": "Poista profiili",
"DisabledEmployeeStatus": "Poistettu käytöstä",
"DisableUserButton": "Poisteta käytöstä",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Nainen",
"InviteLinkTitle": "Kutsulinkki",
"LDAPLbl": "LDAP",
"Location": "Sijainti",
"MaleSexStatus": "Mies",
"maxSizeFileError": "Tiedoston enimmäiskoko ylitetty",
"MaxSizeLabel": "(JPG tai PNG, max 1 Mt)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Käyttäjän tilan muuttaminen onnistui",
"SuccessDeletePersonalData": "Henkilötietojen poistaminen onnistui",
"SuccessSentInvitation": "Kutsu lähetettiin onnistuneesti"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Ajouter des membres",
"Birthdate": "Date de naissance",
"Comments": "Commentaires",
"DeleteSelfProfile": "Supprimer le profil",
"DisabledEmployeeStatus": "Désactivé",
"DisableUserButton": "Désactiver",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Femme",
"InviteLinkTitle": "Lien d'invitation",
"LDAPLbl": "LDAP",
"Location": "Emplacement",
"MaleSexStatus": "Homme",
"maxSizeFileError": "La taille maximale du fichier est dépassée",
"MaxSizeLabel": "(JPG ou PNG, max 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Le statut de l'utilisateur a été modifié avec succès",
"SuccessDeletePersonalData": "Les données personnelles ont été supprimées avec succès",
"SuccessSentInvitation": "L'invitation a été envoyée avec succès"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "Aggiungi membri",
"Birthdate": "Data di nascita",
"Comments": "Commenti",
"DeleteSelfProfile": "Eliminare il profilo",
"DisabledEmployeeStatus": "Disabilitato",
"DisableUserButton": "Disattivare",
@ -11,7 +10,6 @@
"FemaleSexStatus": "Femminile",
"InviteLinkTitle": "Link di invito",
"LDAPLbl": "LDAP",
"Location": "Posizione",
"MaleSexStatus": "Maschile",
"maxSizeFileError": "È superata massima dimensione del file",
"MaxSizeLabel": "(JPG o PNG, max 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "Lo stato dell'utente è stato cambiato con successo",
"SuccessDeletePersonalData": "I dati personali sono stati eliminati con successo",
"SuccessSentInvitation": "L'invito è stato inviato con successo"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "メンバーの追加",
"Birthdate": "誕生日",
"Comments": "コメント",
"DeleteSelfProfile": "プロファイルの削除",
"DisabledEmployeeStatus": "無効化",
"DisableUserButton": "無効にする",
@ -11,7 +10,6 @@
"FemaleSexStatus": "女性",
"InviteLinkTitle": "招待状リンク",
"LDAPLbl": "LDAP",
"Location": "ロケーション",
"MaleSexStatus": "男性",
"maxSizeFileError": "最大ファイルサイズを超えました",
"MaxSizeLabel": "(JPGまたはPNG、最大 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "ユーザーステータスの変更に成功しました",
"SuccessDeletePersonalData": "個人情報の削除が完了しました",
"SuccessSentInvitation": "招待状の送信に成功しました"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "멤버 추가",
"Birthdate": "생년월일",
"Comments": "코멘트",
"DeleteSelfProfile": "프로필 삭제",
"DisabledEmployeeStatus": "비활성화",
"DisableUserButton": "비활성화",
@ -11,7 +10,6 @@
"FemaleSexStatus": "여자",
"InviteLinkTitle": "초대 링크",
"LDAPLbl": "LDAP",
"Location": "위치",
"MaleSexStatus": "남자",
"maxSizeFileError": "최대 파일 크기를 초과했습니다",
"MaxSizeLabel": "(JPG 또는 PNG, 최대 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "사용자 상태가 성공적으로 변경되었습니다",
"SuccessDeletePersonalData": "개인 데이터가 성공적으로 삭제되었습니다",
"SuccessSentInvitation": "초대장이 성공적으로 전송되었습니다"
}
}

View File

@ -1,7 +1,6 @@
{
"AddMembers": "ເພີ່ມສະມາຊິກ",
"Birthdate": "ວັນເກີດ",
"Comments": "ຄໍາເຫັນ",
"DeleteSelfProfile": "ລຶບ profile",
"DisabledEmployeeStatus": "ປິດ",
"DisableUserButton": "ປິດ",
@ -11,7 +10,6 @@
"FemaleSexStatus": "ຜູ່ຍິງ",
"InviteLinkTitle": "link ຄໍາເຊີນ",
"LDAPLbl": "LDAP",
"Location": "ທີ່ຕັ້ງ",
"MaleSexStatus": "ຜູ່ຊາຍ",
"maxSizeFileError": "ເກີນຂະໜາດfileສູງສຸດ",
"MaxSizeLabel": "(JPG ຫຼື PNG ສູງສຸດ 1 MB)",
@ -28,4 +26,4 @@
"SuccessChangeUserStatus": "ຖານະຜູ້ໃຊ້ໄດ້ຖືກປ່ຽນແປງສໍາເລັດ",
"SuccessDeletePersonalData": "ຂໍ້ມູນສ່ວນຕົວໄດ້ຖືກລຶບອອກສໍາເລັດ",
"SuccessSentInvitation": "ການເຊື້ອເຊີນໄດ້ຖືກສົ່ງສໍາເລັດ"
}
}

Some files were not shown because too many files have changed in this diff Show More