diff --git a/build/install/common/publish-backend.sh b/build/install/common/publish-backend.sh index 3a0c3b87c4..93a54547d1 100644 --- a/build/install/common/publish-backend.sh +++ b/build/install/common/publish-backend.sh @@ -60,6 +60,8 @@ servers_products_name_backend=(ASC.CRM) servers_products_name_backend+=(ASC.Files) servers_products_name_backend+=(ASC.People) servers_products_name_backend+=(ASC.Projects) +servers_products_name_backend+=(ASC.Calendar) +servers_products_name_backend+=(ASC.Mail) # Publish server backend products for i in ${!servers_products_name_backend[@]}; do diff --git a/build/install/docker/.env b/build/install/docker/.env index 823635a2ee..ddf27d85e8 100644 --- a/build/install/docker/.env +++ b/build/install/docker/.env @@ -47,10 +47,12 @@ # service host # API_SYSTEM_HOST=${CONTAINER_PREFIX}api-system BACKUP_HOST=${CONTAINER_PREFIX}backup + CALENDAR_HOST=${CONTAINER_PREFIX}calendar CRM_HOST=${CONTAINER_PREFIX}crm STORAGE_ENCRYPTION_HOST=${CONTAINER_PREFIX}storage-encryption FILES_HOST=${CONTAINER_PREFIX}files FILES_SERVICES_HOST=${CONTAINER_PREFIX}files-services + MAIL_HOST=${CONTAINER_PREFIX}mail STORAGE_MIGRATION_HOST=${CONTAINER_PREFIX}storage-migration NOTIFY_HOST=${CONTAINER_PREFIX}notify PEOPLE_SERVER_HOST=${CONTAINER_PREFIX}people-server @@ -68,9 +70,11 @@ SERVICE_API_SYSTEM=${API_SYSTEM_HOST}:${SERVICE_PORT} SERVICE_BACKUP=${BACKUP_HOST}:${SERVICE_PORT} SERVICE_CRM=${CRM_HOST}:${SERVICE_PORT} + SERVICE_CALENDAR=${CALENDAR_HOST}:${SERVICE_PORT} SERVICE_STORAGE_ENCRYPTION=${STORAGE_ENCRYPTION_HOST}:${SERVICE_PORT} SERVICE_FILES=${FILES_HOST}:${SERVICE_PORT} SERVICE_FILES_SERVICES=${FILES_SERVICES_HOST}:${SERVICE_PORT} + SERVICE_MAIL=${MAIL_HOST}:${SERVICE_PORT} SERVICE_STORAGE_MIGRATION=${STORAGE_MIGRATION_HOST}:${SERVICE_PORT} SERVICE_NOTIFY=${NOTIFY_HOST}:${SERVICE_PORT} SERVICE_PEOPLE_SERVER=${PEOPLE_SERVER_HOST}:${SERVICE_PORT} diff --git a/build/install/docker/Dockerfile-app b/build/install/docker/Dockerfile-app index dd3d677de8..2ab5d1bdd3 100644 --- a/build/install/docker/Dockerfile-app +++ b/build/install/docker/Dockerfile-app @@ -45,11 +45,8 @@ RUN echo "nameserver 8.8.8.8" | tee /etc/resolv.conf > /dev/null && \ bash build-frontend.sh -sp ${SRC_PATH} && \ bash build-backend.sh -sp ${SRC_PATH} -ar "--disable-parallel" && \ bash publish-backend.sh -sp ${SRC_PATH} -bp ${BUILD_PATH} -ar "--disable-parallel" - -COPY config/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf -COPY config/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf -RUN sed -i 's/Server=.*;Port=/Server=127.0.0.1;Port=/' /app/onlyoffice/config/appsettings.test.json +COPY config/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/mysql.cnf RUN rm -rf /var/lib/apt/lists/* @@ -107,6 +104,8 @@ COPY --from=base ${SRC_PATH}/web/ASC.Web.Login/dist ${BUILD_PATH}/studio/login COPY --from=base ${SRC_PATH}/products/ASC.People/Client/dist ${BUILD_PATH}/products/ASC.People/client COPY --from=base ${SRC_PATH}/products/ASC.Projects/Client/dist ${BUILD_PATH}/products/ASC.Projects/client COPY --from=base ${SRC_PATH}/web/ASC.Web.Client/dist ${BUILD_PATH}/studio/client +COPY --from=base ${SRC_PATH}/products/ASC.Calendar/Client/dist ${BUILD_PATH}/products/ASC.Calendar/client +COPY --from=base ${SRC_PATH}/products/ASC.Mail/Client/dist ${BUILD_PATH}/products/ASC.Mail/client COPY /config/nginx/templates/upstream.conf.template /etc/nginx/templates/upstream.conf.template @@ -122,6 +121,8 @@ RUN chown nginx:nginx /etc/nginx/* -R && \ sed -i 's/localhost:5020/$service_projects_server/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:5000/$service_api/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:5003/$service_studio/' /etc/nginx/conf.d/onlyoffice.conf && \ + sed -i 's/localhost:5023/$service_calendar/' /etc/nginx/conf.d/onlyoffice.conf && \ + sed -i 's/localhost:5022/$service_mail/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/localhost:9999/$service_urlshortener/' /etc/nginx/conf.d/onlyoffice.conf && \ sed -i 's/172.*/$document_server;/' /etc/nginx/conf.d/onlyoffice.conf && \ # configute the image nginx whith less privileged https://hub.docker.com/_/nginx @@ -146,6 +147,15 @@ COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Data.B CMD ["ASC.Data.Backup.dll", "ASC.Data.Backup", "core:products:folder=/var/www/products/", "core:products:subfolder=server"] +## ASC.Calendar ## +FROM builder AS calendar +WORKDIR ${BUILD_PATH}/products/ASC.Calendar/server/ + +COPY --chown=onlyoffice:onlyoffice docker-entrypoint.sh . +COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/products/ASC.Calendar/server/ . + +CMD ["ASC.Calendar.dll", "ASC.Calendar"] + ## ASC.CRM ## FROM builder AS crm WORKDIR ${BUILD_PATH}/products/ASC.CRM/server/ @@ -182,6 +192,15 @@ COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/services/ASC.Files. CMD ["ASC.Files.Service.dll", "ASC.Files.Service", "core:products:folder=/var/www/products/", "core:products:subfolder=server"] +## ASC.Mail ## +FROM builder AS mail +WORKDIR ${BUILD_PATH}/products/ASC.Mail/server/ + +COPY --chown=onlyoffice:onlyoffice docker-entrypoint.sh . +COPY --from=base --chown=onlyoffice:onlyoffice ${BUILD_PATH}/products/ASC.Mail/server/ . + +CMD ["ASC.Mail.dll", "ASC.Mail"] + ## ASC.Data.Storage.Migration ## FROM builder AS data_storage_migration WORKDIR ${BUILD_PATH}/services/storage.migration/service/ diff --git a/build/install/docker/appserver.yml b/build/install/docker/appserver.yml index 3eef1db072..4912140010 100644 --- a/build/install/docker/appserver.yml +++ b/build/install/docker/appserver.yml @@ -29,6 +29,9 @@ x-service: - people_data:/var/www/products/ASC.People/server/ - crm_data:/var/www/products/ASC.CRM/server/ - project_data:/var/www/products/ASC.Projects/server/ + - calendar_data:/var/www/products/ASC.Calendar/server/ + - mail_data:/var/www/products/ASC.Mail/server/ + services: onlyoffice-elasticsearch: @@ -95,6 +98,11 @@ services: image: "${REPO}/${STATUS}appserver-backup:${SRV_VERSION}" container_name: ${BACKUP_HOST} + onlyoffice-calendar: + <<: *x-service-base + image: "${REPO}/${STATUS}appserver-calendar:${SRV_VERSION}" + container_name: ${CALENDAR_HOST} + onlyoffice-crm: <<: *x-service-base image: "${REPO}/${STATUS}appserver-crm:${SRV_VERSION}" @@ -115,6 +123,11 @@ services: image: "${REPO}/${STATUS}appserver-files-services:${SRV_VERSION}" container_name: ${FILES_SERVICES_HOST} + onlyoffice-mail: + <<: *x-service-base + image: "${REPO}/${STATUS}appserver-mail:${SRV_VERSION}" + container_name: ${MAIL_HOST} + onlyoffice-storage-migration: <<: *x-service-base image: "${REPO}/${STATUS}appserver-storage-migration:${SRV_VERSION}" @@ -187,10 +200,12 @@ services: depends_on: - onlyoffice-api-system - onlyoffice-backup + - onlyoffice-calendar - onlyoffice-crm - onlyoffice-storage-encryption - onlyoffice-files - onlyoffice-files-services + - onlyoffice-mail - onlyoffice-storage-migration - onlyoffice-people-server - onlyoffice-projects-server @@ -204,10 +219,12 @@ services: environment: - SERVICE_API_SYSTEM=${SERVICE_API_SYSTEM} - SERVICE_BACKUP=${SERVICE_BACKUP} + - SERVICE_CALENDAR=${SERVICE_CALENDAR} - SERVICE_CRM=${SERVICE_CRM} - SERVICE_STORAGE_ENCRYPTION=${SERVICE_STORAGE_ENCRYPTION} - SERVICE_FILES=${SERVICE_FILES} - SERVICE_FILES_SERVICES=${SERVICE_FILES_SERVICES} + - SERVICE_MAIL=${SERVICE_MAIL} - SERVICE_STORAGE_MIGRATION=${SERVICE_STORAGE_MIGRATION} - SERVICE_NOTIFY=${SERVICE_NOTIFY} - SERVICE_PEOPLE_SERVER=${SERVICE_PEOPLE_SERVER} @@ -240,3 +257,5 @@ volumes: people_data: crm_data: project_data: + calendar_data: + mail_data: diff --git a/build/install/docker/build.yml b/build/install/docker/build.yml index 1e6209d635..9a43b41e51 100644 --- a/build/install/docker/build.yml +++ b/build/install/docker/build.yml @@ -15,6 +15,13 @@ services: target: backup image: "${REPO}/${STATUS}appserver-backup:${SRV_VERSION}" + onlyoffice-calendar: + build: + context: ./ + dockerfile: "${DOCKERFILE}" + target: calendar + image: "${REPO}/${STATUS}appserver-calendar:${SRV_VERSION}" + onlyoffice-crm: build: context: ./ @@ -43,6 +50,13 @@ services: target: files_services image: "${REPO}/${STATUS}appserver-files-services:${SRV_VERSION}" + onlyoffice-mail: + build: + context: ./ + dockerfile: "${DOCKERFILE}" + target: mail + image: "${REPO}/${STATUS}appserver-mail:${SRV_VERSION}" + onlyoffice-storage-migration: build: context: ./ diff --git a/build/install/docker/config/nginx/templates/upstream.conf.template b/build/install/docker/config/nginx/templates/upstream.conf.template index 675432e66c..a14517cd5a 100644 --- a/build/install/docker/config/nginx/templates/upstream.conf.template +++ b/build/install/docker/config/nginx/templates/upstream.conf.template @@ -10,6 +10,11 @@ map $SERVICE_BACKUP $service_backup { $SERVICE_BACKUP $SERVICE_BACKUP; } +map $SERVICE_CALENDAR $service_calendar { + volatile; + $SERVICE_CALENDAR $SERVICE_CALENDAR; +} + map $SERVICE_CRM $service_crm { volatile; $SERVICE_CRM $SERVICE_CRM; @@ -30,6 +35,11 @@ map $SERVICE_FILES_SERVICES $service_files_services { $SERVICE_FILES_SERVICES $SERVICE_FILES_SERVICES; } +map $SERVICE_MAIL $service_mail { + volatile; + $SERVICE_MAIL $SERVICE_MAIL; +} + map $SERVICE_STORAGE_MIGRATION $service_storage_migration { volatile; $SERVICE_STORAGE_MIGRATION $SERVICE_STORAGE_MIGRATION; diff --git a/build/install/docker/notify.yml b/build/install/docker/notify.yml index 0d813e085a..7a437cf4d3 100644 --- a/build/install/docker/notify.yml +++ b/build/install/docker/notify.yml @@ -29,6 +29,8 @@ x-service: - people_data:/var/www/products/ASC.People/server/ - crm_data:/var/www/products/ASC.CRM/server/ - project_data:/var/www/products/ASC.Projects/server/ + - calendar_data:/var/www/products/ASC.Calendar/server/ + - mail_data:/var/www/products/ASC.Mail/server/ services: onlyoffice-notify: @@ -47,3 +49,5 @@ volumes: people_data: crm_data: project_data: + calendar_data: + mail_data: diff --git a/packages/asc-web-components/badge/index.js b/packages/asc-web-components/badge/index.js index e5bc04118b..4c9d0162fe 100644 --- a/packages/asc-web-components/badge/index.js +++ b/packages/asc-web-components/badge/index.js @@ -11,7 +11,6 @@ const Badge = (props) => { if (!props.onClick) return; e.preventDefault(); - e.stopPropagation(); props.onClick(e); }; diff --git a/packages/asc-web-components/context-menu-button/index.js b/packages/asc-web-components/context-menu-button/index.js index ed23be4ffc..5c4c585b89 100644 --- a/packages/asc-web-components/context-menu-button/index.js +++ b/packages/asc-web-components/context-menu-button/index.js @@ -80,8 +80,8 @@ class ContextMenuButton extends React.Component { } } - onIconButtonClick = () => { - if (this.props.isDisabled) { + onIconButtonClick = (e) => { + if (this.props.isDisabled || this.props.isNew) { this.stopAction; return; } @@ -95,7 +95,7 @@ class ContextMenuButton extends React.Component { !this.props.isDisabled && this.state.isOpen && this.props.onClick && - this.props.onClick() + this.props.onClick(e) ); // eslint-disable-line react/prop-types }; @@ -125,12 +125,17 @@ class ContextMenuButton extends React.Component { } callNewMenu = (e) => { - if (this.props.isDisabled) { + if (this.props.isDisabled || !this.props.isNew) { this.stopAction; return; } - this.props.isNew && this.props.onClick(e); + this.setState( + { + data: this.props.getData(), + }, + () => this.props.onClick(e) + ); }; render() { diff --git a/products/ASC.Files/Client/src/Files.jsx b/products/ASC.Files/Client/src/Files.jsx index 06376c70a6..82b3c9db14 100644 --- a/products/ASC.Files/Client/src/Files.jsx +++ b/products/ASC.Files/Client/src/Files.jsx @@ -12,9 +12,9 @@ import "./custom.scss"; import i18n from "./i18n"; import { I18nextProvider } from "react-i18next"; import { regDesktop } from "@appserver/common/desktop"; -import Home from "./components/pages/Home"; -import Settings from "./components/pages/Settings"; -import VersionHistory from "./components/pages/VersionHistory"; +import Home from "./pages/Home"; +import Settings from "./pages/Settings"; +import VersionHistory from "./pages/VersionHistory"; import ErrorBoundary from "@appserver/common/components/ErrorBoundary"; import Panels from "./components/FilesPanels"; import { AppServerConfig } from "@appserver/common/constants"; diff --git a/products/ASC.Files/Client/src/HOCs/withBadges.js b/products/ASC.Files/Client/src/HOCs/withBadges.js new file mode 100644 index 0000000000..badba784b3 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withBadges.js @@ -0,0 +1,269 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; + +import { + ShareAccessRights, + AppServerConfig, +} from "@appserver/common/constants"; +import toastr from "studio/toastr"; +import { combineUrl } from "@appserver/common/utils"; +import { + convertFile, + getFileConversationProgress, +} from "@appserver/common/api/files"; + +import Badges from "../components/Badges"; +import config from "../../package.json"; + +export default function withBadges(WrappedComponent) { + class WithBadges extends React.Component { + constructor(props) { + super(props); + this.state = { showConvertDialog: false }; + } + onClickLock = () => { + const { item, lockFileAction } = this.props; + const { locked, id } = item; + + lockFileAction(id, !locked).catch((err) => toastr.error(err)); + }; + + onClickFavorite = () => { + const { t, item, setFavoriteAction } = this.props; + + setFavoriteAction("remove", item.id) + .then(() => toastr.success(t("RemovedFromFavorites"))) + .catch((err) => toastr.error(err)); + }; + + onShowVersionHistory = () => { + const { + homepage, + isTabletView, + item, + setIsVerHistoryPanel, + fetchFileVersions, + history, + isTrashFolder, + } = this.props; + if (isTrashFolder) return; + + if (!isTabletView) { + fetchFileVersions(item.id + ""); + setIsVerHistoryPanel(true); + } else { + history.push( + combineUrl(AppServerConfig.proxyURL, homepage, `/${item.id}/history`) + ); + } + }; + onBadgeClick = () => { + const { + item, + selectedFolderPathParts, + markAsRead, + setNewFilesPanelVisible, + setNewFilesIds, + updateRootBadge, + updateFileBadge, + } = this.props; + if (item.fileExst) { + markAsRead([], [item.id]) + .then(() => { + updateRootBadge(selectedFolderPathParts[0], 1); + updateFileBadge(item.id); + }) + .catch((err) => toastr.error(err)); + } else { + setNewFilesPanelVisible(true); + const newFolderIds = selectedFolderPathParts; + newFolderIds.push(item.id); + setNewFilesIds(newFolderIds); + } + }; + + setConvertDialogVisible = () => + this.setState({ showConvertDialog: !this.state.showConvertDialog }); + + onConvert = () => { + const { item, t, setSecondaryProgressBarData } = this.props; + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: 0, + label: t("Convert"), + alert: false, + }); + this.setState({ showConvertDialog: false }, () => + convertFile(item.id).then((convertRes) => { + if (convertRes && convertRes[0] && convertRes[0].progress !== 100) { + this.getConvertProgress(item.id); + } + }) + ); + }; + + getConvertProgress = (fileId) => { + const { + selectedFolderId, + filter, + setIsLoading, + setSecondaryProgressBarData, + t, + clearSecondaryProgressData, + fetchFiles, + } = this.props; + getFileConversationProgress(fileId).then((res) => { + if (res && res[0] && res[0].progress !== 100) { + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: res[0].progress, + label: t("Convert"), + alert: false, + }); + setTimeout(() => this.getConvertProgress(fileId), 1000); + } else { + if (res[0].error) { + setSecondaryProgressBarData({ + visible: true, + alert: true, + }); + toastr.error(res[0].error); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + } else { + setSecondaryProgressBarData({ + icon: "file", + visible: true, + percent: 100, + label: t("Convert"), + alert: false, + }); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + const newFilter = filter.clone(); + fetchFiles(selectedFolderId, newFilter) + .catch((err) => { + setSecondaryProgressBarData({ + visible: true, + alert: true, + }); + //toastr.error(err); + setTimeout(() => clearSecondaryProgressData(), TIMEOUT); + }) + .finally(() => setIsLoading(false)); + } + } + }); + }; + render() { + const { showConvertDialog } = this.state; + const { + item, + canWebEdit, + isTrashFolder, + canConvert, + onFilesClick, // from withFileAction HOC + } = this.props; + const { fileStatus, access } = item; + + const newItems = item.new || fileStatus === 2; + const showNew = !!newItems; + + const accessToEdit = + access === ShareAccessRights.FullAccess || + access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) + + const badgesComponent = ( + + ); + + return ( + <> + {showConvertDialog && ( + + )} + + + ); + } + } + + return inject( + ( + { + auth, + formatsStore, + treeFoldersStore, + filesActionsStore, + versionHistoryStore, + selectedFolderStore, + dialogsStore, + filesStore, + uploadDataStore, + }, + { item } + ) => { + const { docserviceStore } = formatsStore; + const { isRecycleBinFolder, updateRootBadge } = treeFoldersStore; + const { + lockFileAction, + setFavoriteAction, + markAsRead, + } = filesActionsStore; + const { isTabletView } = auth.settingsStore; + const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; + const { setNewFilesPanelVisible, setNewFilesIds } = dialogsStore; + const { updateFileBadge, filter, setIsLoading, fetchFiles } = filesStore; + const { secondaryProgressDataStore } = uploadDataStore; + const { + setSecondaryProgressBarData, + clearSecondaryProgressData, + } = secondaryProgressDataStore; + + const canWebEdit = docserviceStore.canWebEdit(item.fileExst); + const canConvert = docserviceStore.canConvert(item.fileExst); + + return { + canWebEdit, + canConvert, + isTrashFolder: isRecycleBinFolder, + lockFileAction, + setFavoriteAction, + homepage: config.homepage, + isTabletView, + setIsVerHistoryPanel, + fetchFileVersions, + selectedFolderPathParts: selectedFolderStore.pathParts, + markAsRead, + setNewFilesPanelVisible, + setNewFilesIds, + updateRootBadge, + updateFileBadge, + setSecondaryProgressBarData, + selectedFolderId: selectedFolderStore.id, + filter, + setIsLoading, + clearSecondaryProgressData, + fetchFiles, + }; + } + )(observer(WithBadges)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withContent.js b/products/ASC.Files/Client/src/HOCs/withContent.js new file mode 100644 index 0000000000..f453b200a0 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withContent.js @@ -0,0 +1,364 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { Trans } from "react-i18next"; +import { isMobile } from "react-device-detect"; + +import toastr from "studio/toastr"; +import { + AppServerConfig, + FileAction, + ShareAccessRights, +} from "@appserver/common/constants"; +import { combineUrl } from "@appserver/common/utils"; + +import config from "../../package.json"; +import EditingWrapperComponent from "../components/EditingWrapperComponent"; +import { getTitleWithoutExst } from "../helpers/files-helpers"; + +export default function withContent(WrappedContent) { + class WithContent extends React.Component { + constructor(props) { + super(props); + let titleWithoutExt = getTitleWithoutExst(props.item); + + if (props.fileActionId === -1) { + titleWithoutExt = this.getDefaultName(props.fileActionExt); + } + + this.state = { + itemTitle: titleWithoutExt, + + //loading: false + }; + } + + componentDidUpdate(prevProps) { + const { fileActionId, fileActionExt } = this.props; + if (fileActionId === -1 && fileActionExt !== prevProps.fileActionExt) { + const itemTitle = this.getDefaultName(fileActionExt); + this.setState({ itemTitle }); + } + // if (fileAction) { + // if (fileActionId !== prevProps.fileActionId) { + // this.setState({ editingId: fileActionId }); + // } + // } + } + + getDefaultName = (format) => { + const { t } = this.props; + + switch (format) { + case "docx": + return t("NewDocument"); + case "xlsx": + return t("NewSpreadsheet"); + case "pptx": + return t("NewPresentation"); + default: + return t("NewFolder"); + } + }; + + completeAction = (id) => { + const { editCompleteAction, item } = this.props; + + const isCancel = + (id.currentTarget && id.currentTarget.dataset.action === "cancel") || + id.keyCode === 27; + editCompleteAction(id, item, isCancel); + }; + + updateItem = () => { + const { + t, + updateFile, + renameFolder, + item, + setIsLoading, + fileActionId, + editCompleteAction, + } = this.props; + + const { itemTitle } = this.state; + const originalTitle = getTitleWithoutExst(item); + + setIsLoading(true); + const isSameTitle = + originalTitle.trim() === itemTitle.trim() || itemTitle.trim() === ""; + if (isSameTitle) { + this.setState({ + itemTitle: originalTitle, + }); + return editCompleteAction(fileActionId, item, isSameTitle); + } + + item.fileExst || item.contentLength + ? updateFile(fileActionId, itemTitle) + .then(() => this.completeAction(fileActionId)) + .then(() => + toastr.success( + t("FileRenamed", { + oldTitle: item.title, + newTitle: itemTitle + item.fileExst, + }) + ) + ) + .catch((err) => toastr.error(err)) + .finally(() => setIsLoading(false)) + : renameFolder(fileActionId, itemTitle) + .then(() => this.completeAction(fileActionId)) + .then(() => + toastr.success( + t("FolderRenamed", { + folderTitle: item.title, + newFoldedTitle: itemTitle, + }) + ) + ) + .catch((err) => toastr.error(err)) + .finally(() => setIsLoading(false)); + }; + + cancelUpdateItem = (e) => { + const { item } = this.props; + + const originalTitle = getTitleWithoutExst(item); + this.setState({ + itemTitle: originalTitle, + }); + + return this.completeAction(e); + }; + + onClickUpdateItem = (e) => { + const { fileActionType } = this.props; + + fileActionType === FileAction.Create + ? this.createItem(e) + : this.updateItem(e); + }; + + createItem = (e) => { + const { + createFile, + item, + setIsLoading, + openDocEditor, + isPrivacy, + isDesktop, + replaceFileStream, + t, + setEncryptionAccess, + createFolder, + } = this.props; + const { itemTitle } = this.state; + + setIsLoading(true); + + const itemId = e.currentTarget.dataset.itemid; + + if (itemTitle.trim() === "") { + toastr.warning(t("CreateWithEmptyTitle")); + return this.completeAction(itemId); + } + + let tab = + !isDesktop && item.fileExst + ? window.open( + combineUrl( + AppServerConfig.proxyURL, + config.homepage, + "/products/files/doceditor" + ), + "_blank" + ) + : null; + + !item.fileExst && !item.contentLength + ? createFolder(item.parentId, itemTitle) + .then(() => this.completeAction(itemId)) + .then(() => + toastr.success( + + New folder {{ itemTitle }} is created + + ) + ) + .catch((e) => toastr.error(e)) + .finally(() => { + return setIsLoading(false); + }) + : createFile(item.parentId, `${itemTitle}.${item.fileExst}`) + .then((file) => { + if (isPrivacy) { + return setEncryptionAccess(file).then((encryptedFile) => { + if (!encryptedFile) return Promise.resolve(); + toastr.info(t("EncryptedFileSaving")); + return replaceFileStream( + file.id, + encryptedFile, + true, + false + ).then(() => + openDocEditor(file.id, file.providerKey, tab, file.webUrl) + ); + }); + } + return openDocEditor(file.id, file.providerKey, tab, file.webUrl); + }) + .then(() => this.completeAction(itemId)) + .then(() => { + const exst = item.fileExst; + return toastr.success( + + New file {{ itemTitle }}.{{ exst }} is created + + ); + }) + .catch((e) => toastr.error(e)) + .finally(() => { + return setIsLoading(false); + }); + }; + + renameTitle = (e) => { + const { t } = this.props; + + let title = e.target.value; + //const chars = '*+:"<>?|/'; TODO: think how to solve problem with interpolation escape values in i18n translate + const regexp = new RegExp('[*+:"<>?|\\\\/]', "gim"); + if (title.match(regexp)) { + toastr.warning(t("ContainsSpecCharacter")); + } + title = title.replace(regexp, "_"); + return this.setState({ itemTitle: title }); + }; + + getStatusByDate = () => { + const { culture, t, item, sectionWidth } = this.props; + const { created, updated, version, fileExst } = item; + + const title = + version > 1 + ? t("TitleModified") + : fileExst + ? t("TitleUploaded") + : t("TitleCreated"); + + const date = fileExst ? updated : created; + const dateLabel = new Date(date).toLocaleString(culture); + const mobile = (sectionWidth && sectionWidth <= 375) || isMobile; + + return mobile ? dateLabel : `${title}: ${dateLabel}`; + }; + + render() { + const { itemTitle } = this.state; + const { + item, + fileActionId, + fileActionExt, + isLoading, + viewer, + t, + isTrashFolder, + onFilesClick, + } = this.props; + const { id, fileExst, updated, createdBy, access, fileStatus } = item; + + const titleWithoutExt = getTitleWithoutExst(item); + + const isEdit = id === fileActionId && fileExst === fileActionExt; + + const updatedDate = updated && this.getStatusByDate(); + + const fileOwner = + createdBy && + ((viewer.id === createdBy.id && t("AuthorMe")) || + createdBy.displayName); + + const accessToEdit = + access === ShareAccessRights.FullAccess || // only badges? + access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) + + const linkStyles = isTrashFolder //|| window.innerWidth <= 1024 + ? { noHover: true } + : { onClick: onFilesClick }; + + const newItems = item.new || fileStatus === 2; + const showNew = !!newItems; + + return isEdit ? ( + + ) : ( + + ); + } + } + + return inject( + ({ filesActionsStore, filesStore, treeFoldersStore, auth }, {}) => { + const { editCompleteAction } = filesActionsStore; + const { + setIsLoading, + openDocEditor, + updateFile, + renameFolder, + createFile, + createFolder, + isLoading, + } = filesStore; + const { isRecycleBinFolder, isPrivacyFolder } = treeFoldersStore; + + const { + type: fileActionType, + extension: fileActionExt, + id: fileActionId, + } = filesStore.fileActionStore; + const { replaceFileStream, setEncryptionAccess } = auth; + const { culture, isDesktopClient } = auth.settingsStore; + + return { + editCompleteAction, + setIsLoading, + isTrashFolder: isRecycleBinFolder, + openDocEditor, + updateFile, + renameFolder, + fileActionId, + editCompleteAction, + fileActionType, + createFile, + isPrivacy: isPrivacyFolder, + isDesktop: isDesktopClient, + replaceFileStream, + setEncryptionAccess, + createFolder, + fileActionExt, + isLoading, + culture, + homepage: config.homepage, + viewer: auth.userStore.user, + }; + } + )(observer(WithContent)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withContextOptions.js b/products/ASC.Files/Client/src/HOCs/withContextOptions.js new file mode 100644 index 0000000000..8ec66f6c33 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withContextOptions.js @@ -0,0 +1,503 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import copy from "copy-to-clipboard"; + +import { combineUrl } from "@appserver/common/utils"; +import { FileAction, AppServerConfig } from "@appserver/common/constants"; +import toastr from "studio/toastr"; + +import config from "../../package.json"; + +export default function withContextOptions(WrappedComponent) { + class WithContextOptions extends React.Component { + onOpenLocation = () => { + const { item, openLocationAction } = this.props; + const { id, folderId, fileExst } = item; + + const locationId = !fileExst ? id : folderId; + openLocationAction(locationId, !fileExst); + }; + + onOwnerChange = () => { + const { setChangeOwnerPanelVisible } = this.props; + setChangeOwnerPanelVisible(true); + }; + onMoveAction = () => { + const { setMoveToPanelVisible } = this.props; + setMoveToPanelVisible(true); + }; + onCopyAction = () => { + const { setCopyPanelVisible } = this.props; + setCopyPanelVisible(true); + }; + + showVersionHistory = () => { + const { + item, + isTabletView, + fetchFileVersions, + setIsVerHistoryPanel, + history, + homepage, + isTrashFolder, + } = this.props; + const { id } = item; + if (isTrashFolder) return; + + if (!isTabletView) { + fetchFileVersions(id + ""); + setIsVerHistoryPanel(true); + } else { + history.push( + combineUrl(AppServerConfig.proxyURL, homepage, `/${id}/history`) + ); + } + }; + + finalizeVersion = () => { + const { item, finalizeVersionAction } = this.props; + const { id } = item; + finalizeVersionAction(id).catch((err) => toastr.error(err)); + }; + + onClickFavorite = (e) => { + const { item, setFavoriteAction, t } = this.props; + const { id } = item; + const data = (e.currentTarget && e.currentTarget.dataset) || e; + const { action } = data; + + setFavoriteAction(action, id) + .then(() => + action === "mark" + ? toastr.success(t("MarkedAsFavorite")) + : toastr.success(t("RemovedFromFavorites")) + ) + .catch((err) => toastr.error(err)); + }; + + lockFile = () => { + const { item, lockFileAction } = this.props; + const { id, locked } = item; + lockFileAction(id, !locked).catch((err) => toastr.error(err)); + }; + + onClickLinkForPortal = () => { + const { item, homepage, t } = this.props; + const { fileExst, canOpenPlayer, webUrl } = item; + + const isFile = !!fileExst; + copy( + isFile + ? canOpenPlayer + ? `${window.location.href}&preview=${id}` + : webUrl + : `${window.location.origin + homepage}/filter?folder=${id}` + ); + + toastr.success(t("LinkCopySuccess")); + }; + + onClickLinkEdit = () => { + const { item, openDocEditor } = this.props; + const { id, providerKey } = item; + openDocEditor(id, providerKey); + }; + + onClickDownload = () => { + const { item, downloadAction, t } = this.props; + const { fileExst, contentLength, viewUrl } = item; + const isFile = !!fileExst && contentLength; + isFile + ? window.open(viewUrl, "_blank") + : downloadAction(t("ArchivingData")).catch((err) => toastr.error(err)); + }; + + onClickDownloadAs = () => { + const { setDownloadDialogVisible } = this.props; + setDownloadDialogVisible(true); + }; + + onDuplicate = () => { + const { duplicateAction, t, item } = this.props; + duplicateAction(item, t("CopyOperation")).catch((err) => + toastr.error(err) + ); + }; + + onClickRename = () => { + const { item, setAction } = this.props; + const { id, fileExst } = item; + setAction({ + type: FileAction.Rename, + extension: fileExst, + id, + }); + }; + + onChangeThirdPartyInfo = () => { + const { item, setThirdpartyInfo } = this.props; + const { providerKey } = item; + setThirdpartyInfo(providerKey); + }; + + onMediaFileClick = (fileId) => { + const { item, setMediaViewerData } = this.props; + const itemId = typeof fileId !== "object" ? fileId : item.id; + setMediaViewerData({ visible: true, id: itemId }); + }; + + onClickDelete = () => { + const { + item, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + confirmDelete, + setDeleteDialogVisible, + t, + deleteFileAction, + deleteFolderAction, + isThirdPartyFolder, + } = this.props; + const { id, title, fileExst, contentLength, folderId, parentId } = item; + + if (isThirdPartyFolder) { + const splitItem = id.split("-"); + setRemoveItem({ id: splitItem[splitItem.length - 1], title }); + setDeleteThirdPartyDialogVisible(true); + return; + } + + if (confirmDelete) { + setDeleteDialogVisible(true); + } else { + const translations = { + deleteOperation: t("DeleteOperation"), + }; + + fileExst || contentLength + ? deleteFileAction(id, folderId, translations) + .then(() => toastr.success(t("FileRemoved"))) + .catch((err) => toastr.error(err)) + : deleteFolderAction(id, parentId, translations) + .then(() => toastr.success(t("FolderRemoved"))) + .catch((err) => toastr.error(err)); + } + }; + + onClickShare = () => { + const { onSelectItem, setSharingPanelVisible, item } = this.props; + onSelectItem(item); + setSharingPanelVisible(true); + }; + + getFilesContextOptions = () => { + const { item, t, isThirdPartyFolder } = this.props; + const { access, contextOptions } = item; + const isSharable = access !== 1 && access !== 0; + return contextOptions.map((option) => { + switch (option) { + case "open": + return { + key: option, + label: t("Open"), + icon: "images/catalog.folder.react.svg", + onClick: this.onOpenLocation, + disabled: false, + }; + case "show-version-history": + return { + key: option, + label: t("ShowVersionHistory"), + icon: "images/history.react.svg", + onClick: this.showVersionHistory, + disabled: false, + }; + case "finalize-version": + return { + key: option, + label: t("FinalizeVersion"), + icon: "images/history-finalized.react.svg", + onClick: this.finalizeVersion, + disabled: false, + }; + case "separator0": + case "separator1": + case "separator2": + case "separator3": + return { key: option, isSeparator: true }; + case "open-location": + return { + key: option, + label: t("OpenLocation"), + icon: "images/download-as.react.svg", + onClick: this.onOpenLocation, + disabled: false, + }; + case "mark-as-favorite": + return { + key: option, + label: t("MarkAsFavorite"), + icon: "images/favorites.react.svg", + onClick: this.onClickFavorite, + disabled: false, + "data-action": "mark", + action: "mark", + }; + case "block-unblock-version": + return { + key: option, + label: t("UnblockVersion"), + icon: "images/lock.react.svg", + onClick: this.lockFile, + disabled: false, + }; + case "sharing-settings": + return { + key: option, + label: t("SharingSettings"), + icon: "images/catalog.shared.react.svg", + onClick: this.onClickShare, + disabled: isSharable, + }; + case "send-by-email": + return { + key: option, + label: t("SendByEmail"), + icon: "/static/images/mail.react.svg", + disabled: true, + }; + case "owner-change": + return { + key: option, + label: t("ChangeOwner"), + icon: "images/catalog.user.react.svg", + onClick: this.onOwnerChange, + disabled: false, + }; + case "link-for-portal-users": + return { + key: option, + label: t("LinkForPortalUsers"), + icon: "/static/images/invitation.link.react.svg", + onClick: this.onClickLinkForPortal, + disabled: false, + }; + case "edit": + return { + key: option, + label: t("Edit"), + icon: "/static/images/access.edit.react.svg", + onClick: this.onClickLinkEdit, + disabled: false, + }; + case "preview": + return { + key: option, + label: t("Preview"), + icon: "EyeIcon", + onClick: this.onClickLinkEdit, + disabled: true, + }; + case "view": + return { + key: option, + label: t("View"), + icon: "/static/images/eye.react.svg", + onClick: this.onMediaFileClick, + disabled: false, + }; + case "download": + return { + key: option, + label: t("Download"), + icon: "images/download.react.svg", + onClick: this.onClickDownload, + disabled: false, + }; + case "download-as": + return { + key: option, + label: t("DownloadAs"), + icon: "images/download-as.react.svg", + onClick: this.onClickDownloadAs, + disabled: false, + }; + case "move-to": + return { + key: option, + label: t("MoveTo"), + icon: "images/move.react.svg", + onClick: this.onMoveAction, + disabled: false, + }; + case "restore": + return { + key: option, + label: t("Restore"), + icon: "images/move.react.svg", + onClick: this.onMoveAction, + disabled: false, + }; + case "copy-to": + return { + key: option, + label: t("Copy"), + icon: "/static/images/copy.react.svg", + onClick: this.onCopyAction, + disabled: false, + }; + case "copy": + return { + key: option, + label: t("Duplicate"), + icon: "/static/images/copy.react.svg", + onClick: this.onDuplicate, + disabled: false, + }; + case "rename": + return { + key: option, + label: t("Rename"), + icon: "images/rename.react.svg", + onClick: this.onClickRename, + disabled: false, + }; + case "change-thirdparty-info": + return { + key: option, + label: t("ThirdPartyInfo"), + icon: "/static/images/access.edit.react.svg", + onClick: this.onChangeThirdPartyInfo, + disabled: false, + }; + case "delete": + return { + key: option, + label: isThirdPartyFolder ? t("DeleteThirdParty") : t("Delete"), + icon: "/static/images/catalog.trash.react.svg", + onClick: this.onClickDelete, + disabled: false, + }; + case "remove-from-favorites": + return { + key: option, + label: t("RemoveFromFavorites"), + icon: "images/favorites.react.svg", + onClick: this.onClickFavorite, + disabled: false, + "data-action": "remove", + action: "remove", + }; + default: + break; + } + + return undefined; + }); + }; + render() { + const { actionType, actionId, actionExtension, item } = this.props; + const { id, fileExst, contextOptions } = item; + + const isEdit = + !!actionType && actionId === id && fileExst === actionExtension; + + const contextOptionsProps = + !isEdit && contextOptions && contextOptions.length > 0 + ? { + contextOptions: this.getFilesContextOptions(), + } + : {}; + + return ( + + ); + } + } + + return inject( + ( + { + filesStore, + filesActionsStore, + auth, + versionHistoryStore, + mediaViewerDataStore, + settingsStore, + selectedFolderStore, + dialogsStore, + treeFoldersStore, + }, + { item } + ) => { + const { openDocEditor, fileActionStore } = filesStore; + const { + openLocationAction, + finalizeVersionAction, + setFavoriteAction, + lockFileAction, + downloadAction, + duplicateAction, + setThirdpartyInfo, + deleteFileAction, + deleteFolderAction, + onSelectItem, + } = filesActionsStore; + const { + setChangeOwnerPanelVisible, + setMoveToPanelVisible, + setCopyPanelVisible, + setDownloadDialogVisible, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + setDeleteDialogVisible, + setSharingPanelVisible, + } = dialogsStore; + const { isTabletView } = auth.settingsStore; + const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; + const { setAction, type, extension, id } = fileActionStore; + const { setMediaViewerData } = mediaViewerDataStore; + const { isRootFolder } = selectedFolderStore; + const { isRecycleBinFolder } = treeFoldersStore; + + const isThirdPartyFolder = item.providerKey && isRootFolder; + + return { + openLocationAction, + setChangeOwnerPanelVisible, + setMoveToPanelVisible, + setCopyPanelVisible, + isTabletView, + setIsVerHistoryPanel, + fetchFileVersions, + homepage: config.homepage, + finalizeVersionAction, + setFavoriteAction, + lockFileAction, + openDocEditor, + downloadAction, + setDownloadDialogVisible, + duplicateAction, + setAction, + setThirdpartyInfo, + setMediaViewerData, + setRemoveItem, + setDeleteThirdPartyDialogVisible, + confirmDelete: settingsStore.confirmDelete, + setDeleteDialogVisible, + deleteFileAction, + deleteFolderAction, + isThirdPartyFolder, + onSelectItem, + setSharingPanelVisible, + actionType: type, + actionId: id, + actionExtension: extension, + isTrashFolder: isRecycleBinFolder, + }; + } + )(observer(WithContextOptions)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withFileActions.js b/products/ASC.Files/Client/src/HOCs/withFileActions.js new file mode 100644 index 0000000000..08f5d8f870 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withFileActions.js @@ -0,0 +1,339 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { ReactSVG } from "react-svg"; + +import IconButton from "@appserver/components/icon-button"; +import Text from "@appserver/components/text"; + +import { EncryptedFileIcon } from "../components/Icons"; + +const svgLoader = () =>
; +export default function withFileActions(WrappedFileItem) { + class WithFileActions extends React.Component { + onContentRowSelect = (checked, file) => { + const { selectRowAction } = this.props; + if (!file) return; + selectRowAction(checked, file); + }; + + onClickShare = () => { + const { onSelectItem, setSharingPanelVisible, item } = this.props; + onSelectItem(item); + setSharingPanelVisible(true); + }; + + rowContextClick = () => { + const { onSelectItem, item } = this.props; + onSelectItem(item); + }; + + getSharedButton = (shared) => { + const { t } = this.props; + const color = shared ? "#657077" : "#a3a9ae"; + return ( + + + {t("Share")} + + ); + }; + + getItemIcon = (isEdit) => { + const { item, isPrivacy } = this.props; + const { icon, fileExst } = item; + return ( + <> + + {isPrivacy && fileExst && } + + ); + }; + + onDropZoneUpload = (files, uploadToFolder) => { + const { + selectedFolderId, + dragging, + setDragging, + startUpload, + } = this.props; + + const folderId = uploadToFolder ? uploadToFolder : selectedFolderId; + dragging && setDragging(false); + startUpload(files, folderId, t); + }; + + onDrop = (items) => { + const { item, selectedFolderId } = this.props; + const { fileExst, id } = item; + + if (!fileExst) { + this.onDropZoneUpload(items, id); + } else { + this.onDropZoneUpload(items, selectedFolderId); + } + }; + + onMouseDown = (e) => { + const { draggable, setTooltipPosition, setStartDrag } = this.props; + if (!draggable) { + return; + } + + if ( + window.innerWidth < 1025 || + e.target.tagName === "rect" || + e.target.tagName === "path" + ) { + return; + } + const mouseButton = e.which + ? e.which !== 1 + : e.button + ? e.button !== 0 + : false; + const label = e.currentTarget.getAttribute("label"); + if (mouseButton || e.currentTarget.tagName !== "DIV" || label) { + return; + } + + setTooltipPosition(e.pageX, e.pageY); + setStartDrag(true); + }; + + onFilesClick = () => { + const { + filter, + parentFolder, + setIsLoading, + fetchFiles, + isImage, + isSound, + isVideo, + canWebEdit, + item, + isTrashFolder, + openDocEditor, + expandedKeys, + addExpandedKeys, + setMediaViewerData, + } = this.props; + const { id, fileExst, viewUrl, providerKey, contentLength } = item; + + if (isTrashFolder) return; + + if (!fileExst && !contentLength) { + setIsLoading(true); + + if (!expandedKeys.includes(parentFolder + "")) { + addExpandedKeys(parentFolder + ""); + } + + fetchFiles(id, filter) + .catch((err) => { + toastr.error(err); + setIsLoading(false); + }) + .finally(() => setIsLoading(false)); + } else { + if (canWebEdit) { + return openDocEditor(id, providerKey); + } + + if (isImage || isSound || isVideo) { + setMediaViewerData({ visible: true, id }); + return; + } + + return window.open(viewUrl, "_blank"); + } + }; + + render() { + const { + item, + isRecycleBin, + draggable, + canShare, + isPrivacy, + actionType, + actionExtension, + actionId, + sectionWidth, + checked, + dragging, + isFolder, + } = this.props; + const { fileExst, access, contentLength, id, shared } = item; + + const isEdit = + !!actionType && actionId === id && fileExst === actionExtension; + + const isDragging = isFolder && access < 2 && !isRecycleBin; + + let className = isDragging ? " droppable" : ""; + if (draggable) className += " draggable not-selectable"; + + let value = fileExst || contentLength ? `file_${id}` : `folder_${id}`; + value += draggable ? "_draggable" : ""; + + const isMobile = sectionWidth < 500; + const displayShareButton = isMobile + ? "26px" + : !canShare + ? "38px" + : "96px"; + + const sharedButton = + !canShare || (isPrivacy && !fileExst) || isEdit || id <= 0 || isMobile + ? null + : this.getSharedButton(shared); + + const checkedProps = isEdit || id <= 0 ? {} : { checked }; + const element = this.getItemIcon(isEdit || id <= 0); + + return ( + + ); + } + } + + return inject( + ( + { + filesActionsStore, + dialogsStore, + treeFoldersStore, + selectedFolderStore, + filesStore, + uploadDataStore, + formatsStore, + mediaViewerDataStore, + }, + { item, t, history } + ) => { + const { selectRowAction, onSelectItem } = filesActionsStore; + const { setSharingPanelVisible } = dialogsStore; + const { + isPrivacyFolder, + isRecycleBinFolder, + expandedKeys, + addExpandedKeys, + } = treeFoldersStore; + const { id: selectedFolderId, isRootFolder } = selectedFolderStore; + const { + dragging, + setDragging, + selection, + setTooltipPosition, + setStartDrag, + fileActionStore, + canShare, + isFileSelected, + filter, + setIsLoading, + fetchFiles, + openDocEditor, + } = filesStore; + const { startUpload } = uploadDataStore; + const { type, extension, id } = fileActionStore; + const { + iconFormatsStore, + mediaViewersFormatsStore, + docserviceStore, + } = formatsStore; + const { setMediaViewerData } = mediaViewerDataStore; + + const selectedItem = selection.find( + (x) => x.id === item.id && x.fileExst === item.fileExst + ); + + const draggable = + !isRecycleBinFolder && selectedItem && selectedItem.id !== id; + + const isFolder = selectedItem + ? false + : item.fileExst //|| item.contentLength + ? false + : true; + + const isImage = iconFormatsStore.isImage(item.fileExst); + const isSound = iconFormatsStore.isSound(item.fileExst); + const isVideo = mediaViewersFormatsStore.isVideo(item.fileExst); + const canWebEdit = docserviceStore.canWebEdit(item.fileExst); + + return { + t, + item, + selectRowAction, + onSelectItem, + setSharingPanelVisible, + isPrivacy: isPrivacyFolder, + selectedFolderId, + dragging, + setDragging, + startUpload, + draggable, + setTooltipPosition, + setStartDrag, + history, + isFolder, + isRootFolder, + canShare, + actionType: type, + actionExtension: extension, + actionId: id, + checked: isFileSelected(item.id, item.parentId), + filter, + parentFolder: selectedFolderStore.parentId, + setIsLoading, + fetchFiles, + isImage, + isSound, + isVideo, + canWebEdit, + isTrashFolder: isRecycleBinFolder, + openDocEditor, + expandedKeys, + addExpandedKeys, + setMediaViewerData, + }; + } + )(observer(WithFileActions)); +} diff --git a/products/ASC.Files/Client/src/HOCs/withLoader.js b/products/ASC.Files/Client/src/HOCs/withLoader.js new file mode 100644 index 0000000000..669617e745 --- /dev/null +++ b/products/ASC.Files/Client/src/HOCs/withLoader.js @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from "react"; +import { observer, inject } from "mobx-react"; +import { isMobile } from "react-device-detect"; + +import Loaders from "@appserver/common/components/Loaders"; + +let loadTimeout = null; +export default function withLoader(WrappedComponent, type) { + const withLoader = (props) => { + const { tReady, firstLoad, isLoaded, isLoading } = props; + const [inLoad, setInLoad] = useState(false); + + const cleanTimer = () => { + loadTimeout && clearTimeout(loadTimeout); + loadTimeout = null; + }; + + useEffect(() => { + if (isLoading) { + cleanTimer(); + loadTimeout = setTimeout(() => { + console.log("inLoad", true); + setInLoad(true); + }, 500); + } else { + cleanTimer(); + console.log("inLoad", false); + setInLoad(false); + } + + return () => { + cleanTimer(); + }; + }, [isLoading]); + + return firstLoad || !isLoaded || (isMobile && inLoad) || !tReady ? ( + + ) : ( + + ); + }; + + return inject(({ auth, filesStore }) => { + const { firstLoad, isLoading } = filesStore; + return { + firstLoad, + isLoaded: auth.isLoaded, + isLoading, + }; + })(observer(withLoader)); +} diff --git a/products/ASC.Files/Client/src/components/Badges.js b/products/ASC.Files/Client/src/components/Badges.js new file mode 100644 index 0000000000..6e18e4ea52 --- /dev/null +++ b/products/ASC.Files/Client/src/components/Badges.js @@ -0,0 +1,123 @@ +import React from "react"; +import Badge from "@appserver/components/badge"; +import IconButton from "@appserver/components/icon-button"; +import { + StyledFavoriteIcon, + StyledFileActionsConvertEditDocIcon, + StyledFileActionsLockedIcon, +} from "./Icons"; + +const Badges = ({ + newItems, + item, + canWebEdit, + isTrashFolder, + /* canConvert, */ + accessToEdit, + showNew, + onFilesClick, + onClickLock, + onClickFavorite, + onShowVersionHistory, + onBadgeClick, + /*setConvertDialogVisible*/ +}) => { + const { id, locked, fileStatus, versionGroup, title, fileExst } = item; + + return fileExst ? ( +
+ {/* TODO: Uncomment after fix conversation {canConvert && !isTrashFolder && ( + + )} */} + {canWebEdit && !isTrashFolder && accessToEdit && ( + + )} + {locked && ( + + )} + {fileStatus === 32 && !isTrashFolder && ( + + )} + {fileStatus === 1 && ( + + )} + {versionGroup > 1 && ( + + )} + {showNew && ( + + )} +
+ ) : ( + showNew && ( + + ) + ); +}; + +export default Badges; diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js b/products/ASC.Files/Client/src/components/EditingWrapperComponent.js similarity index 81% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js rename to products/ASC.Files/Client/src/components/EditingWrapperComponent.js index 20542112db..6ac5d79df7 100644 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EditingWrapperComponent.js +++ b/products/ASC.Files/Client/src/components/EditingWrapperComponent.js @@ -2,6 +2,35 @@ import React, { useState } from "react"; import styled from "styled-components"; import Button from "@appserver/components/button"; import TextInput from "@appserver/components/text-input"; +import commonIconsStyles from "@appserver/components/utils/common-icons-style"; + +import CheckIcon from "../../public/images/check.react.svg"; +import CrossIcon from "../../../../../public/images/cross.react.svg"; + +const StyledCheckIcon = styled(CheckIcon)` + ${commonIconsStyles} + path { + fill: #a3a9ae; + } + :hover { + fill: #657077; + } +`; + +const StyledCrossIcon = styled(CrossIcon)` + ${commonIconsStyles} + path { + fill: #a3a9ae; + } + :hover { + fill: #657077; + } +`; + +export const okIcon = ; +export const cancelIcon = ( + +); const EditingWrapper = styled.div` width: 100%; @@ -49,8 +78,6 @@ const EditingWrapperComponent = (props) => { const { itemTitle, itemId, - okIcon, - cancelIcon, renameTitle, onClickUpdateItem, cancelUpdateItem, diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFilterContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyFilterContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFilterContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyFilterContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFolderContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/EmptyFolderContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/EmptyFolderContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/EmptyFolderContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/RootFolderContainer.js b/products/ASC.Files/Client/src/components/EmptyContainer/RootFolderContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/RootFolderContainer.js rename to products/ASC.Files/Client/src/components/EmptyContainer/RootFolderContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/index.js b/products/ASC.Files/Client/src/components/EmptyContainer/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/EmptyContainer/index.js rename to products/ASC.Files/Client/src/components/EmptyContainer/index.js diff --git a/products/ASC.Files/Client/src/components/Icons.js b/products/ASC.Files/Client/src/components/Icons.js new file mode 100644 index 0000000000..3e9538e4bf --- /dev/null +++ b/products/ASC.Files/Client/src/components/Icons.js @@ -0,0 +1,36 @@ +import styled from "styled-components"; + +import commonIconsStyles from "@appserver/components/utils/common-icons-style"; + +import FavoriteIcon from "../../public/images/favorite.react.svg"; +import FileActionsConvertEditDocIcon from "../../public/images/file.actions.convert.edit.doc.react.svg"; +import FileActionsLockedIcon from "../../public/images/file.actions.locked.react.svg"; + +export const EncryptedFileIcon = styled.div` + background: url("images/security.svg") no-repeat 0 0 / 16px 16px transparent; + height: 16px; + position: absolute; + width: 16px; + margin-top: 14px; + margin-left: ${(props) => (props.isEdit ? "40px" : "12px")}; +`; + +export const StyledFavoriteIcon = styled(FavoriteIcon)` + ${commonIconsStyles} +`; + +export const StyledFileActionsConvertEditDocIcon = styled( + FileActionsConvertEditDocIcon +)` + ${commonIconsStyles} + path { + fill: #3b72a7; + } +`; + +export const StyledFileActionsLockedIcon = styled(FileActionsLockedIcon)` + ${commonIconsStyles} + path { + fill: #3b72a7; + } +`; diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContainer.js b/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContainer.js deleted file mode 100644 index c63c072e9e..0000000000 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContainer.js +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { inject, observer } from "mobx-react"; -import RowContainer from "@appserver/components/row-container"; -import { Consumer } from "@appserver/components/utils/context"; -import SimpleFilesRow from "./SimpleFilesRow"; -import Loaders from "@appserver/common/components/Loaders"; -import { isMobile } from "react-device-detect"; - -let loadTimeout = null; - -const FilesRowContainer = ({ isLoaded, isLoading, filesList, tReady }) => { - const [inLoad, setInLoad] = useState(false); - - const cleanTimer = () => { - loadTimeout && clearTimeout(loadTimeout); - loadTimeout = null; - }; - - useEffect(() => { - if (isLoading) { - cleanTimer(); - loadTimeout = setTimeout(() => { - console.log("inLoad", true); - setInLoad(true); - }, 500); - } else { - cleanTimer(); - console.log("inLoad", false); - setInLoad(false); - } - - return () => { - cleanTimer(); - }; - }, [isLoading]); - - return !isLoaded || (isMobile && inLoad) || !tReady ? ( - - ) : ( - - {(context) => ( - - {filesList.map((item, index) => ( - - ))} - - )} - - ); -}; - -export default inject(({ auth, filesStore }) => { - const { filesList, isLoading } = filesStore; - - return { - filesList, - isLoading, - isLoaded: auth.isLoaded, - }; -})(observer(FilesRowContainer)); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContent.js b/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContent.js deleted file mode 100644 index d6f9cc7ffe..0000000000 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/FilesRowContent.js +++ /dev/null @@ -1,902 +0,0 @@ -import React from "react"; -import { withRouter } from "react-router"; -import { Trans, withTranslation } from "react-i18next"; -import styled from "styled-components"; -import Link from "@appserver/components/link"; -import Text from "@appserver/components/text"; -import RowContent from "@appserver/components/row-content"; -import IconButton from "@appserver/components/icon-button"; -import Badge from "@appserver/components/badge"; -import commonIconsStyles from "@appserver/components/utils/common-icons-style"; -import { - convertFile, - getFileConversationProgress, -} from "@appserver/common/api/files"; -import { - AppServerConfig, - FileAction, - ShareAccessRights, -} from "@appserver/common/constants"; -import toastr from "studio/toastr"; -import FavoriteIcon from "../../../../../../../public/images/favorite.react.svg"; -import FileActionsConvertEditDocIcon from "../../../../../../../public/images/file.actions.convert.edit.doc.react.svg"; -import FileActionsLockedIcon from "../../../../../../../public/images/file.actions.locked.react.svg"; -import CheckIcon from "../../../../../../../public/images/check.react.svg"; -import CrossIcon from "../../../../../../../../../../public/images/cross.react.svg"; -import { TIMEOUT } from "../../../../../../helpers/constants"; -import { getTitleWithoutExst } from "../../../../../../helpers/files-helpers"; -import { ConvertDialog } from "../../../../../dialogs"; -import EditingWrapperComponent from "../EditingWrapperComponent"; -import { isMobile } from "react-device-detect"; -import { observer, inject } from "mobx-react"; -import config from "../../../../../../../package.json"; -import { combineUrl } from "@appserver/common/utils"; - -const sideColor = "#A3A9AE"; -const StyledCheckIcon = styled(CheckIcon)` - ${commonIconsStyles} - path { - fill: #a3a9ae; - } - :hover { - fill: #657077; - } -`; - -const StyledCrossIcon = styled(CrossIcon)` - ${commonIconsStyles} - path { - fill: #a3a9ae; - } - :hover { - fill: #657077; - } -`; - -const StyledFavoriteIcon = styled(FavoriteIcon)` - ${commonIconsStyles} -`; - -const StyledFileActionsConvertEditDocIcon = styled( - FileActionsConvertEditDocIcon -)` - ${commonIconsStyles} - path { - fill: #3b72a7; - } -`; - -const StyledFileActionsLockedIcon = styled(FileActionsLockedIcon)` - ${commonIconsStyles} - path { - fill: #3b72a7; - } -`; -const SimpleFilesRowContent = styled(RowContent)` - .badge-ext { - margin-left: -8px; - margin-right: 8px; - } - - .badge { - height: 14px; - width: 14px; - margin-right: 6px; - } - .lock-file { - cursor: pointer; - } - .badges { - display: flex; - align-items: center; - } - - .favorite { - cursor: pointer; - margin-right: 6px; - } - - .share-icon { - margin-top: -4px; - padding-right: 8px; - } - - .row_update-text { - overflow: hidden; - text-overflow: ellipsis; - } -`; - -const okIcon = ; - -const cancelIcon = ( - -); - -class FilesRowContent extends React.PureComponent { - constructor(props) { - super(props); - let titleWithoutExt = getTitleWithoutExst(props.item); - - if (props.fileActionId === -1) { - titleWithoutExt = this.getDefaultName(props.fileActionExt); - } - - this.state = { - itemTitle: titleWithoutExt, - showConvertDialog: false, - //loading: false - }; - } - - completeAction = (id) => { - const isCancel = - (id.currentTarget && id.currentTarget.dataset.action === "cancel") || - id.keyCode === 27; - this.props.editCompleteAction(id, this.props.item, isCancel); - }; - - updateItem = () => { - const { - t, - updateFile, - renameFolder, - item, - setIsLoading, - fileActionId, - editCompleteAction, - } = this.props; - - const { itemTitle } = this.state; - const originalTitle = getTitleWithoutExst(item); - - setIsLoading(true); - const isSameTitle = - originalTitle.trim() === itemTitle.trim() || itemTitle.trim() === ""; - if (isSameTitle) { - this.setState({ - itemTitle: originalTitle, - }); - return editCompleteAction(fileActionId, item, isSameTitle); - } - - item.fileExst || item.contentLength - ? updateFile(fileActionId, itemTitle) - .then(() => this.completeAction(fileActionId)) - .then(() => - toastr.success( - t("FileRenamed", { - oldTitle: item.title, - newTitle: itemTitle + item.fileExst, - }) - ) - ) - .catch((err) => toastr.error(err)) - .finally(() => setIsLoading(false)) - : renameFolder(fileActionId, itemTitle) - .then(() => this.completeAction(fileActionId)) - .then(() => - toastr.success( - t("FolderRenamed", { - folderTitle: item.title, - newFoldedTitle: itemTitle, - }) - ) - ) - .catch((err) => toastr.error(err)) - .finally(() => setIsLoading(false)); - }; - - createItem = (e) => { - const { - createFile, - item, - setIsLoading, - openDocEditor, - isPrivacy, - isDesktop, - replaceFileStream, - t, - setEncryptionAccess, - createFolder, - } = this.props; - const { itemTitle } = this.state; - - setIsLoading(true); - - const itemId = e.currentTarget.dataset.itemid; - - if (itemTitle.trim() === "") { - toastr.warning(this.props.t("CreateWithEmptyTitle")); - return this.completeAction(itemId); - } - - let tab = - !isDesktop && item.fileExst - ? window.open( - combineUrl( - AppServerConfig.proxyURL, - config.homepage, - "/products/files/doceditor" - ), - "_blank" - ) - : null; - - !item.fileExst && !item.contentLength - ? createFolder(item.parentId, itemTitle) - .then(() => this.completeAction(itemId)) - .then(() => - toastr.success( - - New folder {{ itemTitle }} is created - - ) - ) - .catch((e) => toastr.error(e)) - .finally(() => { - return setIsLoading(false); - }) - : createFile(item.parentId, `${itemTitle}.${item.fileExst}`) - .then((file) => { - if (isPrivacy) { - return setEncryptionAccess(file).then((encryptedFile) => { - if (!encryptedFile) return Promise.resolve(); - toastr.info(t("EncryptedFileSaving")); - return replaceFileStream( - file.id, - encryptedFile, - true, - false - ).then(() => - openDocEditor(file.id, file.providerKey, tab, file.webUrl) - ); - }); - } - return openDocEditor(file.id, file.providerKey, tab, file.webUrl); - }) - .then(() => this.completeAction(itemId)) - .then(() => { - const exst = item.fileExst; - return toastr.success( - - New file {{ itemTitle }}.{{ exst }} is created - - ); - }) - .catch((e) => toastr.error(e)) - .finally(() => { - return setIsLoading(false); - }); - }; - - componentDidUpdate(prevProps) { - const { fileActionId, fileActionExt } = this.props; - if (fileActionId === -1 && fileActionExt !== prevProps.fileActionExt) { - const itemTitle = this.getDefaultName(fileActionExt); - this.setState({ itemTitle }); - } - // if (fileAction) { - // if (fileActionId !== prevProps.fileActionId) { - // this.setState({ editingId: fileActionId }); - // } - // } - } - - renameTitle = (e) => { - let title = e.target.value; - //const chars = '*+:"<>?|/'; TODO: think how to solve problem with interpolation escape values in i18n translate - const regexp = new RegExp('[*+:"<>?|\\\\/]', "gim"); - if (title.match(regexp)) { - toastr.warning(this.props.t("ContainsSpecCharacter")); - } - title = title.replace(regexp, "_"); - return this.setState({ itemTitle: title }); - }; - - cancelUpdateItem = (e) => { - const originalTitle = getTitleWithoutExst(this.props.item); - this.setState({ - itemTitle: originalTitle, - }); - - return this.completeAction(e); - }; - - onClickUpdateItem = (e) => { - this.props.fileActionType === FileAction.Create - ? this.createItem(e) - : this.updateItem(e); - }; - - onFilesClick = () => { - const { - filter, - parentFolder, - setIsLoading, - fetchFiles, - isImage, - isSound, - isVideo, - canWebEdit, - item, - isTrashFolder, - openDocEditor, - expandedKeys, - addExpandedKeys, - setMediaViewerData, - } = this.props; - const { id, fileExst, viewUrl, providerKey, contentLength } = item; - - if (isTrashFolder) return; - - if (!fileExst && !contentLength) { - setIsLoading(true); - - if (!expandedKeys.includes(parentFolder + "")) { - addExpandedKeys(parentFolder + ""); - } - - fetchFiles(id, filter) - .catch((err) => { - toastr.error(err); - setIsLoading(false); - }) - .finally(() => setIsLoading(false)); - } else { - if (canWebEdit) { - return openDocEditor(id, providerKey); - } - - if (isImage || isSound || isVideo) { - setMediaViewerData({ visible: true, id }); - return; - } - - return window.open(viewUrl, "_blank"); - } - }; - - onMobileRowClick = () => { - if (this.props.isTrashFolder || window.innerWidth > 1024) return; - this.onFilesClick(); - }; - - getStatusByDate = () => { - const { culture, t, item, sectionWidth } = this.props; - const { created, updated, version, fileExst } = item; - - const title = - version > 1 - ? t("TitleModified") - : fileExst - ? t("TitleUploaded") - : t("TitleCreated"); - - const date = fileExst ? updated : created; - const dateLabel = new Date(date).toLocaleString(culture); - const mobile = (sectionWidth && sectionWidth <= 375) || isMobile; - - return mobile ? dateLabel : `${title}: ${dateLabel}`; - }; - - getDefaultName = (format) => { - const { t } = this.props; - - switch (format) { - case "docx": - return t("NewDocument"); - case "xlsx": - return t("NewSpreadsheet"); - case "pptx": - return t("NewPresentation"); - default: - return t("NewFolder"); - } - }; - - onShowVersionHistory = () => { - const { - homepage, - isTabletView, - item, - setIsVerHistoryPanel, - fetchFileVersions, - history, - isTrashFolder, - } = this.props; - if (isTrashFolder) return; - - if (!isTabletView) { - fetchFileVersions(item.id + ""); - setIsVerHistoryPanel(true); - } else { - history.push( - combineUrl(AppServerConfig.proxyURL, homepage, `/${item.id}/history`) - ); - } - }; - - onBadgeClick = () => { - const { - item, - selectedFolderPathParts, - markAsRead, - setNewFilesPanelVisible, - setNewFilesIds, - updateRootBadge, - updateFileBadge, - } = this.props; - if (item.fileExst) { - markAsRead([], [item.id]) - .then(() => { - updateRootBadge(selectedFolderPathParts[0], 1); - updateFileBadge(item.id); - }) - .catch((err) => toastr.error(err)); - } else { - setNewFilesPanelVisible(true); - const newFolderIds = this.props.selectedFolderPathParts; - newFolderIds.push(item.id); - setNewFilesIds(newFolderIds); - } - }; - - setConvertDialogVisible = () => - this.setState({ showConvertDialog: !this.state.showConvertDialog }); - - getConvertProgress = (fileId) => { - const { - selectedFolderId, - filter, - setIsLoading, - setSecondaryProgressBarData, - t, - clearSecondaryProgressData, - fetchFiles, - } = this.props; - getFileConversationProgress(fileId).then((res) => { - if (res && res[0] && res[0].progress !== 100) { - setSecondaryProgressBarData({ - icon: "file", - visible: true, - percent: res[0].progress, - label: t("Convert"), - alert: false, - }); - setTimeout(() => this.getConvertProgress(fileId), 1000); - } else { - if (res[0].error) { - setSecondaryProgressBarData({ - visible: true, - alert: true, - }); - toastr.error(res[0].error); - setTimeout(() => clearSecondaryProgressData(), TIMEOUT); - } else { - setSecondaryProgressBarData({ - icon: "file", - visible: true, - percent: 100, - label: t("Convert"), - alert: false, - }); - setTimeout(() => clearSecondaryProgressData(), TIMEOUT); - const newFilter = filter.clone(); - fetchFiles(selectedFolderId, newFilter) - .catch((err) => { - setSecondaryProgressBarData({ - visible: true, - alert: true, - }); - //toastr.error(err); - setTimeout(() => clearSecondaryProgressData(), TIMEOUT); - }) - .finally(() => setIsLoading(false)); - } - } - }); - }; - - onConvert = () => { - const { item, t, setSecondaryProgressBarData } = this.props; - setSecondaryProgressBarData({ - icon: "file", - visible: true, - percent: 0, - label: t("Convert"), - alert: false, - }); - this.setState({ showConvertDialog: false }, () => - convertFile(item.id).then((convertRes) => { - if (convertRes && convertRes[0] && convertRes[0].progress !== 100) { - this.getConvertProgress(item.id); - } - }) - ); - }; - - onClickLock = () => { - const { item } = this.props; - const { locked, id } = item; - this.props.lockFileAction(id, !locked).catch((err) => toastr.error(err)); - }; - - onClickFavorite = () => { - const { t, item } = this.props; - this.props - .setFavoriteAction("remove", item.id) - .then(() => toastr.success(t("RemovedFromFavorites"))) - .catch((err) => toastr.error(err)); - }; - render() { - const { - t, - item, - isTrashFolder, - isLoading, - isMobile, - canWebEdit, - /* canConvert,*/ - sectionWidth, - fileActionId, - fileActionExt, - } = this.props; - const { itemTitle, showConvertDialog } = this.state; - const { - contentLength, - updated, - createdBy, - fileExst, - filesCount, - foldersCount, - fileStatus, - id, - versionGroup, - locked, - providerKey, - } = item; - const titleWithoutExt = getTitleWithoutExst(item); - const fileOwner = - createdBy && - ((this.props.viewer.id === createdBy.id && t("AuthorMe")) || - createdBy.displayName); - const updatedDate = updated && this.getStatusByDate(); - - const accessToEdit = - item.access === ShareAccessRights.FullAccess || - item.access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) - const isEdit = id === fileActionId && fileExst === fileActionExt; - - const linkStyles = isTrashFolder //|| window.innerWidth <= 1024 - ? { noHover: true } - : { onClick: this.onFilesClick }; - - const newItems = item.new || fileStatus === 2; - const showNew = !!newItems; - - return isEdit ? ( - - ) : ( - <> - {showConvertDialog && ( - - )} - - - {titleWithoutExt} - - <> - {fileExst ? ( -
- - {fileExst} - - {/* TODO: Uncomment after fix conversation {canConvert && !isTrashFolder && ( - - )} */} - {canWebEdit && !isTrashFolder && accessToEdit && ( - - )} - {locked && ( - - )} - {fileStatus === 32 && !isTrashFolder && ( - - )} - {fileStatus === 1 && ( - - )} - {versionGroup > 1 && ( - - )} - {showNew && ( - - )} -
- ) : ( -
- {showNew && ( - - )} -
- )} - - - {fileOwner} - - - {(fileExst || contentLength || !providerKey) && - updatedDate && - updatedDate} - - - {fileExst || contentLength - ? contentLength - : !providerKey - ? `${t("TitleDocuments")}: ${filesCount} | ${t( - "TitleSubfolders" - )}: ${foldersCount}` - : ""} - -
- - ); - } -} - -export default inject( - ( - { - auth, - filesStore, - formatsStore, - uploadDataStore, - treeFoldersStore, - selectedFolderStore, - filesActionsStore, - mediaViewerDataStore, - versionHistoryStore, - dialogsStore, - }, - { item } - ) => { - const { replaceFileStream, setEncryptionAccess } = auth; - const { culture, isDesktopClient, isTabletView } = auth.settingsStore; - const { secondaryProgressDataStore } = uploadDataStore; - const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; - const { - iconFormatsStore, - mediaViewersFormatsStore, - docserviceStore, - } = formatsStore; - - const { - fetchFiles, - filter, - createFile, - updateFile, - renameFolder, - createFolder, - openDocEditor, - setIsLoading, - isLoading, - updateFileBadge, - } = filesStore; - - const { - isRecycleBinFolder, - isPrivacyFolder, - expandedKeys, - addExpandedKeys, - updateRootBadge, - } = treeFoldersStore; - - const { - type: fileActionType, - extension: fileActionExt, - id: fileActionId, - } = filesStore.fileActionStore; - - const { - setSecondaryProgressBarData, - clearSecondaryProgressData, - } = secondaryProgressDataStore; - - const canWebEdit = docserviceStore.canWebEdit(item.fileExst); - const canConvert = docserviceStore.canConvert(item.fileExst); - const isVideo = mediaViewersFormatsStore.isVideo(item.fileExst); - const isImage = iconFormatsStore.isImage(item.fileExst); - const isSound = iconFormatsStore.isSound(item.fileExst); - - const { setMediaViewerData } = mediaViewerDataStore; - const { - editCompleteAction, - lockFileAction, - setFavoriteAction, - markAsRead, - } = filesActionsStore; - - const { setNewFilesPanelVisible, setNewFilesIds } = dialogsStore; - - return { - isDesktop: isDesktopClient, - isTabletView, - homepage: config.homepage, - viewer: auth.userStore.user, - culture, - fileActionId, - fileActionType, - fileActionExt, - selectedFolderId: selectedFolderStore.id, - selectedFolderPathParts: selectedFolderStore.pathParts, - parentFolder: selectedFolderStore.parentId, - isLoading, - isTrashFolder: isRecycleBinFolder, - isPrivacy: isPrivacyFolder, - filter, - canWebEdit, - canConvert, - isVideo, - isImage, - isSound, - expandedKeys, - - setIsLoading, - fetchFiles, - setSecondaryProgressBarData, - clearSecondaryProgressData, - createFile, - createFolder, - updateFile, - renameFolder, - replaceFileStream, - setEncryptionAccess, - addExpandedKeys, - openDocEditor, - editCompleteAction, - lockFileAction, - setFavoriteAction, - setMediaViewerData, - setIsVerHistoryPanel, - fetchFileVersions, - markAsRead, - setNewFilesPanelVisible, - setNewFilesIds, - updateRootBadge, - updateFileBadge, - }; - } -)(withRouter(withTranslation("Home")(observer(FilesRowContent)))); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/SimpleFilesRow.js b/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/SimpleFilesRow.js deleted file mode 100644 index cadd767582..0000000000 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesRow/SimpleFilesRow.js +++ /dev/null @@ -1,721 +0,0 @@ -import React, { useCallback } from "react"; -import { ReactSVG } from "react-svg"; -import styled from "styled-components"; -import { inject, observer } from "mobx-react"; -import { withTranslation } from "react-i18next"; -import IconButton from "@appserver/components/icon-button"; -import Text from "@appserver/components/text"; -import DragAndDrop from "@appserver/components/drag-and-drop"; -import Row from "@appserver/components/row"; -import FilesRowContent from "./FilesRowContent"; -import { withRouter } from "react-router-dom"; -import toastr from "studio/toastr"; -import { FileAction, AppServerConfig } from "@appserver/common/constants"; -import copy from "copy-to-clipboard"; -import config from "../../../../../../../package.json"; -import { combineUrl } from "@appserver/common/utils"; -import { createSelectable } from "react-selectable-fast"; - -const StyledSimpleFilesRow = styled(Row)` - margin-top: -2px; - ${(props) => - !props.contextOptions && - ` - & > div:last-child { - width: 0px; - } - `} - - .share-button-icon { - margin-right: 7px; - margin-top: -1px; - } - - .share-button:hover, - .share-button-icon:hover { - cursor: pointer; - color: #657077; - path { - fill: #657077; - } - } - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - - @media (max-width: 1312px) { - .share-button { - padding-top: 3px; - } - } - - .styled-element { - margin-right: 7px; - } -`; - -const EncryptedFileIcon = styled.div` - background: url("images/security.svg") no-repeat 0 0 / 16px 16px transparent; - height: 16px; - position: absolute; - width: 16px; - margin-top: 14px; - margin-left: ${(props) => (props.isEdit ? "40px" : "12px")}; -`; - -const svgLoader = () =>
; - -const SimpleFilesRow = createSelectable((props) => { - const { - t, - item, - sectionWidth, - actionType, - actionExtension, - isPrivacy, - isRecycleBin, - dragging, - checked, - canShare, - isFolder, - draggable, - isRootFolder, - homepage, - isTabletView, - actionId, - selectedFolderId, - - setSharingPanelVisible, - setChangeOwnerPanelVisible, - setDeleteThirdPartyDialogVisible, - setRemoveItem, - setMoveToPanelVisible, - setCopyPanelVisible, - openDocEditor, - setIsVerHistoryPanel, - fetchFileVersions, - setAction, - deleteFileAction, - deleteFolderAction, - lockFileAction, - duplicateAction, - finalizeVersionAction, - setFavoriteAction, - openLocationAction, - selectRowAction, - setThirdpartyInfo, - setMediaViewerData, - setDragging, - setStartDrag, - startUpload, - onSelectItem, - history, - setTooltipPosition, - setDownloadDialogVisible, - downloadAction, - confirmDelete, - setDeleteDialogVisible, - } = props; - - const { - id, - title, - fileExst, - contentLength, - shared, - access, - contextOptions, - icon, - providerKey, - folderId, - viewUrl, - webUrl, - canOpenPlayer, - locked, - parentId, - } = item; - - const isThirdPartyFolder = providerKey && isRootFolder; - - const onContentRowSelect = (checked, file) => { - if (!file) return; - - selectRowAction(checked, file); - }; - - const onClickShare = () => { - onSelectItem(item); - setSharingPanelVisible(true); - }; - const onOwnerChange = () => setChangeOwnerPanelVisible(true); - const onMoveAction = () => setMoveToPanelVisible(true); - const onCopyAction = () => setCopyPanelVisible(true); - - const getSharedButton = (shared) => { - const color = shared ? "#657077" : "#a3a9ae"; - return ( - - - {t("Share")} - - ); - }; - - const getItemIcon = (isEdit) => { - return ( - <> - - {isPrivacy && fileExst && } - - ); - }; - - const onOpenLocation = () => { - const locationId = isFolder ? id : folderId; - openLocationAction(locationId, isFolder); - }; - - const showVersionHistory = () => { - if (!isTabletView) { - fetchFileVersions(id + ""); - setIsVerHistoryPanel(true); - } else { - history.push( - combineUrl(AppServerConfig.proxyURL, homepage, `/${id}/history`) - ); - } - }; - - const finalizeVersion = () => - finalizeVersionAction(id).catch((err) => toastr.error(err)); - - const onClickFavorite = (e) => { - const data = (e.currentTarget && e.currentTarget.dataset) || e; - const { action } = data; - - setFavoriteAction(action, id) - .then(() => - action === "mark" - ? toastr.success(t("MarkedAsFavorite")) - : toastr.success(t("RemovedFromFavorites")) - ) - .catch((err) => toastr.error(err)); - }; - - const lockFile = () => - lockFileAction(id, !locked).catch((err) => toastr.error(err)); - - const onClickLinkForPortal = () => { - const isFile = !!fileExst; - copy( - isFile - ? canOpenPlayer - ? `${window.location.href}&preview=${id}` - : webUrl - : `${window.location.origin + homepage}/filter?folder=${id}` - ); - - toastr.success(t("LinkCopySuccess")); - }; - - const onClickLinkEdit = () => openDocEditor(id, providerKey); - - const onClickDownload = () => { - const isFile = !!fileExst && contentLength; - isFile - ? window.open(viewUrl, "_blank") - : downloadAction(t("ArchivingData")).catch((err) => toastr.error(err)); - }; - - const onClickDownloadAs = () => setDownloadDialogVisible(true); - - const onDuplicate = () => - duplicateAction(item, t("CopyOperation")).catch((err) => toastr.error(err)); - - const onClickRename = () => { - setAction({ - type: FileAction.Rename, - extension: fileExst, - id, - }); - }; - - const onChangeThirdPartyInfo = () => setThirdpartyInfo(providerKey); - - const onMediaFileClick = (fileId) => { - const itemId = typeof fileId !== "object" ? fileId : id; - setMediaViewerData({ visible: true, id: itemId }); - }; - - const onClickDelete = () => { - if (isThirdPartyFolder) { - const splitItem = id.split("-"); - setRemoveItem({ id: splitItem[splitItem.length - 1], title }); - setDeleteThirdPartyDialogVisible(true); - return; - } - - if (confirmDelete) { - setDeleteDialogVisible(true); - } else { - const translations = { - deleteOperation: t("DeleteOperation"), - }; - - fileExst || contentLength - ? deleteFileAction(id, folderId, translations) - .then(() => toastr.success(t("FileRemoved"))) - .catch((err) => toastr.error(err)) - : deleteFolderAction(id, parentId, translations) - .then(() => toastr.success(t("FolderRemoved"))) - .catch((err) => toastr.error(err)); - } - }; - - const rowContextClick = () => { - onSelectItem(item); - }; - - const getFilesContextOptions = useCallback(() => { - const isSharable = access !== 1 && access !== 0; - return contextOptions.map((option) => { - switch (option) { - case "open": - return { - key: option, - label: t("Open"), - icon: "images/catalog.folder.react.svg", - onClick: onOpenLocation, - disabled: false, - }; - case "show-version-history": - return { - key: option, - label: t("ShowVersionHistory"), - icon: "images/history.react.svg", - onClick: showVersionHistory, - disabled: false, - }; - case "finalize-version": - return { - key: option, - label: t("FinalizeVersion"), - icon: "images/history-finalized.react.svg", - onClick: finalizeVersion, - disabled: false, - }; - case "separator0": - case "separator1": - case "separator2": - case "separator3": - return { key: option, isSeparator: true }; - case "open-location": - return { - key: option, - label: t("OpenLocation"), - icon: "images/download-as.react.svg", - onClick: onOpenLocation, - disabled: false, - }; - case "mark-as-favorite": - return { - key: option, - label: t("MarkAsFavorite"), - icon: "images/favorites.react.svg", - onClick: onClickFavorite, - disabled: false, - "data-action": "mark", - action: "mark", - }; - case "block-unblock-version": - return { - key: option, - label: t("UnblockVersion"), - icon: "images/lock.react.svg", - onClick: lockFile, - disabled: false, - }; - case "sharing-settings": - return { - key: option, - label: t("SharingSettings"), - icon: "images/catalog.shared.react.svg", - onClick: onClickShare, - disabled: isSharable, - }; - case "send-by-email": - return { - key: option, - label: t("SendByEmail"), - icon: "/static/images/mail.react.svg", - disabled: true, - }; - case "owner-change": - return { - key: option, - label: t("ChangeOwner"), - icon: "images/catalog.user.react.svg", - onClick: onOwnerChange, - disabled: false, - }; - case "link-for-portal-users": - return { - key: option, - label: t("LinkForPortalUsers"), - icon: "/static/images/invitation.link.react.svg", - onClick: onClickLinkForPortal, - disabled: false, - }; - case "edit": - return { - key: option, - label: t("Edit"), - icon: "/static/images/access.edit.react.svg", - onClick: onClickLinkEdit, - disabled: false, - }; - case "preview": - return { - key: option, - label: t("Preview"), - icon: "EyeIcon", - onClick: onClickLinkEdit, - disabled: true, - }; - case "view": - return { - key: option, - label: t("View"), - icon: "/static/images/eye.react.svg", - onClick: onMediaFileClick, - disabled: false, - }; - case "download": - return { - key: option, - label: t("Download"), - icon: "images/download.react.svg", - onClick: onClickDownload, - disabled: false, - }; - case "download-as": - return { - key: option, - label: t("DownloadAs"), - icon: "images/download-as.react.svg", - onClick: onClickDownloadAs, - disabled: false, - }; - case "move-to": - return { - key: option, - label: t("MoveTo"), - icon: "images/move.react.svg", - onClick: onMoveAction, - disabled: false, - }; - case "restore": - return { - key: option, - label: t("Restore"), - icon: "images/move.react.svg", - onClick: onMoveAction, - disabled: false, - }; - case "copy-to": - return { - key: option, - label: t("Copy"), - icon: "/static/images/copy.react.svg", - onClick: onCopyAction, - disabled: false, - }; - case "copy": - return { - key: option, - label: t("Duplicate"), - icon: "/static/images/copy.react.svg", - onClick: onDuplicate, - disabled: false, - }; - case "rename": - return { - key: option, - label: t("Rename"), - icon: "images/rename.react.svg", - onClick: onClickRename, - disabled: false, - }; - case "change-thirdparty-info": - return { - key: option, - label: t("ThirdPartyInfo"), - icon: "/static/images/access.edit.react.svg", - onClick: onChangeThirdPartyInfo, - disabled: false, - }; - case "delete": - return { - key: option, - label: isThirdPartyFolder ? t("DeleteThirdParty") : t("Delete"), - icon: "/static/images/catalog.trash.react.svg", - onClick: onClickDelete, - disabled: false, - }; - case "remove-from-favorites": - return { - key: option, - label: t("RemoveFromFavorites"), - icon: "images/favorites.react.svg", - onClick: onClickFavorite, - disabled: false, - "data-action": "remove", - action: "remove", - }; - default: - break; - } - - return undefined; - }); - }, [contextOptions, item]); - - const onDropZoneUpload = (files, uploadToFolder) => { - const folderId = uploadToFolder ? uploadToFolder : selectedFolderId; - - dragging && setDragging(false); - startUpload(files, folderId, t); - }; - - const onDrop = (items) => { - if (!fileExst) { - onDropZoneUpload(items, id); - } else { - onDropZoneUpload(items, selectedFolderId); - } - }; - - const onMouseDown = (e) => { - if (!draggable) { - return; - } - - if ( - window.innerWidth < 1025 || - e.target.tagName === "rect" || - e.target.tagName === "path" - ) { - return; - } - const mouseButton = e.which - ? e.which !== 1 - : e.button - ? e.button !== 0 - : false; - const label = e.currentTarget.getAttribute("label"); - if (mouseButton || e.currentTarget.tagName !== "DIV" || label) { - return; - } - - setTooltipPosition(e.pageX, e.pageY); - setStartDrag(true); - }; - - const isMobile = sectionWidth < 500; - const isEdit = - !!actionType && actionId === id && fileExst === actionExtension; - const contextOptionsProps = - !isEdit && contextOptions && contextOptions.length > 0 - ? { - contextOptions: getFilesContextOptions(), - } - : {}; - - const isDragging = isFolder && access < 2 && !isRecycleBin; - const checkedProps = isEdit || id <= 0 ? {} : { checked }; - const element = getItemIcon(isEdit || id <= 0); - const displayShareButton = isMobile ? "26px" : !canShare ? "38px" : "96px"; - let className = isDragging ? " droppable" : ""; - if (draggable) className += " draggable not-selectable"; - - let value = fileExst || contentLength ? `file_${id}` : `folder_${id}`; - value += draggable ? "_draggable" : ""; - - const sharedButton = - !canShare || (isPrivacy && !fileExst) || isEdit || id <= 0 || isMobile - ? null - : getSharedButton(shared); - - return ( -
- - - - - -
- ); -}); - -export default inject( - ( - { - auth, - filesStore, - treeFoldersStore, - selectedFolderStore, - dialogsStore, - versionHistoryStore, - filesActionsStore, - mediaViewerDataStore, - uploadDataStore, - settingsStore, - }, - { item } - ) => { - const { isTabletView } = auth.settingsStore; - const { type, extension, id } = filesStore.fileActionStore; - const { isRecycleBinFolder, isPrivacyFolder } = treeFoldersStore; - - const { - setSharingPanelVisible, - setChangeOwnerPanelVisible, - setRemoveItem, - setDeleteThirdPartyDialogVisible, - setMoveToPanelVisible, - setCopyPanelVisible, - setDownloadDialogVisible, - setDeleteDialogVisible, - } = dialogsStore; - - const { - selection, - canShare, - openDocEditor, - fileActionStore, - dragging, - setDragging, - setStartDrag, - setTooltipPosition, - isFileSelected, - } = filesStore; - - const { isRootFolder, id: selectedFolderId } = selectedFolderStore; - const { setIsVerHistoryPanel, fetchFileVersions } = versionHistoryStore; - const { setAction } = fileActionStore; - - const selectedItem = selection.find( - (x) => x.id === item.id && x.fileExst === item.fileExst - ); - - const isFolder = selectedItem - ? false - : item.fileExst || item.contentLength - ? false - : true; - - const draggable = - !isRecycleBinFolder && selectedItem && selectedItem.id !== id; - - const { - deleteFileAction, - deleteFolderAction, - lockFileAction, - finalizeVersionAction, - duplicateAction, - setFavoriteAction, - openLocationAction, - selectRowAction, - setThirdpartyInfo, - onSelectItem, - downloadAction, - } = filesActionsStore; - - const { setMediaViewerData } = mediaViewerDataStore; - const { startUpload } = uploadDataStore; - - return { - dragging, - actionType: type, - actionExtension: extension, - isPrivacy: isPrivacyFolder, - isRecycleBin: isRecycleBinFolder, - isRootFolder, - canShare, - checked: isFileSelected(item.id, item.parentId), - isFolder, - draggable, - isItemsSelected: !!selection.length, - homepage: config.homepage, - isTabletView, - actionId: fileActionStore.id, - setSharingPanelVisible, - setChangeOwnerPanelVisible, - setRemoveItem, - setDeleteThirdPartyDialogVisible, - setMoveToPanelVisible, - setCopyPanelVisible, - setDownloadDialogVisible, - openDocEditor, - setIsVerHistoryPanel, - fetchFileVersions, - setAction, - deleteFileAction, - deleteFolderAction, - lockFileAction, - finalizeVersionAction, - duplicateAction, - setFavoriteAction, - openLocationAction, - selectRowAction, - setThirdpartyInfo, - setMediaViewerData, - selectedFolderId, - setDragging, - setStartDrag, - startUpload, - onSelectItem, - setTooltipPosition, - downloadAction, - confirmDelete: settingsStore.confirmDelete, - setDeleteDialogVisible, - }; - } -)(withTranslation("Home")(observer(withRouter(SimpleFilesRow)))); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesTileContent.js b/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesTileContent.js deleted file mode 100644 index bde292e9ab..0000000000 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesTileContent.js +++ /dev/null @@ -1,500 +0,0 @@ -import React from "react"; -import { withRouter } from "react-router"; -import { Trans, withTranslation } from "react-i18next"; -import styled from "styled-components"; -import Badge from "@appserver/components/badge"; -import Link from "@appserver/components/link"; -import Text from "@appserver/components/text"; -import { markAsRead } from "@appserver/common/api/files"; -import { FileAction, AppServerConfig } from "@appserver/common/constants"; -import toastr from "studio/toastr"; -import { getTitleWithoutExst } from "../../../../../helpers/files-helpers"; -import { NewFilesPanel } from "../../../../panels"; -import EditingWrapperComponent from "./EditingWrapperComponent"; -import TileContent from "./TileContent"; -import { isMobile } from "react-device-detect"; -import { inject, observer } from "mobx-react"; -import CheckIcon from "../../../../../../public/images/check.react.svg"; -import CrossIcon from "../../../../../../../../../public/images/cross.react.svg"; -import config from "../../../../../../package.json"; -import { combineUrl } from "@appserver/common/utils"; - -const SimpleFilesTileContent = styled(TileContent)` - .rowMainContainer { - height: auto; - max-width: 100%; - align-self: flex-end; - - a { - word-break: break-word; - } - } - - .mainIcons { - align-self: flex-end; - } - - .badge-ext { - margin-left: -8px; - margin-right: 8px; - } - - .badge { - margin-right: 8px; - } - - .badges { - display: flex; - align-items: center; - } - - .share-icon { - margin-top: -4px; - padding-right: 8px; - } - - @media (max-width: 1024px) { - display: inline-flex; - height: auto; - - & > div { - margin-top: 0; - } - } -`; - -const okIcon = ( - -); - -const cancelIcon = ( - -); - -class FilesTileContent extends React.PureComponent { - constructor(props) { - super(props); - let titleWithoutExt = getTitleWithoutExst(props.item); - - if (props.fileAction.id === -1) { - titleWithoutExt = this.getDefaultName(props.fileAction.extension); - } - - this.state = { - itemTitle: titleWithoutExt, - editingId: props.fileAction.id, - showNewFilesPanel: false, - newFolderId: [], - newItems: props.item.new, - //loading: false - }; - } - - completeAction = (e) => { - //this.setState({ loading: false }, () =>) - this.props.onEditComplete(e); - }; - - updateItem = (e) => { - const { - fileAction, - updateFile, - renameFolder, - item, - setIsLoading, - } = this.props; - - const { itemTitle } = this.state; - const originalTitle = getTitleWithoutExst(item); - - setIsLoading(true); - if (originalTitle === itemTitle) return this.completeAction(e); - - item.fileExst - ? updateFile(fileAction.id, itemTitle) - .then(() => this.completeAction(e)) - .finally(() => setIsLoading(false)) - : renameFolder(fileAction.id, itemTitle) - .then(() => this.completeAction(e)) - .finally(() => setIsLoading(false)); - }; - - createItem = (e) => { - const { createFile, item, setIsLoading, createFolder, t } = this.props; - const { itemTitle } = this.state; - - setIsLoading(true); - - if (itemTitle.trim() === "") return this.completeAction(); - - !item.fileExst - ? createFolder(item.parentId, itemTitle) - .then(() => this.completeAction(e)) - .finally(() => { - toastr.success( - - New folder {{ itemTitle }} is created - - ); - return setIsLoading(false); - }) - : createFile(item.parentId, `${itemTitle}.${item.fileExst}`) - .then(() => this.completeAction(e)) - .finally(() => { - const exst = item.fileExst; - toastr.success( - - New file {{ itemTitle }}.{{ exst }} is created - - ); - return setIsLoading(false); - }); - }; - - componentDidUpdate(prevProps) { - const { fileAction } = this.props; - if (fileAction) { - if (fileAction.id !== prevProps.fileAction.id) { - this.setState({ editingId: fileAction.id }); - } - } - } - - renameTitle = (e) => { - this.setState({ itemTitle: e.target.value }); - }; - - cancelUpdateItem = (e) => { - //this.setState({ loading: false }); - this.completeAction(e); - }; - - onClickUpdateItem = () => { - this.props.fileAction.type === FileAction.Create - ? this.createItem() - : this.updateItem(); - }; - - onKeyUpUpdateItem = (e) => { - if (e.keyCode === 13) { - this.props.fileAction.type === FileAction.Create - ? this.createItem() - : this.updateItem(); - } - - if (e.keyCode === 27) return this.cancelUpdateItem(); - }; - - onFilesClick = () => { - const { id, fileExst, viewUrl, providerKey } = this.props.item; - const { - filter, - parentFolder, - setIsLoading, - onMediaFileClick, - fetchFiles, - canWebEdit, - openDocEditor, - isVideo, - isImage, - isSound, - expandedKeys, - addExpandedKeys, - } = this.props; - if (!fileExst) { - setIsLoading(true); - - if (!expandedKeys.includes(parentFolder + "")) { - addExpandedKeys(parentFolder + ""); - } - - fetchFiles(id, filter) - .catch((err) => { - toastr.error(err); - setIsLoading(false); - }) - .finally(() => setIsLoading(false)); - } else { - if (canWebEdit) { - return openDocEditor(id, providerKey); - } - - const isOpenMedia = isImage || isSound || isVideo; - - if (isOpenMedia) { - onMediaFileClick(id); - return; - } - - return window.open(viewUrl, "_blank"); - } - }; - - onMobileRowClick = (e) => { - if (!isMobile) return; - - this.onFilesClick(); - }; - - getStatusByDate = () => { - const { culture, t, item, sectionWidth } = this.props; - const { created, updated, version, fileExst } = item; - - const title = - version > 1 - ? t("TitleModified") - : fileExst - ? t("TitleUploaded") - : t("TitleCreated"); - - const date = fileExst ? updated : created; - const dateLabel = new Date(date).toLocaleString(culture); - const mobile = (sectionWidth && sectionWidth <= 375) || isMobile; - - return mobile ? dateLabel : `${title}: ${dateLabel}`; - }; - - getDefaultName = (format) => { - const { t } = this.props; - - switch (format) { - case "docx": - return t("NewDocument"); - case "xlsx": - return t("NewSpreadsheet"); - case "pptx": - return t("NewPresentation"); - default: - return t("NewFolder"); - } - }; - - onShowVersionHistory = (e) => { - const { homepage, history } = this.props; - const fileId = e.currentTarget.dataset.id; - - history.push( - combineUrl(AppServerConfig.proxyURL, homepage, `/${fileId}/history`) - ); - }; - - onBadgeClick = () => { - const { showNewFilesPanel } = this.state; - const { - item, - treeFolders, - setTreeFolders, - rootFolderId, - newItems, - filter, - fetchFiles, - } = this.props; - if (item.fileExst) { - markAsRead([], [item.id]) - .then(() => { - const data = treeFolders; - const dataItem = data.find((x) => x.id === rootFolderId); - dataItem.newItems = newItems ? dataItem.newItems - 1 : 0; //////newItems - setTreeFolders(data); - fetchFiles(this.props.selectedFolderId, filter.clone()); - }) - .catch((err) => toastr.error(err)); - } else { - const newFolderId = this.props.selectedFolderPathParts; - newFolderId.push(item.id); - this.setState({ - showNewFilesPanel: !showNewFilesPanel, - newFolderId, - }); - } - }; - - onShowNewFilesPanel = () => { - const { showNewFilesPanel } = this.state; - this.setState({ showNewFilesPanel: !showNewFilesPanel }); - }; - - render() { - const { item, fileAction, isTrashFolder, folders } = this.props; - const { - itemTitle, - editingId, - showNewFilesPanel, - newItems, - newFolderId, - } = this.state; - const { fileExst, id } = item; - - const titleWithoutExt = getTitleWithoutExst(item); - - const isEdit = id === editingId && fileExst === fileAction.extension; - const linkStyles = isTrashFolder - ? { noHover: true } - : { onClick: this.onFilesClick }; - const showNew = item.new && item.new > 0; - - return isEdit ? ( - - ) : ( - <> - {showNewFilesPanel && ( - - )} - - - {titleWithoutExt} - - <> - {fileExst ? ( -
- - {fileExst} - -
- ) : ( -
- {!!showNew && ( - - )} -
- )} - -
- - ); - } -} - -export default inject( - ( - { auth, filesStore, formatsStore, treeFoldersStore, selectedFolderStore }, - { item } - ) => { - const { culture } = auth.settingsStore; - const { - iconFormatsStore, - mediaViewersFormatsStore, - docserviceStore, - } = formatsStore; - const { - folders, - fetchFiles, - filter, - newRowItems, - createFile, - updateFile, - renameFolder, - createFolder, - setIsLoading, - isLoading, - dragging, - } = filesStore; - - const { - treeFolders, - setTreeFolders, - isRecycleBinFolder, - expandedKeys, - addExpandedKeys, - } = treeFoldersStore; - - const { type, extension, id } = filesStore.fileActionStore; - - const fileAction = { type, extension, id }; - - const canWebEdit = docserviceStore.canWebEdit(item.fileExst); - const isVideo = mediaViewersFormatsStore.isVideo(item.fileExst); - const isImage = iconFormatsStore.isImage(item.fileExst); - const isSound = iconFormatsStore.isSound(item.fileExst); - - return { - culture, - homepage: config.homepage, - fileAction, - folders, - rootFolderId: selectedFolderStore.pathParts, - selectedFolderId: selectedFolderStore.id, - selectedFolderPathParts: selectedFolderStore.pathParts, - newItems: selectedFolderStore.new, - parentFolder: selectedFolderStore.parentId, - isLoading, - treeFolders, - isTrashFolder: isRecycleBinFolder, - filter, - dragging, - canWebEdit, - isVideo, - isImage, - isSound, - newRowItems, - expandedKeys, - - setIsLoading, - fetchFiles, - setTreeFolders, - createFile, - createFolder, - updateFile, - renameFolder, - addExpandedKeys, - }; - } -)(withRouter(withTranslation("Home")(observer(FilesTileContent)))); diff --git a/products/ASC.Files/Client/src/components/panels/VersionHistoryPanel/index.js b/products/ASC.Files/Client/src/components/panels/VersionHistoryPanel/index.js index 01355c8f2a..250dd4d13f 100644 --- a/products/ASC.Files/Client/src/components/panels/VersionHistoryPanel/index.js +++ b/products/ASC.Files/Client/src/components/panels/VersionHistoryPanel/index.js @@ -12,7 +12,7 @@ import { StyledHeaderContent, StyledBody, } from "../StyledPanels"; -import { SectionBodyContent } from "../../pages/VersionHistory/Section/"; +import { SectionBodyContent } from "../../../pages/VersionHistory/Section/"; import { inject, observer } from "mobx-react"; import config from "../../../../package.json"; diff --git a/products/ASC.Files/Client/src/components/pages/Home/MediaViewer/index.js b/products/ASC.Files/Client/src/pages/Home/MediaViewer/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/MediaViewer/index.js rename to products/ASC.Files/Client/src/pages/Home/MediaViewer/index.js diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContainer.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContainer.js new file mode 100644 index 0000000000..96518e92ef --- /dev/null +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContainer.js @@ -0,0 +1,35 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import RowContainer from "@appserver/components/row-container"; +import { Consumer } from "@appserver/components/utils/context"; +import SimpleFilesRow from "./SimpleFilesRow"; + +const FilesRowContainer = ({ filesList }) => { + return ( + + {(context) => ( + + {filesList.map((item, index) => ( + + ))} + + )} + + ); +}; + +export default inject(({ filesStore }) => { + const { filesList } = filesStore; + + return { + filesList, + }; +})(observer(FilesRowContainer)); diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContent.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContent.js new file mode 100644 index 0000000000..84e2782144 --- /dev/null +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/FilesRowContent.js @@ -0,0 +1,164 @@ +import React from "react"; +import { withRouter } from "react-router"; +import { withTranslation } from "react-i18next"; +import styled from "styled-components"; +import { isMobile } from "react-device-detect"; + +import Link from "@appserver/components/link"; +import Text from "@appserver/components/text"; +import RowContent from "@appserver/components/row-content"; + +import withContent from "../../../../../HOCs/withContent"; +import withBadges from "../../../../../HOCs/withBadges"; + +const sideColor = "#A3A9AE"; + +const SimpleFilesRowContent = styled(RowContent)` + .badge-ext { + margin-left: -8px; + margin-right: 8px; + } + + .badge { + height: 14px; + width: 14px; + margin-right: 6px; + } + .lock-file { + cursor: pointer; + } + .badges { + display: flex; + align-items: center; + } + + .favorite { + cursor: pointer; + margin-right: 6px; + } + + .share-icon { + margin-top: -4px; + padding-right: 8px; + } + + .row_update-text { + overflow: hidden; + text-overflow: ellipsis; + } +`; + +const FilesRowContent = ({ + t, + item, + sectionWidth, + titleWithoutExt, + updatedDate, + fileOwner, + linkStyles, + isTrashFolder, + onFilesClick, + badgesComponent, +}) => { + const { + contentLength, + fileExst, + filesCount, + foldersCount, + providerKey, + } = item; + + const onMobileRowClick = () => { + if (isTrashFolder || window.innerWidth > 1024) return; + onFilesClick(); + }; + + return ( + <> + + + {titleWithoutExt} + + +
+ {fileExst ? ( + + {fileExst} + + ) : null} + {badgesComponent} +
+ + {fileOwner} + + + {(fileExst || contentLength || !providerKey) && + updatedDate && + updatedDate} + + + {fileExst || contentLength + ? contentLength + : !providerKey + ? `${t("TitleDocuments")}: ${filesCount} | ${t( + "TitleSubfolders" + )}: ${foldersCount}` + : ""} + +
+ + ); +}; + +export default withRouter( + withTranslation("Home")(withContent(withBadges(FilesRowContent))) +); diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/SimpleFilesRow.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/SimpleFilesRow.js new file mode 100644 index 0000000000..7c9fbcbb3d --- /dev/null +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/RowsView/SimpleFilesRow.js @@ -0,0 +1,107 @@ +import React from "react"; +import styled from "styled-components"; +import { withTranslation } from "react-i18next"; +import DragAndDrop from "@appserver/components/drag-and-drop"; +import Row from "@appserver/components/row"; +import FilesRowContent from "./FilesRowContent"; +import { withRouter } from "react-router-dom"; +import { createSelectable } from "react-selectable-fast"; + +import withFileActions from "../../../../../HOCs/withFileActions"; +import withContextOptions from "../../../../../HOCs/withContextOptions"; + +const StyledSimpleFilesRow = styled(Row)` + margin-top: -2px; + ${(props) => + !props.contextOptions && + ` + & > div:last-child { + width: 0px; + } + `} + + .share-button-icon { + margin-right: 7px; + margin-top: -1px; + } + + .share-button:hover, + .share-button-icon:hover { + cursor: pointer; + color: #657077; + path { + fill: #657077; + } + } + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + + @media (max-width: 1312px) { + .share-button { + padding-top: 3px; + } + } + + .styled-element { + margin-right: 7px; + } +`; + +const SimpleFilesRow = createSelectable((props) => { + const { + item, + sectionWidth, + dragging, + onContentRowSelect, + rowContextClick, + onDrop, + onMouseDown, + className, + isDragging, + value, + displayShareButton, + isPrivacy, + sharedButton, + contextOptionsProps, + checkedProps, + element, + onFilesClick, + } = props; + + return ( +
+ + + + + +
+ ); +}); + +export default withTranslation("Home")( + withFileActions(withContextOptions(withRouter(SimpleFilesRow))) +); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/BadgesFileTile.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/BadgesFileTile.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/BadgesFileTile.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/BadgesFileTile.js diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FileTile.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FileTile.js new file mode 100644 index 0000000000..868c7e560d --- /dev/null +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FileTile.js @@ -0,0 +1,64 @@ +import React from "react"; +import styled from "styled-components"; +import { withTranslation } from "react-i18next"; +import DragAndDrop from "@appserver/components/drag-and-drop"; + +import Tile from "./sub-components/Tile"; +import FilesTileContent from "./FilesTileContent"; +import { withRouter } from "react-router-dom"; +import { createSelectable } from "react-selectable-fast"; + +import withFileActions from "../hoc/withFileActions"; + +const FilesTile = createSelectable((props) => { + const { + item, + sectionWidth, + dragging, + onContentRowSelect, + rowContextClick, + onDrop, + onMouseDown, + className, + isDragging, + value, + displayShareButton, + isPrivacy, + sharedButton, + contextOptionsProps, + checkedProps, + element, + } = props; + + return ( +
+ + + + + +
+ ); +}); + +export default withTranslation("Home")(withFileActions(withRouter(FilesTile))); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesTile/FilesTileContainer.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FilesTileContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/FilesTile/FilesTileContainer.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FilesTileContainer.js diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FilesTileContent.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FilesTileContent.js new file mode 100644 index 0000000000..0fc01424cc --- /dev/null +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/FilesTileContent.js @@ -0,0 +1,153 @@ +import React from "react"; +import { withRouter } from "react-router"; +import { withTranslation } from "react-i18next"; +import styled from "styled-components"; + +import Link from "@appserver/components/link"; +import Text from "@appserver/components/text"; + +import TileContent from "./TileContent"; +import withContent from "../hoc/withContent"; +import Badges from "../sub-components/Badges"; + +const SimpleFilesTileContent = styled(TileContent)` + .rowMainContainer { + height: auto; + max-width: 100%; + align-self: flex-end; + + a { + word-break: break-word; + } + } + + .mainIcons { + align-self: flex-end; + } + + .badge-ext { + margin-left: -8px; + margin-right: 8px; + } + + .badge { + margin-right: 8px; + } + + .badges { + display: flex; + align-items: center; + } + + .share-icon { + margin-top: -4px; + padding-right: 8px; + } + + @media (max-width: 1024px) { + display: inline-flex; + height: auto; + + & > div { + margin-top: 0; + } + } +`; + +const FilesTileContent = ({ + t, + item, + sectionWidth, + titleWithoutExt, + updatedDate, + fileOwner, + accessToEdit, + linkStyles, + newItems, + showNew, + canWebEdit, + canConvert, + isTrashFolder, + onFilesClick, + onShowVersionHistory, + onBadgeClick, + onClickLock, + onClickFavorite, + setConvertDialogVisible, +}) => { + const { + contentLength, + fileExst, + filesCount, + foldersCount, + fileStatus, + id, + versionGroup, + locked, + providerKey, + } = item; + + const onMobileRowClick = () => { + if (isTrashFolder || window.innerWidth > 1024) return; + onFilesClick(); + }; + + return ( + <> + + + {titleWithoutExt} + {fileExst ? ( + + {fileExst} + + ) : null} + + +
+ +
+
+ + ); +}; + +export default withRouter( + withTranslation("Home")(withContent(FilesTileContent)) +); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/Tile.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/Tile.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/Tile.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/Tile.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/TileContainer.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/TileContainer.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/TileContainer.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/TileContent.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/TileContent.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/TileContent.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/TilesView/sub-components/TileContent.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js similarity index 81% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Body/index.js rename to products/ASC.Files/Client/src/pages/Home/Section/Body/index.js index 640d9d3f4a..282eeafba5 100644 --- a/products/ASC.Files/Client/src/components/pages/Home/Section/Body/index.js +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js @@ -1,25 +1,22 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { withRouter } from "react-router"; import { withTranslation } from "react-i18next"; -import Loaders from "@appserver/common/components/Loaders"; import { isMobile } from "react-device-detect"; import { observer, inject } from "mobx-react"; -import FilesRowContainer from "./FilesRow/FilesRowContainer"; -import FilesTileContainer from "./FilesTile/FilesTileContainer"; -import EmptyContainer from "./EmptyContainer"; +import FilesRowContainer from "./RowsView/FilesRowContainer"; +import FilesTileContainer from "./TilesView/FilesTileContainer"; +import EmptyContainer from "../../../../components/EmptyContainer"; + +import withLoader from "../../../../HOCs/withLoader"; let currentDroppable = null; -let loadTimeout = null; - const SectionBodyContent = (props) => { const { t, tReady, fileActionId, viewAs, - firstLoad, - isLoading, isEmptyFilesList, folderId, dragging, @@ -31,31 +28,6 @@ const SectionBodyContent = (props) => { moveDragItems, } = props; - const [inLoad, setInLoad] = useState(false); - - const cleanTimer = () => { - loadTimeout && clearTimeout(loadTimeout); - loadTimeout = null; - }; - - useEffect(() => { - if (isLoading) { - cleanTimer(); - loadTimeout = setTimeout(() => { - console.log("inLoad", true); - setInLoad(true); - }, 500); - } else { - cleanTimer(); - console.log("inLoad", false); - setInLoad(false); - } - - return () => { - cleanTimer(); - }; - }, [isLoading]); - useEffect(() => { const customScrollElm = document.querySelector( "#customScrollBar > .scroll-body" @@ -174,11 +146,7 @@ const SectionBodyContent = (props) => { //console.log("Files Home SectionBodyContent render", props); return (!fileActionId && isEmptyFilesList) || null ? ( - firstLoad || (isMobile && inLoad) ? ( - - ) : ( - - ) + ) : viewAs === "tile" ? ( ) : ( @@ -194,14 +162,12 @@ export default inject( filesActionsStore, }) => { const { - firstLoad, fileActionStore, filesList, dragging, setDragging, startDrag, setStartDrag, - isLoading, viewAs, setTooltipPosition, } = filesStore; @@ -209,9 +175,7 @@ export default inject( return { dragging, fileActionId: fileActionStore.id, - firstLoad, viewAs, - isLoading, isEmptyFilesList: filesList.length <= 0, setDragging, startDrag, @@ -222,4 +186,6 @@ export default inject( moveDragItems: filesActionsStore.moveDragItems, }; } -)(withRouter(withTranslation("Home")(observer(SectionBodyContent)))); +)( + withRouter(withTranslation("Home")(withLoader(observer(SectionBodyContent)))) +); diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Filter/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Filter/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Filter/index.js rename to products/ASC.Files/Client/src/pages/Home/Section/Filter/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Header/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Header/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Header/index.js rename to products/ASC.Files/Client/src/pages/Home/Section/Header/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/Paging/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Paging/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/Paging/index.js rename to products/ASC.Files/Client/src/pages/Home/Section/Paging/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/Section/index.js b/products/ASC.Files/Client/src/pages/Home/Section/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Home/Section/index.js rename to products/ASC.Files/Client/src/pages/Home/Section/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Home/index.js b/products/ASC.Files/Client/src/pages/Home/index.js similarity index 98% rename from products/ASC.Files/Client/src/components/pages/Home/index.js rename to products/ASC.Files/Client/src/pages/Home/index.js index e5fb22e8d3..4683c599c0 100644 --- a/products/ASC.Files/Client/src/components/pages/Home/index.js +++ b/products/ASC.Files/Client/src/pages/Home/index.js @@ -14,7 +14,7 @@ import { ArticleBodyContent, ArticleHeaderContent, ArticleMainButtonContent, -} from "../../Article"; +} from "../../components/Article"; import { SectionBodyContent, SectionFilterContent, @@ -22,11 +22,11 @@ import { SectionPagingContent, } from "./Section"; -import { ConvertDialog } from "../../dialogs"; +import { ConvertDialog } from "../../components/dialogs"; import MediaViewer from "./MediaViewer"; -import DragTooltip from "../../DragTooltip"; +import DragTooltip from "../../components/DragTooltip"; import { observer, inject } from "mobx-react"; -import config from "../../../../package.json"; +import config from "../../../package.json"; class PureHome extends React.Component { componentDidMount() { diff --git a/products/ASC.Files/Client/src/components/pages/Settings/Section/Body/ConnectedClouds.js b/products/ASC.Files/Client/src/pages/Settings/Section/Body/ConnectedClouds.js similarity index 90% rename from products/ASC.Files/Client/src/components/pages/Settings/Section/Body/ConnectedClouds.js rename to products/ASC.Files/Client/src/pages/Settings/Section/Body/ConnectedClouds.js index 37cffb9dba..f2d61d9a14 100644 --- a/products/ASC.Files/Client/src/components/pages/Settings/Section/Body/ConnectedClouds.js +++ b/products/ASC.Files/Client/src/pages/Settings/Section/Body/ConnectedClouds.js @@ -7,22 +7,22 @@ import Box from "@appserver/components/box"; import Row from "@appserver/components/row"; import RowContainer from "@appserver/components/row-container"; import { withTranslation } from "react-i18next"; -import EmptyFolderContainer from "../../../Home/Section/Body/EmptyContainer/EmptyContainer"; -import BoxIcon from "../../../../../../public/images/icon_box.react.svg"; -import DropBoxIcon from "../../../../../../public/images/icon_dropbox.react.svg"; -import GoogleDriveIcon from "../../../../../../public/images/icon_google_drive.react.svg"; -import KDriveIcon from "../../../../../../public/images/icon_kdrive.react.svg"; -import NextCloudIcon from "../../../../../../public/images/icon_nextcloud.react.svg"; -import OneDriveIcon from "../../../../../../public/images/icon_onedrive.react.svg"; -import OwnCloudIcon from "../../../../../../public/images/icon_owncloud.react.svg"; -import SharePointIcon from "../../../../../../public/images/icon_sharepoint.react.svg"; -import WebDavIcon from "../../../../../../public/images/icon_webdav.react.svg"; -import YandexDiskIcon from "../../../../../../public/images/icon_yandex_disk.react.svg"; +import EmptyFolderContainer from "../../../../components/EmptyContainer/EmptyContainer"; +import BoxIcon from "../../../../../public/images/icon_box.react.svg"; +import DropBoxIcon from "../../../../../public/images/icon_dropbox.react.svg"; +import GoogleDriveIcon from "../../../../../public/images/icon_google_drive.react.svg"; +import KDriveIcon from "../../../../../public/images/icon_kdrive.react.svg"; +import NextCloudIcon from "../../../../../public/images/icon_nextcloud.react.svg"; +import OneDriveIcon from "../../../../../public/images/icon_onedrive.react.svg"; +import OwnCloudIcon from "../../../../../public/images/icon_owncloud.react.svg"; +import SharePointIcon from "../../../../../public/images/icon_sharepoint.react.svg"; +import WebDavIcon from "../../../../../public/images/icon_webdav.react.svg"; +import YandexDiskIcon from "../../../../../public/images/icon_yandex_disk.react.svg"; import commonIconsStyles from "@appserver/components/utils/common-icons-style"; import { inject, observer } from "mobx-react"; import combineUrl from "@appserver/common/utils/combineUrl"; import AppServerConfig from "@appserver/common/constants/AppServerConfig"; -import config from "../../../../../../package.json"; +import config from "../../../../../package.json"; import { withRouter } from "react-router"; const StyledBoxIcon = styled(BoxIcon)` diff --git a/products/ASC.Files/Client/src/components/pages/Settings/Section/Body/index.js b/products/ASC.Files/Client/src/pages/Settings/Section/Body/index.js similarity index 98% rename from products/ASC.Files/Client/src/components/pages/Settings/Section/Body/index.js rename to products/ASC.Files/Client/src/pages/Settings/Section/Body/index.js index 6ee1b38286..9e31b08160 100644 --- a/products/ASC.Files/Client/src/components/pages/Settings/Section/Body/index.js +++ b/products/ASC.Files/Client/src/pages/Settings/Section/Body/index.js @@ -6,7 +6,7 @@ import Error403 from "studio/Error403"; import Error520 from "studio/Error520"; import ConnectClouds from "./ConnectedClouds"; import { inject, observer } from "mobx-react"; -import { loopTreeFolders } from "../../../../../helpers/files-helpers"; +import { loopTreeFolders } from "../../../../helpers/files-helpers"; const StyledSettings = styled.div` display: grid; diff --git a/products/ASC.Files/Client/src/components/pages/Settings/Section/Header/index.js b/products/ASC.Files/Client/src/pages/Settings/Section/Header/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Settings/Section/Header/index.js rename to products/ASC.Files/Client/src/pages/Settings/Section/Header/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Settings/Section/index.js b/products/ASC.Files/Client/src/pages/Settings/Section/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/Settings/Section/index.js rename to products/ASC.Files/Client/src/pages/Settings/Section/index.js diff --git a/products/ASC.Files/Client/src/components/pages/Settings/index.js b/products/ASC.Files/Client/src/pages/Settings/index.js similarity index 96% rename from products/ASC.Files/Client/src/components/pages/Settings/index.js rename to products/ASC.Files/Client/src/pages/Settings/index.js index b96270289c..21a3f1667f 100644 --- a/products/ASC.Files/Client/src/components/pages/Settings/index.js +++ b/products/ASC.Files/Client/src/pages/Settings/index.js @@ -7,10 +7,10 @@ import { ArticleHeaderContent, ArticleBodyContent, ArticleMainButtonContent, -} from "../../Article"; +} from "../../components/Article"; import { SectionHeaderContent, SectionBodyContent } from "./Section"; import { withTranslation } from "react-i18next"; -import { setDocumentTitle } from "../../../helpers/utils"; +import { setDocumentTitle } from "../../helpers/utils"; import { inject, observer } from "mobx-react"; const PureSettings = ({ diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/StyledVersionRow.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/StyledVersionRow.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/StyledVersionRow.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/StyledVersionRow.js diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/VersionBadge.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/VersionBadge.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/VersionBadge.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/VersionBadge.js diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/VersionRow.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/VersionRow.js similarity index 98% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/VersionRow.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/VersionRow.js index dc967ef89d..d7af931dae 100644 --- a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/VersionRow.js +++ b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/VersionRow.js @@ -10,7 +10,7 @@ import { withTranslation } from "react-i18next"; import { withRouter } from "react-router"; import VersionBadge from "./VersionBadge"; import StyledVersionRow from "./StyledVersionRow"; -import ExternalLinkIcon from "../../../../../../public/images/external.link.react.svg"; +import ExternalLinkIcon from "../../../../../public/images/external.link.react.svg"; import commonIconsStyles from "@appserver/components/utils/common-icons-style"; import { inject, observer } from "mobx-react"; diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/index.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Body/index.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/Body/index.js diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Header/index.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/Header/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/Header/index.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/Header/index.js diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/Section/index.js b/products/ASC.Files/Client/src/pages/VersionHistory/Section/index.js similarity index 100% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/Section/index.js rename to products/ASC.Files/Client/src/pages/VersionHistory/Section/index.js diff --git a/products/ASC.Files/Client/src/components/pages/VersionHistory/index.js b/products/ASC.Files/Client/src/pages/VersionHistory/index.js similarity index 98% rename from products/ASC.Files/Client/src/components/pages/VersionHistory/index.js rename to products/ASC.Files/Client/src/pages/VersionHistory/index.js index c7b55ce021..760d71fda8 100644 --- a/products/ASC.Files/Client/src/components/pages/VersionHistory/index.js +++ b/products/ASC.Files/Client/src/pages/VersionHistory/index.js @@ -8,7 +8,7 @@ import { ArticleHeaderContent, ArticleBodyContent, ArticleMainButtonContent, -} from "../../Article"; +} from "../../components/Article"; import { SectionHeaderContent, SectionBodyContent } from "./Section"; //import { setDocumentTitle } from "../../../helpers/utils"; import { inject, observer } from "mobx-react"; diff --git a/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs b/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs index 5df944c3d1..c9201245e1 100644 --- a/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs +++ b/products/ASC.Files/Core/Core/Dao/TeamlabDao/FolderDao.cs @@ -976,10 +976,12 @@ namespace ASC.Files.Core.Data }; FilesDbContext.AddOrUpdate(r => r.BunchObjects, toInsert); + FilesDbContext.SaveChanges(); + tx.Commit(); //Commit changes } - FilesDbContext.SaveChanges(); + return newFolderId; } diff --git a/products/ASC.People/Client/src/pages/Profile/Section/Body/index.js b/products/ASC.People/Client/src/pages/Profile/Section/Body/index.js index deeb6b2674..9287dd6acb 100644 --- a/products/ASC.People/Client/src/pages/Profile/Section/Body/index.js +++ b/products/ASC.People/Client/src/pages/Profile/Section/Body/index.js @@ -193,6 +193,7 @@ class SectionBodyContent extends React.PureComponent { const providerButtons = providers && providers.map((item) => { + if (!providersData[item.provider]) return; const { icon, label, iconOptions } = providersData[item.provider]; if (!icon || !label) return ; @@ -247,17 +248,21 @@ class SectionBodyContent extends React.PureComponent { return providerButtons; }; + oauthDataExists = () => { + const { providers } = this.props; + + let existProviders = 0; + providers && providers.length > 0; + providers.map((item) => { + if (!providersData[item.provider]) return; + existProviders++; + }); + + return !!existProviders; + }; + render() { - const { - profile, - cultures, - culture, - isAdmin, - viewer, - t, - isSelf, - providers, - } = this.props; + const { profile, cultures, culture, isAdmin, t, isSelf } = this.props; const contacts = profile.contacts && getUserContacts(profile.contacts); const role = getUserRole(profile); @@ -300,7 +305,7 @@ class SectionBodyContent extends React.PureComponent { culture={culture} /> - {isSelf && providers && providers.length > 0 && ( + {isSelf && this.oauthDataExists() && ( diff --git a/web/ASC.Web.Api/Controllers/SettingsController.cs b/web/ASC.Web.Api/Controllers/SettingsController.cs index 6f4f1ffb32..3b218ff60e 100644 --- a/web/ASC.Web.Api/Controllers/SettingsController.cs +++ b/web/ASC.Web.Api/Controllers/SettingsController.cs @@ -1372,21 +1372,54 @@ namespace ASC.Api.Settings return FirstTimeTenantSettings.SaveData(wizardModel); } + [Read("tfaapp")] + public IEnumerable GetTfaSettings() + { + var result = new List(); + + var SmsVisible = StudioSmsNotificationSettingsHelper.IsVisibleSettings(); + var SmsEnable = SmsVisible && SmsProviderManager.Enabled(); + var TfaVisible = TfaAppAuthSettings.IsVisibleSettings; + + if (SmsVisible) + { + result.Add(new TfaSettings + { + Enabled = StudioSmsNotificationSettingsHelper.Enable, + Id = "sms", + Title = Resource.ButtonSmsEnable, + Avaliable = SmsEnable + }); + } + + if (TfaVisible) + { + result.Add(new TfaSettings + { + Enabled = SettingsManager.Load().EnableSetting, + Id = "app", + Title = Resource.ButtonTfaAppEnable, + Avaliable = true + }); + } + + return result; + } [Update("tfaapp")] public bool TfaSettingsFromBody([FromBody]TfaModel model) { - return TfaSettings(model); + return TfaSettingsUpdate(model); } [Update("tfaapp")] [Consumes("application/x-www-form-urlencoded")] public bool TfaSettingsFromForm([FromForm] TfaModel model) { - return TfaSettings(model); + return TfaSettingsUpdate(model); } - private bool TfaSettings(TfaModel model) + private bool TfaSettingsUpdate(TfaModel model) { PermissionContext.DemandPermissions(SecutiryConstants.EditPortalSettings); diff --git a/web/ASC.Web.Api/Models/TfaSettings.cs b/web/ASC.Web.Api/Models/TfaSettings.cs new file mode 100644 index 0000000000..a5738aea0d --- /dev/null +++ b/web/ASC.Web.Api/Models/TfaSettings.cs @@ -0,0 +1,10 @@ +namespace ASC.Web.Api.Models +{ + public class TfaSettings + { + public string Id { get; set; } + public string Title { get; set; } + public bool Enabled { get; set; } + public bool Avaliable { get; set; } + } +} diff --git a/web/ASC.Web.Client/src/components/pages/Confirm/sub-components/createUser.js b/web/ASC.Web.Client/src/components/pages/Confirm/sub-components/createUser.js index 53b92daaa0..b4c8755c1e 100644 --- a/web/ASC.Web.Client/src/components/pages/Confirm/sub-components/createUser.js +++ b/web/ASC.Web.Client/src/components/pages/Confirm/sub-components/createUser.js @@ -195,10 +195,11 @@ class Confirm extends React.PureComponent { const providerButtons = providers && providers.map((item, index) => { + if (!providersData[item.provider]) return; const { icon, label, iconOptions, className } = providersData[ item.provider ]; - if (!icon) return; + if (item.provider === "Facebook") { facebookIndex = index; return; @@ -223,6 +224,19 @@ class Confirm extends React.PureComponent { return providerButtons; }; + oauthDataExists = () => { + const { providers } = this.props; + + let existProviders = 0; + providers && providers.length > 0; + providers.map((item) => { + if (!providersData[item.provider]) return; + existProviders++; + }); + + return !!existProviders; + }; + authCallback = (profile) => { const { t, defaultPage } = this.props; const { FirstName, LastName, EMail, Serialized } = profile; @@ -493,7 +507,7 @@ class Confirm extends React.PureComponent { onClick={this.onSubmit} /> - {providers && providers.length > 0 && ( + {this.oauthDataExists && ( {this.providerButtons()} diff --git a/web/ASC.Web.Core/PublicResources/Resource.Designer.cs b/web/ASC.Web.Core/PublicResources/Resource.Designer.cs index a50bb4c61d..c3cb8c1481 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.Designer.cs +++ b/web/ASC.Web.Core/PublicResources/Resource.Designer.cs @@ -105,6 +105,24 @@ namespace ASC.Web.Core.PublicResources { } } + /// + /// Looks up a localized string similar to By SMS. + /// + public static string ButtonSmsEnable { + get { + return ResourceManager.GetString("ButtonSmsEnable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to By authenticator app. + /// + public static string ButtonTfaAppEnable { + get { + return ResourceManager.GetString("ButtonTfaAppEnable", resourceCulture); + } + } + /// /// Looks up a localized string similar to A link to confirm the operation has been sent to :email (the email address of the portal owner).. /// diff --git a/web/ASC.Web.Core/PublicResources/Resource.de.resx b/web/ASC.Web.Core/PublicResources/Resource.de.resx index a7ce5ef24d..5b51f25a42 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.de.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.de.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Das Feld mit dem Prüfcode darf nicht leer sein @@ -73,6 +73,12 @@ Ihre Nachricht wurde erfolgreich gesendet. Der Portaladministrator wird Sie kontaktieren. + + Per SMS + + + Über die Authentifizierungs-App + Der Link zum Bestätigen der Operation wurde an :email verschickt (die E-Mail-Adresse des Portalbesitzers). diff --git a/web/ASC.Web.Core/PublicResources/Resource.es.resx b/web/ASC.Web.Core/PublicResources/Resource.es.resx index 0af7e35468..6733c1d541 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.es.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.es.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Campo de código de validación no puede estar vacío @@ -73,6 +73,12 @@ Su mensaje fue enviado con éxito. El administrador del portal se pondrá en contacto con usted. + + Por SMS + + + Con ayuda de la aplicación para autenticación + Un enlace para confirmar la operación ha sido enviada al :correo electrónico (la dirección del correo electrónico del propietario del portal). diff --git a/web/ASC.Web.Core/PublicResources/Resource.fr.resx b/web/ASC.Web.Core/PublicResources/Resource.fr.resx index f7b244108b..da2e892699 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.fr.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.fr.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Le champ du code de validation ne peut pas être vide @@ -73,6 +73,12 @@ Votre message a été envoyé avec succès. Vous serez contacté par l'administrateur du portail. + + par SMS + + + Par l'application authentificateur + Un lien pour confirmer l'opération a été envoyé à :e-mail (l'adresse e-mail du propriétaire du portail). diff --git a/web/ASC.Web.Core/PublicResources/Resource.it.resx b/web/ASC.Web.Core/PublicResources/Resource.it.resx index 649248d0d3..c051cfded0 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.it.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.it.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Il campo Valida codice non può essere vuoto @@ -73,6 +73,12 @@ Il tuo messaggio è stato inviato con successo. Sarai contattato dall'amministratore del portale. + + Tramite SMS + + + Con authenticator app + Un collegamento per confermare l'operazione è stato inviato all'indirizzo :email (l'indirizzo email del proprietario del portale). diff --git a/web/ASC.Web.Core/PublicResources/Resource.resx b/web/ASC.Web.Core/PublicResources/Resource.resx index d6a6f2aa69..bf9175aa54 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Validation code field cannot be empty @@ -73,6 +73,12 @@ Your message was successfully sent. You will be contacted by the portal administrator. + + By SMS + + + By authenticator app + A link to confirm the operation has been sent to :email (the email address of the portal owner). diff --git a/web/ASC.Web.Core/PublicResources/Resource.ru.resx b/web/ASC.Web.Core/PublicResources/Resource.ru.resx index 1ddb41e5c2..c7722cb4ee 100644 --- a/web/ASC.Web.Core/PublicResources/Resource.ru.resx +++ b/web/ASC.Web.Core/PublicResources/Resource.ru.resx @@ -53,10 +53,10 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.4.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.5.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Поле кода подтверждения не может быть пустым @@ -73,6 +73,12 @@ Ваше сообщение успешно отправлено. Администратор портала с Вами свяжется. + + С помощью SMS + + + С помощью приложения для аутентификации + Ссылка для подтверждения операции была отправлена на :email (адрес электронной почты владельца портала). diff --git a/web/ASC.Web.Login/src/Login.jsx b/web/ASC.Web.Login/src/Login.jsx index e80f2562b6..ae3dfa97e8 100644 --- a/web/ASC.Web.Login/src/Login.jsx +++ b/web/ASC.Web.Login/src/Login.jsx @@ -362,10 +362,12 @@ const Form = (props) => { const providerButtons = providers && providers.map((item, index) => { + if (!providersData[item.provider]) return; + const { icon, label, iconOptions, className } = providersData[ item.provider ]; - if (!icon) return; + if (item.provider === "Facebook") { facebookIndex = index; return; @@ -390,6 +392,17 @@ const Form = (props) => { return providerButtons; }; + const oauthDataExists = () => { + let existProviders = 0; + providers && providers.length > 0; + providers.map((item) => { + if (!providersData[item.provider]) return; + existProviders++; + }); + + return !!existProviders; + }; + //console.log("Login render"); return ( @@ -507,7 +520,7 @@ const Form = (props) => { )} - {providers && providers.length > 0 && ( + {oauthDataExists() && ( <>