diff --git a/products/ASC.Files/Client/src/Files.jsx b/products/ASC.Files/Client/src/Files.jsx index 0612756bb3..1522b28190 100644 --- a/products/ASC.Files/Client/src/Files.jsx +++ b/products/ASC.Files/Client/src/Files.jsx @@ -29,6 +29,7 @@ import { ArticleMainButtonContent, } from "./components/Article"; import FormGallery from "./pages/FormGallery"; +import GlobalEvents from "./components/GlobalEvents"; const { proxyURL } = AppServerConfig; const homepage = config.homepage; @@ -169,6 +170,7 @@ class FilesContent extends React.Component { return ( <> + diff --git a/products/ASC.Files/Client/src/HOCs/withContent.js b/products/ASC.Files/Client/src/HOCs/withContent.js index 0bcd29fbcb..43711145b2 100644 --- a/products/ASC.Files/Client/src/HOCs/withContent.js +++ b/products/ASC.Files/Client/src/HOCs/withContent.js @@ -21,384 +21,19 @@ export default function withContent(WrappedContent) { constructor(props) { super(props); - const { - item, - fileActionId, - fileActionExt, - fileActionTemplateId, - fromTemplate, - } = props; let titleWithoutExt = props.titleWithoutExt; - if ( - fileActionId === -1 && - item.id === fileActionId && - fileActionTemplateId === null && - !fromTemplate - ) { - titleWithoutExt = getDefaultFileName(fileActionExt); - } this.state = { itemTitle: titleWithoutExt }; } componentDidUpdate(prevProps) { - const { - fileActionId, - fileActionExt, - setIsUpdatingRowItem, - isUpdatingRowItem, - isEdit, - titleWithoutExt, - } = this.props; - if (fileActionId === -1 && fileActionExt !== prevProps.fileActionExt) { - const itemTitle = getDefaultFileName(fileActionExt); - this.setState({ itemTitle }); - } - if (fileActionId === null && prevProps.fileActionId !== fileActionId) { - isUpdatingRowItem && setIsUpdatingRowItem(false); - } + const { titleWithoutExt } = this.props; - if (!isEdit && titleWithoutExt !== this.state.itemTitle) { + if (titleWithoutExt !== this.state.itemTitle) { this.setState({ itemTitle: titleWithoutExt }); } } - 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, - addActiveItems, - clearActiveOperations, - } = this.props; - - const { itemTitle } = this.state; - const originalTitle = getTitleWithoutExst(item); - - setIsLoading(true); - let timerId; - - const isSameTitle = - originalTitle.trim() === itemTitle.trim() || itemTitle.trim() === ""; - - const isFile = item.fileExst || item.contentLength; - - if (isSameTitle) { - this.setState({ - itemTitle: originalTitle, - }); - return editCompleteAction(fileActionId, item, isSameTitle); - } else { - timerId = setTimeout(() => { - isFile ? addActiveItems([item.id]) : addActiveItems(null, [item.id]); - }, 500); - } - - isFile - ? updateFile(fileActionId, itemTitle) - .then(() => this.completeAction(fileActionId)) - .then(() => - toastr.success( - t("FileRenamed", { - oldTitle: item.title, - newTitle: itemTitle + item.fileExst, - }) - ) - ) - .catch((err) => { - toastr.error(err); - this.completeAction(fileActionId); - }) - .finally(() => { - clearTimeout(timerId); - timerId = null; - clearActiveOperations([item.id]); - - setIsLoading(false); - }) - : renameFolder(fileActionId, itemTitle) - .then(() => this.completeAction(fileActionId)) - .then(() => - toastr.success( - t("FolderRenamed", { - folderTitle: item.title, - newFoldedTitle: itemTitle, - }) - ) - ) - .catch((err) => { - toastr.error(err); - this.completeAction(fileActionId); - }) - .finally(() => { - clearTimeout(timerId); - timerId = null; - clearActiveOperations(null, [item.id]); - - setIsLoading(false); - }); - }; - - cancelUpdateItem = (e) => { - const { item } = this.props; - - const originalTitle = getTitleWithoutExst(item); - this.setState({ - itemTitle: originalTitle, - }); - - return this.completeAction(e); - }; - - onClickUpdateItem = (e, open = true) => { - const { - fileActionType, - setIsUpdatingRowItem, - addActiveItems, - item, - } = this.props; - - setIsUpdatingRowItem(true); - - if (fileActionType === FileAction.Create) { - !item.fileExst && !item.contentLength - ? addActiveItems(null, [item.id]) - : addActiveItems([item.id]); - this.createItem(e, open); - } else { - this.updateItem(e); - } - }; - - createItem = (e, open) => { - const { - createFile, - createFolder, - fileActionTemplateId, - isDesktop, - isPrivacy, - item, - openDocEditor, - replaceFileStream, - setEncryptionAccess, - setIsLoading, - t, - setConvertPasswordDialogVisible, - setFormCreationInfo, - setIsUpdatingRowItem, - clearActiveOperations, - addActiveItems, - fileCopyAs, - fromTemplate, - gallerySelected, - setCreatedItem, - } = this.props; - const { itemTitle } = this.state; - const { parentId, fileExst } = item; - - const isMakeFormFromFile = fileActionTemplateId ? true : false; - - let title = itemTitle; - - setIsLoading(true); - - const itemId = e.currentTarget.dataset.itemid; - - let createdFileId, createdFolderId; - - if (itemTitle.trim() === "") { - title = - fileActionTemplateId === null - ? getDefaultFileName(item.fileExst) - : getTitleWithoutExst(item); - - this.setState({ - itemTitle: title, - }); - } - - let tab = - !isDesktop && item.fileExst && open - ? window.open( - combineUrl( - AppServerConfig.proxyURL, - config.homepage, - "/doceditor" - ), - "_blank" - ) - : null; - - if (!item.fileExst && !item.contentLength) { - createFolder(item.parentId, title) - .then((folder) => { - createdFolderId = folder.id; - addActiveItems(null, [folder.id]); - setCreatedItem({ id: createdFolderId, type: "folder" }); - }) - .then(() => this.completeAction(itemId)) - .catch((e) => { - toastr.error(e); - this.completeAction(itemId); - }) - .finally(() => { - const folderIds = [+itemId]; - createdFolderId && folderIds.push(createdFolderId); - - setIsUpdatingRowItem(false); - clearActiveOperations(null, folderIds); - - return setIsLoading(false); - }); - } else { - if (isMakeFormFromFile) { - fileCopyAs( - fileActionTemplateId, - `${title}.${item.fileExst}`, - item.parentId - ) - .then((file) => { - createdFileId = file.id; - addActiveItems([file.id]); - - open && openDocEditor(file.id, file.providerKey, tab); - }) - .then(() => this.completeAction(itemId)) - .catch((err) => { - if (err.indexOf("password") == -1) { - toastr.error(err, t("Common:Warning")); - return; - } - - toastr.error( - t("Translations:FileProtected"), - t("Common:Warning") - ); - - setFormCreationInfo({ - newTitle: `${title}.${item.fileExst}`, - fromExst: ".docx", - toExst: item.fileExst, - open, - actionId: itemId, - fileInfo: { - id: fileActionTemplateId, - folderId: item.parentId, - fileExst: item.fileExst, - }, - }); - setConvertPasswordDialogVisible(true); - - open && openDocEditor(null, null, tab); - }) - .finally(() => { - const fileIds = [+itemId]; - createdFileId && fileIds.push(createdFileId); - - setIsUpdatingRowItem(false); - clearActiveOperations(fileIds); - - return setIsLoading(false); - }); - } else if (fromTemplate) { - createFile( - parentId, - `${itemTitle}.${fileExst}`, - undefined, - gallerySelected.id - ) - .then((file) => { - createdFileId = file.id; - setCreatedItem({ id: createdFileId, type: "file" }); - addActiveItems([file.id]); - - return open && openDocEditor(file.id, file.providerKey, tab); - }) - .then(() => this.completeAction(itemId)) - .catch((e) => { - toastr.error(e); - tab && tab.close(); - this.completeAction(itemId); - }) - .finally(() => { - const fileIds = [+itemId]; - createdFileId && fileIds.push(createdFileId); - - setIsUpdatingRowItem(false); - clearActiveOperations(fileIds); - - return setIsLoading(false); - }); - } else { - createFile(item.parentId, `${title}.${item.fileExst}`) - .then((file) => { - createdFileId = file.id; - setCreatedItem({ id: createdFileId, type: "file" }); - addActiveItems([file.id]); - - if (isPrivacy) { - return setEncryptionAccess(file).then((encryptedFile) => { - if (!encryptedFile) return Promise.resolve(); - toastr.info(t("Translations:EncryptedFileSaving")); - return replaceFileStream( - file.id, - encryptedFile, - true, - false - ).then( - () => open && openDocEditor(file.id, file.providerKey, tab) - ); - }); - } - return open && openDocEditor(file.id, file.providerKey, tab); - }) - .then(() => this.completeAction(itemId)) - .catch((e) => { - toastr.error(e); - tab && tab.close(); - this.completeAction(itemId); - }) - .finally(() => { - const fileIds = [+itemId]; - createdFileId && fileIds.push(createdFileId); - - setIsUpdatingRowItem(false); - clearActiveOperations(fileIds); - - return setIsLoading(false); - }); - } - } - }; - - renameTitle = (e) => { - const { t, folderFormValidation } = this.props; - - let title = e.target.value; - //const chars = '*+:"<>?|/'; TODO: think how to solve problem with interpolation escape values in i18n translate - - if (title.match(folderFormValidation)) { - toastr.warning(t("ContainsSpecCharacter")); - } - - title = title.replace(folderFormValidation, "_"); - - return this.setState({ itemTitle: title }); - }; - getStatusByDate = (create) => { const { culture, item, personal } = this.props; const { created, updated } = item; @@ -413,7 +48,6 @@ export default function withContent(WrappedContent) { }; render() { - const { itemTitle } = this.state; const { element, isDesktop, @@ -421,23 +55,13 @@ export default function withContent(WrappedContent) { item, onFilesClick, t, - viewAs, + viewer, - isUpdatingRowItem, - passwordEntryProcess, - isEdit, + titleWithoutExt, } = this.props; - const { - access, - createdBy, - fileExst, - fileStatus, - href, - icon, - id, - isFolder, - } = item; + + const { access, createdBy, fileExst, fileStatus, href, icon, id } = item; const updatedDate = this.getStatusByDate(false); const createdDate = this.getStatusByDate(true); @@ -462,27 +86,8 @@ export default function withContent(WrappedContent) { const newItems = item.new || (fileStatus & FileStatus.IsNew) === FileStatus.IsNew; const showNew = !!newItems; - const elementIcon = element ? ( - element - ) : ( - - ); - return isEdit ? ( - - ) : ( + return ( @@ -277,7 +270,7 @@ export default function withFileActions(WrappedFileItem) { selection, setTooltipPosition, setStartDrag, - fileActionStore, + getFolderInfo, viewAs, bufferSelection, @@ -290,14 +283,12 @@ export default function withFileActions(WrappedFileItem) { } = filesStore; const { startUpload } = uploadDataStore; - const { type, extension, id } = fileActionStore; const selectedItem = selection.find( (x) => x.id === item.id && x.fileExst === item.fileExst ); - const draggable = - !isRecycleBinFolder && selectedItem && selectedItem.id !== id; + const draggable = !isRecycleBinFolder && selectedItem; const isFolder = selectedItem ? false : !item.isFolder ? false : true; const canWebEdit = settingsStore.canWebEdit(item.fileExst); @@ -338,9 +329,7 @@ export default function withFileActions(WrappedFileItem) { setStartDrag, isFolder, allowShareIn: filesStore.canShare, - actionType: type, - actionExtension: extension, - actionId: id, + checked: !!selectedItem, //parentFolder: selectedFolderStore.parentId, setParentId: selectedFolderStore.setParentId, diff --git a/products/ASC.Files/Client/src/HOCs/withHotkeys.js b/products/ASC.Files/Client/src/HOCs/withHotkeys.js index a02810b616..8dd86ea7d3 100644 --- a/products/ASC.Files/Client/src/HOCs/withHotkeys.js +++ b/products/ASC.Files/Client/src/HOCs/withHotkeys.js @@ -2,6 +2,7 @@ import React, { useEffect } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { observer, inject } from "mobx-react"; import { FileAction } from "@appserver/common/constants"; +import { Events } from "../helpers/constants"; import toastr from "studio/toastr"; const withHotkeys = (Component) => { @@ -12,7 +13,6 @@ const withHotkeys = (Component) => { setSelected, viewAs, setViewAs, - setAction, setHotkeyPanelVisible, confirmDelete, setDeleteDialogVisible, @@ -65,6 +65,20 @@ const withHotkeys = (Component) => { const folderWithNoAction = isFavoritesFolder || isRecentFolder || isTrashFolder; + const onCreate = (extension) => { + if (folderWithNoAction) return; + const event = new Event(Events.CREATE); + + const payload = { + extension: extension, + id: -1, + }; + + event.payload = payload; + + window.dispatchEvent(event); + }; + useEffect(() => { window.addEventListener("keydown", onKeyDown); @@ -163,48 +177,28 @@ const withHotkeys = (Component) => { ); //Crete document - useHotkeys( - "Shift+d", - () => { - if (folderWithNoAction) return; - setAction({ type: FileAction.Create, extension: "docx", id: -1 }); - }, - - { ...hotkeysFilter, ...{ keyup: true } } - ); + useHotkeys("Shift+d", () => onCreate("docx"), { + ...hotkeysFilter, + ...{ keyup: true }, + }); //Crete spreadsheet - useHotkeys( - "Shift+s", - () => { - if (folderWithNoAction) return; - setAction({ type: FileAction.Create, extension: "xlsx", id: -1 }); - }, - - { ...hotkeysFilter, ...{ keyup: true } } - ); + useHotkeys("Shift+s", () => onCreate("xlsx"), { + ...hotkeysFilter, + ...{ keyup: true }, + }); //Crete presentation - useHotkeys( - "Shift+p", - () => { - if (folderWithNoAction) return; - setAction({ type: FileAction.Create, extension: "pptx", id: -1 }); - }, - - { ...hotkeysFilter, ...{ keyup: true } } - ); + useHotkeys("Shift+p", () => onCreate("pptx"), { + ...hotkeysFilter, + ...{ keyup: true }, + }); //Crete form template - useHotkeys( - "Shift+o", - () => { - if (folderWithNoAction) return; - setAction({ type: FileAction.Create, extension: "docxf", id: -1 }); - }, - - { ...hotkeysFilter, ...{ keyup: true } } - ); + useHotkeys("Shift+o", () => onCreate("docxf"), { + ...hotkeysFilter, + ...{ keyup: true }, + }); //Crete form template from file useHotkeys( @@ -218,14 +212,10 @@ const withHotkeys = (Component) => { ); //Crete folder - useHotkeys( - "Shift+f", - () => { - if (folderWithNoAction) return; - setAction({ type: FileAction.Create, id: -1 }); - }, - { ...hotkeysFilter, ...{ keyup: true } } - ); + useHotkeys("Shift+f", () => onCreate(null), { + ...hotkeysFilter, + ...{ keyup: true }, + }); //Delete selection useHotkeys( @@ -329,7 +319,6 @@ const withHotkeys = (Component) => { enabledHotkeys, selection, } = filesStore; - const { setAction } = fileActionStore; const { selectFile, @@ -376,7 +365,6 @@ const withHotkeys = (Component) => { setSelected, viewAs, setViewAs, - setAction, setHotkeyPanelVisible, setDeleteDialogVisible, diff --git a/products/ASC.Files/Client/src/HOCs/withQuickButtons.js b/products/ASC.Files/Client/src/HOCs/withQuickButtons.js index b59eb25668..f3d7e3cde5 100644 --- a/products/ASC.Files/Client/src/HOCs/withQuickButtons.js +++ b/products/ASC.Files/Client/src/HOCs/withQuickButtons.js @@ -67,8 +67,6 @@ export default function withQuickButtons(WrappedComponent) { isTrashFolder, isAdmin, showShare, - fileActionExt, - fileActionId, sectionWidth, viewAs, } = this.props; @@ -79,9 +77,7 @@ export default function withQuickButtons(WrappedComponent) { access === ShareAccessRights.FullAccess || access === ShareAccessRights.None; // TODO: fix access type for owner (now - None) - const isEdit = id === fileActionId && fileExst === fileActionExt; - - const quickButtonsComponent = !isEdit ? ( + const quickButtonsComponent = ( - ) : null; + ); return ( { const { @@ -47,11 +48,16 @@ const ArticleMainButtonContent = (props) => { const onCreate = React.useCallback( (e) => { const format = e.action || null; - setAction({ - type: FileAction.Create, + + const event = new Event(Events.CREATE); + + const payload = { extension: format, id: -1, - }); + }; + event.payload = payload; + + window.dispatchEvent(event); }, [setAction] ); @@ -286,13 +292,7 @@ export default inject( treeFoldersStore, selectedFolderStore, }) => { - const { - isLoaded, - firstLoad, - isLoading, - fileActionStore, - canCreate, - } = filesStore; + const { isLoaded, firstLoad, isLoading, canCreate } = filesStore; const { isPrivacyFolder, isFavoritesFolder, @@ -321,7 +321,6 @@ export default inject( isShareFolder, canCreate, - setAction: fileActionStore.setAction, startUpload, setSelectFileDialogVisible, diff --git a/products/ASC.Files/Client/src/components/EmptyContainer/index.js b/products/ASC.Files/Client/src/components/EmptyContainer/index.js index 13a18c2bf1..a6f2035ff0 100644 --- a/products/ASC.Files/Client/src/components/EmptyContainer/index.js +++ b/products/ASC.Files/Client/src/components/EmptyContainer/index.js @@ -5,6 +5,7 @@ import EmptyFilterContainer from "./EmptyFilterContainer"; import EmptyFolderContainer from "./EmptyFolderContainer"; import { FileAction } from "@appserver/common/constants"; import { isMobile } from "react-device-detect"; +import { Events } from "../../helpers/constants"; const linkStyles = { isHovered: true, @@ -16,7 +17,6 @@ const linkStyles = { const EmptyContainer = ({ isFiltered, - setAction, isPrivacyFolder, parentId, isEncryptionSupport, @@ -26,11 +26,16 @@ const EmptyContainer = ({ const onCreate = (e) => { const format = e.currentTarget.dataset.format || null; - setAction({ - type: FileAction.Create, + + const event = new Event(Events.CREATE); + + const payload = { extension: format, id: -1, - }); + }; + event.payload = payload; + + window.dispatchEvent(event); }; return isFiltered ? ( @@ -59,7 +64,6 @@ export default inject( isEncryptionSupport: auth.settingsStore.isEncryptionSupport, theme: auth.settingsStore.theme, isFiltered, - setAction: filesStore.fileActionStore.setAction, isPrivacyFolder, parentId: selectedFolderStore.parentId, }; diff --git a/products/ASC.Files/Client/src/components/GlobalEvents/CreateEvent.js b/products/ASC.Files/Client/src/components/GlobalEvents/CreateEvent.js new file mode 100644 index 0000000000..7b17e62573 --- /dev/null +++ b/products/ASC.Files/Client/src/components/GlobalEvents/CreateEvent.js @@ -0,0 +1,312 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { useTranslation } from "react-i18next"; + +import toastr from "studio/toastr"; + +import { AppServerConfig } from "@appserver/common/constants"; +import { combineUrl } from "@appserver/common/utils"; + +import config from "../../../package.json"; + +import { getTitleWithoutExst } from "../../helpers/files-helpers"; +import { getDefaultFileName } from "../../helpers/utils"; + +import Dialog from "./sub-components/Dialog"; + +const CreateEvent = ({ + id, + type, + extension, + title, + templateId, + fromTemplate, + onClose, + + setIsLoading, + createFile, + createFolder, + addActiveItems, + openDocEditor, + setIsUpdatingRowItem, + gallerySelected, + setCreatedItem, + + parentId, + + isPrivacy, + isDesktop, + editCompleteAction, + + clearActiveOperations, + fileCopyAs, + + setConvertPasswordDialogVisible, + setFormCreationInfo, + + replaceFileStream, + setEncryptionAccess, +}) => { + const [visible, setVisible] = React.useState(false); + const [headerTitle, setHeaderTitle] = React.useState(null); + const [startValue, setStartValue] = React.useState(""); + + const { t } = useTranslation(["Translations", "Common"]); + + React.useEffect(() => { + const defaultName = getDefaultFileName(extension); + + if (title) { + const item = { fileExst: extension, title: title }; + + setStartValue(getTitleWithoutExst(item, fromTemplate)); + } else { + setStartValue(defaultName); + } + + setHeaderTitle(defaultName); + setVisible(true); + }, [extension, title, fromTemplate]); + + const onSave = (e, value, open = true) => { + let item; + let createdFileId, createdFolderId; + + const isMakeFormFromFile = templateId ? true : false; + + setIsLoading(true); + + const newValue = value; + + if (value.trim() === "") { + newValue = + templateId === null + ? getDefaultFileName(extension) + : getTitleWithoutExst({ fileExst: extension }); + + setStartValue(newValue); + } + + let tab = + !isDesktop && extension && open + ? window.open( + combineUrl(AppServerConfig.proxyURL, config.homepage, "/doceditor"), + "_blank" + ) + : null; + + if (!extension) { + createFolder(parentId, newValue) + .then((folder) => { + item = folder; + createdFolderId = folder.id; + addActiveItems(null, [folder.id]); + setCreatedItem({ id: createdFolderId, type: "folder" }); + }) + .then(() => editCompleteAction(id, item, false, type)) + .catch((e) => toastr.error(e)) + .finally(() => { + const folderIds = [+id]; + createdFolderId && folderIds.push(createdFolderId); + + clearActiveOperations(null, folderIds); + onClose(); + return setIsLoading(false); + }); + } else { + if (isMakeFormFromFile) { + fileCopyAs(templateId, `${newValue}.${extension}`, parentId) + .then((file) => { + item = file; + createdFileId = file.id; + addActiveItems([file.id]); + + open && openDocEditor(file.id, file.providerKey, tab); + }) + .then(() => editCompleteAction(id, item, false, type)) + .catch((err) => { + if (err.indexOf("password") == -1) { + toastr.error(err, t("Common:Warning")); + return; + } + + toastr.error(t("Translations:FileProtected"), t("Common:Warning")); + + setVisible(false); + + setFormCreationInfo({ + newTitle: `${newValue}.${extension}`, + fromExst: ".docx", + toExst: extension, + open, + actionId: id, + fileInfo: { + id: templateId, + folderId: parentId, + fileExst: extension, + }, + }); + setConvertPasswordDialogVisible(true); + + open && openDocEditor(null, null, tab); + }) + .finally(() => { + const fileIds = [+id]; + createdFileId && fileIds.push(createdFileId); + + clearActiveOperations(fileIds); + onClose(); + return setIsLoading(false); + }); + } else if (fromTemplate) { + createFile( + parentId, + `${newValue}.${extension}`, + undefined, + gallerySelected.id + ) + .then((file) => { + item = file; + createdFileId = file.id; + setCreatedItem({ id: createdFileId, type: "file" }); + addActiveItems([file.id]); + + return open && openDocEditor(file.id, file.providerKey, tab); + }) + .then(() => editCompleteAction(id, item, false, type)) + .catch((e) => toastr.error(e)) + .finally(() => { + const fileIds = [+id]; + createdFileId && fileIds.push(createdFileId); + + clearActiveOperations(fileIds); + onClose(); + return setIsLoading(false); + }); + } else { + createFile(parentId, `${newValue}.${extension}`) + .then((file) => { + createdFileId = file.id; + item = file; + setCreatedItem({ id: createdFileId, type: "file" }); + addActiveItems([file.id]); + + if (isPrivacy) { + return setEncryptionAccess(file).then((encryptedFile) => { + if (!encryptedFile) return Promise.resolve(); + toastr.info(t("Translations:EncryptedFileSaving")); + + return replaceFileStream( + file.id, + encryptedFile, + true, + false + ).then( + () => open && openDocEditor(file.id, file.providerKey, tab) + ); + }); + } + + return open && openDocEditor(file.id, file.providerKey, tab); + }) + .then(() => editCompleteAction(id, item, false, type)) + .catch((e) => toastr.error(e)) + .finally(() => { + const fileIds = [+id]; + createdFileId && fileIds.push(createdFileId); + + clearActiveOperations(fileIds); + onClose(); + return setIsLoading(false); + }); + } + } + }; + + const onCancel = React.useCallback( + (e) => { + onClose && onClose(); + }, + [onClose] + ); + + return ( + + ); +}; + +export default inject( + ({ + auth, + filesStore, + filesActionsStore, + selectedFolderStore, + treeFoldersStore, + uploadDataStore, + dialogsStore, + }) => { + const { + setIsLoading, + createFile, + createFolder, + addActiveItems, + openDocEditor, + setIsUpdatingRowItem, + gallerySelected, + setCreatedItem, + } = filesStore; + + const { editCompleteAction } = filesActionsStore; + + const { clearActiveOperations, fileCopyAs } = uploadDataStore; + + const { isRecycleBinFolder, isPrivacyFolder } = treeFoldersStore; + + const { id: parentId } = selectedFolderStore; + + const { replaceFileStream, setEncryptionAccess } = auth; + + const { isDesktopClient } = auth.settingsStore; + + const { + setConvertPasswordDialogVisible, + + setFormCreationInfo, + } = dialogsStore; + + return { + setIsLoading, + createFile, + createFolder, + addActiveItems, + openDocEditor, + setIsUpdatingRowItem, + gallerySelected, + setCreatedItem, + + parentId, + + isDesktop: isDesktopClient, + isPrivacy: isPrivacyFolder, + isTrashFolder: isRecycleBinFolder, + editCompleteAction, + + clearActiveOperations, + fileCopyAs, + + setConvertPasswordDialogVisible, + setFormCreationInfo, + + replaceFileStream, + setEncryptionAccess, + }; + } +)(observer(CreateEvent)); diff --git a/products/ASC.Files/Client/src/components/GlobalEvents/RenameEvent.js b/products/ASC.Files/Client/src/components/GlobalEvents/RenameEvent.js new file mode 100644 index 0000000000..46bab7b525 --- /dev/null +++ b/products/ASC.Files/Client/src/components/GlobalEvents/RenameEvent.js @@ -0,0 +1,142 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import { useTranslation } from "react-i18next"; + +import toastr from "studio/toastr"; + +import { getTitleWithoutExst } from "../../helpers/files-helpers"; + +import Dialog from "./sub-components/Dialog"; + +const RenameEvent = ({ + type, + item, + onClose, + + setIsLoading, + addActiveItems, + + updateFile, + renameFolder, + + editCompleteAction, + clearActiveOperations, +}) => { + const [visible, setVisible] = React.useState(false); + + const [startValue, setStartValue] = React.useState(""); + + const { t } = useTranslation(["Home"]); + + React.useEffect(() => { + setStartValue(getTitleWithoutExst(item, false)); + + setVisible(true); + }, [item]); + + const onUpdate = React.useCallback((e, value) => { + const originalTitle = getTitleWithoutExst(item); + + setIsLoading(true); + let timerId; + + const isSameTitle = + originalTitle.trim() === value.trim() || value.trim() === ""; + + const isFile = item.fileExst || item.contentLength; + + if (isSameTitle) { + setStartValue(originalTitle); + + return editCompleteAction(item.id, item, isSameTitle, type); + } else { + timerId = setTimeout(() => { + isFile ? addActiveItems([item.id]) : addActiveItems(null, [item.id]); + }, 500); + } + + isFile + ? updateFile(item.id, value) + .then(() => editCompleteAction(item.id, item, false, type)) + .then(() => + toastr.success( + t("FileRenamed", { + oldTitle: item.title, + newTitle: value + item.fileExst, + }) + ) + ) + .catch((err) => { + toastr.error(err); + editCompleteAction(item.id, item, false, type); + }) + .finally(() => { + clearTimeout(timerId); + timerId = null; + clearActiveOperations([item.id]); + + setIsLoading(false); + onClose(); + }) + : renameFolder(item.id, value) + .then(() => editCompleteAction(item.id, item, false, type)) + .then(() => + toastr.success( + t("FolderRenamed", { + folderTitle: item.title, + newFoldedTitle: value, + }) + ) + ) + .catch((err) => { + toastr.error(err); + editCompleteAction(item.id, item, false, type); + }) + .finally(() => { + clearTimeout(timerId); + timerId = null; + clearActiveOperations(null, [item.id]); + + setIsLoading(false); + onClose(); + }); + }, []); + + const onCancel = React.useCallback( + (e) => { + onClose && onClose(); + }, + [onClose] + ); + + return ( + + ); +}; + +export default inject(({ filesStore, filesActionsStore, uploadDataStore }) => { + const { setIsLoading, addActiveItems, updateFile, renameFolder } = filesStore; + + const { editCompleteAction } = filesActionsStore; + + const { clearActiveOperations } = uploadDataStore; + + return { + setIsLoading, + addActiveItems, + updateFile, + renameFolder, + + editCompleteAction, + + clearActiveOperations, + }; +})(observer(RenameEvent)); diff --git a/products/ASC.Files/Client/src/components/GlobalEvents/index.js b/products/ASC.Files/Client/src/components/GlobalEvents/index.js new file mode 100644 index 0000000000..d45d38a8b2 --- /dev/null +++ b/products/ASC.Files/Client/src/components/GlobalEvents/index.js @@ -0,0 +1,93 @@ +import React from "react"; + +import { FileAction } from "@appserver/common/constants"; + +import { Events } from "../../helpers/constants"; + +import CreateEvent from "./CreateEvent"; +import RenameEvent from "./RenameEvent"; + +const GlobalEvents = () => { + const [createDialogProps, setCreateDialogProps] = React.useState({ + visible: false, + id: null, + type: null, + extension: null, + title: "", + templateId: null, + fromTemplate: null, + onClose: null, + }); + + const [renameDialogProps, setRenameDialogProps] = React.useState({ + visible: false, + item: null, + onClose: null, + }); + + const onCreate = React.useCallback((e) => { + const { payload } = e; + + const visible = payload.id ? true : false; + + setCreateDialogProps({ + visible: visible, + id: payload.id, + type: FileAction.Create, + extension: payload.extension, + title: payload.title || null, + templateId: payload.templateId || null, + fromTemplate: payload.fromTemplate || null, + onClose: () => { + setCreateDialogProps({ + visible: false, + id: null, + type: null, + extension: null, + title: "", + templateId: null, + fromTemplate: null, + onClose: null, + }); + }, + }); + }, []); + + const onRename = React.useCallback((e) => { + const visible = e.item ? true : false; + + setRenameDialogProps({ + visible: visible, + type: FileAction.Rename, + item: e.item, + onClose: () => { + setRenameDialogProps({ + visible: false, + typ: null, + item: null, + }); + }, + }); + }, []); + + React.useEffect(() => { + window.addEventListener(Events.CREATE, onCreate); + window.addEventListener(Events.RENAME, onRename); + + return () => { + window.removeEventListener(Events.CREATE, onCreate); + window.removeEventListener(Events.RENAME, onRename); + }; + }, [onRename, onCreate]); + + return [ + createDialogProps.visible && ( + + ), + renameDialogProps.visible && ( + + ), + ]; +}; + +export default React.memo(GlobalEvents); diff --git a/products/ASC.Files/Client/src/components/GlobalEvents/sub-components/Dialog.js b/products/ASC.Files/Client/src/components/GlobalEvents/sub-components/Dialog.js new file mode 100644 index 0000000000..132e49191b --- /dev/null +++ b/products/ASC.Files/Client/src/components/GlobalEvents/sub-components/Dialog.js @@ -0,0 +1,120 @@ +import React from "react"; +import { inject, observer } from "mobx-react"; +import styled from "styled-components"; + +import toastr from "@appserver/components/toast/toastr"; +import ModalDialog from "@appserver/components/modal-dialog"; +import TextInput from "@appserver/components/text-input"; +import SaveCancelButtons from "@appserver/components/save-cancel-buttons"; + +const StyledModalDialog = styled(ModalDialog)` + width: 400px; + + @media (max-width: 400px) { + width: 100%; + } +`; + +const StyledSaveCancelButtons = styled(SaveCancelButtons)` + position: relative !important; + + padding: 8px 0 0; + + .buttons-flex { + width: 100%; + + display: grid; + align-items: center; + grid-template-columns: 1fr 1fr; + grid-gap: 8px; + } + + button { + width: 100%; + } +`; + +const Dialog = ({ + t, + title, + startValue, + visible, + folderFormValidation, + onSave, + onCancel, + onClose, +}) => { + const [value, setValue] = React.useState(""); + const [isDisabled, setIsDisabled] = React.useState(false); + + React.useEffect(() => { + if (startValue) setValue(startValue); + }, [startValue]); + + const onChange = React.useCallback((e) => { + let newValue = e.target.value; + + if (newValue.match(folderFormValidation)) { + toastr.warning(t("ContainsSpecCharacter")); + } + + newValue = newValue.replace(folderFormValidation, "_"); + + setValue(newValue); + }, []); + + const onFocus = React.useCallback((e) => { + e.target.select(); + }, []); + + const onSaveAction = React.useCallback( + (e) => { + setIsDisabled(true); + onSave && onSave(e, value); + }, + [onSave, value] + ); + + const onCancelAction = React.useCallback((e) => { + onCancel && onCancel(e); + }, []); + + return ( + + {title} + + + + + + + + ); +}; + +export default inject(({ auth }) => { + const { folderFormValidation } = auth.settingsStore; + + return { folderFormValidation }; +})(observer(Dialog)); diff --git a/products/ASC.Files/Client/src/components/ItemIcon.js b/products/ASC.Files/Client/src/components/ItemIcon.js index 173e052e27..1d97c5918a 100644 --- a/products/ASC.Files/Client/src/components/ItemIcon.js +++ b/products/ASC.Files/Client/src/components/ItemIcon.js @@ -23,35 +23,42 @@ const ItemIcon = ({ fileExst, isPrivacy, viewAs, - actionType, - actionExtension, - actionId, + // actionType, + // actionExtension, + // actionId, }) => { - const isEdit = - (actionType !== null && actionId === id && fileExst === actionExtension) || - id <= 0; + // const isEdit = + // (actionType !== null && actionId === id && fileExst === actionExtension) || + // id <= 0; + + // return ( + // <> + // + // {isPrivacy && fileExst && ( + // + // )} + // + // ); return ( <> - - {isPrivacy && fileExst && ( - - )} + + {isPrivacy && fileExst && } ); }; export default inject(({ filesStore, treeFoldersStore }) => { - const { type, extension, id } = filesStore.fileActionStore; + // const { type, extension, id } = filesStore.fileActionStore; return { viewAs: filesStore.viewAs, isPrivacy: treeFoldersStore.isPrivacyFolder, - actionType: type, - actionExtension: extension, - actionId: id, + // actionType: type, + // actionExtension: extension, + // actionId: id, }; })(observer(ItemIcon)); diff --git a/products/ASC.Files/Client/src/helpers/constants.js b/products/ASC.Files/Client/src/helpers/constants.js index f0668ad30a..40aba5bb72 100644 --- a/products/ASC.Files/Client/src/helpers/constants.js +++ b/products/ASC.Files/Client/src/helpers/constants.js @@ -10,3 +10,5 @@ export const thumbnailStatuses = { }; export const ADS_TIMEOUT = 300000; // 5 min + +export const Events = Object.freeze({ CREATE: "create", RENAME: "rename" }); diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js index f3e38405a5..b52a6f84c8 100644 --- a/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js +++ b/products/ASC.Files/Client/src/pages/Home/Section/Body/index.js @@ -20,7 +20,6 @@ const SectionBodyContent = (props) => { const { t, tReady, - fileActionId, isEmptyFilesList, folderId, dragging, @@ -243,7 +242,7 @@ const SectionBodyContent = (props) => { return ( {(context) => - (!fileActionId && isEmptyFilesList) || null ? ( + isEmptyFilesList || null ? ( <> @@ -277,7 +276,6 @@ export default inject( uploadDataStore, }) => { const { - fileActionStore, isEmptyFilesList, dragging, setDragging, @@ -299,7 +297,7 @@ export default inject( dragging, startDrag, setStartDrag, - fileActionId: fileActionStore.id, + isEmptyFilesList, setDragging, folderId: selectedFolderStore.id, diff --git a/products/ASC.Files/Client/src/pages/Home/Section/Header/index.js b/products/ASC.Files/Client/src/pages/Home/Section/Header/index.js index b1cc63fa15..365c4e9af5 100644 --- a/products/ASC.Files/Client/src/pages/Home/Section/Header/index.js +++ b/products/ASC.Files/Client/src/pages/Home/Section/Header/index.js @@ -13,6 +13,7 @@ import { Consumer } from "@appserver/components/utils/context"; import { inject, observer } from "mobx-react"; import TableGroupMenu from "@appserver/components/table-container/TableGroupMenu"; import Navigation from "@appserver/common/components/Navigation"; +import { Events } from "../../../../helpers/constants"; import config from "../../../../../package.json"; import { combineUrl } from "@appserver/common/utils"; @@ -49,11 +50,16 @@ class SectionHeaderContent extends React.Component { } onCreate = (format) => { - this.props.setAction({ - type: FileAction.Create, + const event = new Event(Events.CREATE); + + const payload = { extension: format, id: -1, - }); + }; + + event.payload = payload; + + window.dispatchEvent(event); }; createDocument = () => this.onCreate("docx"); @@ -445,7 +451,7 @@ export default inject( const { setSelected, setSelection, - fileActionStore, + canCreate, isHeaderVisible, isHeaderIndeterminate, @@ -462,7 +468,7 @@ export default inject( activeFiles, activeFolders, } = filesStore; - const { setAction } = fileActionStore; + const { setSharingPanelVisible, setMoveToPanelVisible, @@ -509,7 +515,7 @@ export default inject( setSelected, setSelection, - setAction, + setSharingPanelVisible, setMoveToPanelVisible, setCopyPanelVisible, diff --git a/products/ASC.Files/Client/src/pages/Home/index.js b/products/ASC.Files/Client/src/pages/Home/index.js index a18115f55b..64688a36c5 100644 --- a/products/ASC.Files/Client/src/pages/Home/index.js +++ b/products/ASC.Files/Client/src/pages/Home/index.js @@ -32,7 +32,7 @@ import DragTooltip from "../../components/DragTooltip"; import { observer, inject } from "mobx-react"; import config from "../../../package.json"; import { Consumer } from "@appserver/components/utils/context"; -import { FileAction } from "@appserver/common/constants"; +import { Events } from "../../helpers/constants"; class PureHome extends React.Component { componentDidMount() { @@ -48,7 +48,6 @@ class PureHome extends React.Component { isMediaOrImage, getFileInfo, gallerySelected, - setAction, setIsUpdatingRowItem, } = this.props; @@ -169,13 +168,19 @@ class PureHome extends React.Component { .then(() => { if (gallerySelected) { setIsUpdatingRowItem(false); - setAction({ - type: FileAction.Create, + + const event = new Event(Events.CREATE); + + const payload = { extension: "docxf", + id: -1, fromTemplate: true, title: gallerySelected.attributes.name_form, - id: -1, - }); + }; + + event.payload = payload; + + window.dispatchEvent(event); } }) .finally(() => { @@ -290,7 +295,7 @@ class PureHome extends React.Component { //console.log("Home render"); const { viewAs, - fileActionId, + firstLoad, isHeaderVisible, isPrivacyFolder, @@ -337,9 +342,7 @@ class PureHome extends React.Component { clearUploadedFilesHistory={clearUploadedFilesHistory} viewAs={viewAs} hideAside={ - !!fileActionId || - primaryProgressDataVisible || - secondaryProgressDataStoreVisible //TODO: use hideArticle action + primaryProgressDataVisible || secondaryProgressDataStoreVisible //TODO: use hideArticle action } isLoaded={!firstLoad} isHeaderVisible={isHeaderVisible} @@ -412,7 +415,6 @@ export default inject( firstLoad, setFirstLoad, fetchFiles, - fileActionStore, selection, setSelections, dragging, @@ -425,7 +427,6 @@ export default inject( setIsUpdatingRowItem, } = filesStore; - const { id, setAction } = fileActionStore; const { isRecycleBinFolder, isPrivacyFolder, @@ -478,7 +479,6 @@ export default inject( homepage: config.homepage, firstLoad, dragging, - fileActionId: id, viewAs, uploaded, converted, @@ -527,7 +527,6 @@ export default inject( isMediaOrImage: settingsStore.isMediaOrImage, getFileInfo, gallerySelected, - setAction, setIsUpdatingRowItem, }; } diff --git a/products/ASC.Files/Client/src/store/ContextOptionsStore.js b/products/ASC.Files/Client/src/store/ContextOptionsStore.js index ec016d86b7..5086c24edf 100644 --- a/products/ASC.Files/Client/src/store/ContextOptionsStore.js +++ b/products/ASC.Files/Client/src/store/ContextOptionsStore.js @@ -10,6 +10,7 @@ import { isMobile as isMobileUtils, isTablet as isTabletUtils, } from "@appserver/components/utils/device"; +import { Events } from "../helpers/constants"; class ContextOptionsStore { authStore; @@ -246,12 +247,11 @@ class ContextOptionsStore { }; onClickRename = (item) => { - const { id, fileExst } = item; - this.filesStore.fileActionStore.setAction({ - type: FileAction.Rename, - extension: fileExst, - id, - }); + const event = new Event(Events.RENAME); + + event.item = item; + + window.dispatchEvent(event); }; onChangeThirdPartyInfo = (providerKey) => { @@ -793,13 +793,11 @@ class ContextOptionsStore { getModel = (item, t) => { const { selection } = this.filesStore; - const { type, id, extension } = this.filesStore.fileActionStore; + const { fileExst, contextOptions } = item; - const isEdit = !!type && id === item.id && fileExst === extension; - const contextOptionsProps = - !isEdit && contextOptions && contextOptions.length > 0 + contextOptions && contextOptions.length > 0 ? selection.length > 1 ? this.getGroupContextOptions(t) : this.getFilesContextOptions(item, t) diff --git a/products/ASC.Files/Client/src/store/DialogsStore.js b/products/ASC.Files/Client/src/store/DialogsStore.js index 71c084af39..7f511aa5bb 100644 --- a/products/ASC.Files/Client/src/store/DialogsStore.js +++ b/products/ASC.Files/Client/src/store/DialogsStore.js @@ -1,6 +1,7 @@ import { getNewFiles } from "@appserver/common/api/files"; import { FileAction } from "@appserver/common/constants"; import { makeAutoObservable } from "mobx"; +import { Events } from "../helpers/constants"; class DialogsStore { authStore; @@ -212,18 +213,21 @@ class DialogsStore { }; createMasterForm = async (fileInfo) => { - const { setAction } = this.filesStore.fileActionStore; - let newTitle = fileInfo.title; newTitle = newTitle.substring(0, newTitle.lastIndexOf(".")); - setAction({ - type: FileAction.Create, + const event = new Event(Events.CREATE); + + const payload = { extension: "docxf", id: -1, title: `${newTitle}.docxf`, templateId: fileInfo.id, - }); + }; + + event.payload = payload; + + window.dispatchEvent(event); }; get someDialogIsOpen() { diff --git a/products/ASC.Files/Client/src/store/FileActionStore.js b/products/ASC.Files/Client/src/store/FileActionStore.js deleted file mode 100644 index 04c88143ae..0000000000 --- a/products/ASC.Files/Client/src/store/FileActionStore.js +++ /dev/null @@ -1,27 +0,0 @@ -import { makeAutoObservable } from "mobx"; - -class FileActionStore { - id = null; - type = null; - extension = null; - title = ""; - templateId = null; - fromTemplate = null; - - constructor() { - makeAutoObservable(this); - } - - setAction = (fileAction) => { - if (fileAction.fromTemplate === undefined && this.fromTemplate) return; - - const fileActionItems = Object.keys(fileAction); - for (let key of fileActionItems) { - if (key in this) { - this[key] = fileAction[key]; - } - } - }; -} - -export default new FileActionStore(); diff --git a/products/ASC.Files/Client/src/store/FilesActionsStore.js b/products/ASC.Files/Client/src/store/FilesActionsStore.js index e8415ac7b2..191245f3fb 100644 --- a/products/ASC.Files/Client/src/store/FilesActionsStore.js +++ b/products/ASC.Files/Client/src/store/FilesActionsStore.js @@ -20,7 +20,7 @@ import { import { makeAutoObservable } from "mobx"; import toastr from "studio/toastr"; -import { TIMEOUT } from "../helpers/constants"; +import { Events, TIMEOUT } from "../helpers/constants"; import { loopTreeFolders, checkProtocol } from "../helpers/files-helpers"; import { combineUrl } from "@appserver/common/utils"; import { AppServerConfig } from "@appserver/common/constants"; @@ -453,16 +453,16 @@ class FilesActionStore { return this.downloadFiles(fileIds, folderIds, label); }; - editCompleteAction = async (id, selectedItem, isCancelled = false) => { + editCompleteAction = async (id, selectedItem, isCancelled = false, type) => { const { filter, folders, files, - fileActionStore, + fetchFiles, setIsLoading, } = this.filesStore; - const { type, setAction } = fileActionStore; + const { treeFolders, setTreeFolders } = this.treeFoldersStore; const items = [...folders, ...files]; @@ -480,14 +480,7 @@ class FilesActionStore { setTreeFolders(treeFolders); } } - setAction({ - type: null, - id: null, - extension: null, - title: "", - templateId: null, - fromTemplate: null, - }); + setIsLoading(false); type === FileAction.Rename && this.onSelectItem( diff --git a/products/ASC.Files/Client/src/store/FilesStore.js b/products/ASC.Files/Client/src/store/FilesStore.js index dc336b6219..293dc02e5f 100644 --- a/products/ASC.Files/Client/src/store/FilesStore.js +++ b/products/ASC.Files/Client/src/store/FilesStore.js @@ -26,7 +26,7 @@ class FilesStore { authStore; settingsStore; userStore; - fileActionStore; + selectedFolderStore; treeFoldersStore; filesSettingsStore; @@ -78,7 +78,7 @@ class FilesStore { authStore, settingsStore, userStore, - fileActionStore, + selectedFolderStore, treeFoldersStore, filesSettingsStore @@ -90,7 +90,7 @@ class FilesStore { this.authStore = authStore; this.settingsStore = settingsStore; this.userStore = userStore; - this.fileActionStore = fileActionStore; + this.selectedFolderStore = selectedFolderStore; this.treeFoldersStore = treeFoldersStore; this.filesSettingsStore = filesSettingsStore; @@ -654,7 +654,6 @@ class FilesStore { }); if (clearFilter) { - this.fileActionStore.setAction({ type: null }); if (clearSelection) { this.setSelected("close"); } @@ -1669,10 +1668,6 @@ class FilesStore { }; }); - if (this.fileActionStore.type === FileAction.Create) { - this.onCreateAddTempItem(newItem); - } - return newItem; } diff --git a/products/ASC.Files/Client/src/store/index.js b/products/ASC.Files/Client/src/store/index.js index 6b6fdd8906..31731d5be4 100644 --- a/products/ASC.Files/Client/src/store/index.js +++ b/products/ASC.Files/Client/src/store/index.js @@ -1,5 +1,4 @@ import FilesStore from "./FilesStore"; -import fileActionStore from "./FileActionStore"; import SelectedFolderStore from "./SelectedFolderStore"; import TreeFoldersStore from "./TreeFoldersStore"; import thirdPartyStore from "./ThirdPartyStore"; @@ -26,7 +25,6 @@ const filesStore = new FilesStore( store.auth, store.auth.settingsStore, store.auth.userStore, - fileActionStore, selectedFolderStore, treeFoldersStore, settingsStore,