From b0322cd68baa32c37778bb4deaf5da8b1ecbbb05 Mon Sep 17 00:00:00 2001 From: Timofey Boyko Date: Mon, 19 Feb 2024 11:40:07 +0300 Subject: [PATCH] Doceditor: refactoring --- packages/doceditor/src/components/Editor.tsx | 285 ++++++--- packages/doceditor/src/components/Root.tsx | 186 ++++++ .../src/components/SelectFileDialog.tsx | 26 +- .../src/components/SelectFolderDialog.tsx | 18 +- .../doceditor/src/hooks/useEditorEvents.ts | 605 ++++++++++++++++++ packages/doceditor/src/hooks/useInit.ts | 85 +++ .../src/hooks/useSelectFolderDialog.ts | 7 +- packages/doceditor/src/hooks/useWhiteLabel.ts | 2 +- packages/doceditor/src/utils/constants.ts | 12 + packages/doceditor/src/utils/events.ts | 82 +++ packages/doceditor/src/utils/index.ts | 128 ++++ packages/doceditor/src/utils/initDesktop.ts | 48 ++ 12 files changed, 1363 insertions(+), 121 deletions(-) create mode 100644 packages/doceditor/src/components/Root.tsx create mode 100644 packages/doceditor/src/hooks/useEditorEvents.ts create mode 100644 packages/doceditor/src/hooks/useInit.ts create mode 100644 packages/doceditor/src/utils/constants.ts create mode 100644 packages/doceditor/src/utils/events.ts create mode 100644 packages/doceditor/src/utils/initDesktop.ts diff --git a/packages/doceditor/src/components/Editor.tsx b/packages/doceditor/src/components/Editor.tsx index 8f4385fbb0..498f0a2d8f 100644 --- a/packages/doceditor/src/components/Editor.tsx +++ b/packages/doceditor/src/components/Editor.tsx @@ -1,122 +1,225 @@ "use client"; import React from "react"; -import { ThemeProvider } from "styled-components"; -import { I18nextProvider } from "react-i18next"; -import { i18n } from "i18next"; +import { isMobile } from "react-device-detect"; import { DocumentEditor } from "@onlyoffice/document-editor-react"; -import IConfig from "@onlyoffice/document-editor-react/dist/esm/model/config"; +import IConfig from "@onlyoffice/document-editor-react/dist/esm/types/model/config"; +import { EditorProps, TGoBack } from "@/types"; -import { EditorProps } from "@/types"; -import useSocketHelper from "@/hooks/useSocketHelper"; -import useSelectFolderDialog from "@/hooks/useSelectFolderDialog"; -import useSelectFileDialog from "@/hooks/useSelectFileDialog"; -import useTheme from "@/hooks/useTheme"; -import useI18N from "@/hooks/useI18N"; +import useInit from "@/hooks/useInit"; +import useEditorEvents from "@/hooks/useEditorEvents"; -import SelectFolderDialog from "./SelectFolderDialog"; -import SelectFileDialog from "./SelectFileDialog"; +import { FolderType } from "@docspace/shared/enums"; +import { getBackUrl, getIsZoom } from "@/utils"; +import { IS_DESKTOP_EDITOR } from "@/utils/constants"; +import { + onSDKRequestHistoryClose, + onSDKRequestEditRights, + onSDKInfo, + onSDKWarning, + onSDKError, + onSDKRequestRename, +} from "@/utils/events"; + +import { getEditorTheme } from "@docspace/shared/utils"; const Editor = ({ config, - editorUrl, - settings, successAuth, user, + view, + doc, + documentserverUrl, + fileInfo, + t, + onSDKRequestSaveAs, + onSDKRequestInsertImage, + onSDKRequestSelectSpreadsheet, + onSDKRequestSelectDocument, + onSDKRequestReferenceSource, }: EditorProps) => { - const fileInfo = config?.file; - const documentserverUrl = editorUrl.docServiceUrl; - const instanceId = config?.document?.referenceData.instanceId; - - const { i18n } = useI18N({ settings, user }); - const { theme } = useTheme({ user }); - - const { socketHelper } = useSocketHelper({ socketUrl: settings.socketUrl }); const { - onSDKRequestSaveAs, - onCloseSelectFolderDialog, - onSubmitSelectFolderDialog, - getIsDisabledSelectFolderDialog, - isVisibleSelectFolderDialog, - titleSelectorFolderDialog, - } = useSelectFolderDialog({}); - const { - onSDKRequestInsertImage, - onSDKRequestReferenceSource, - onSDKRequestSelectDocument, - onSDKRequestSelectSpreadsheet, - onCloseSelectFileDialog, - onSubmitSelectFileDialog, - getIsDisabledSelectFileDialog, + onDocumentReady, + onSDKRequestOpen, + onSDKRequestReferenceData, + onSDKAppReady, + onSDKRequestClose, + onSDKRequestCreateNew, + onSDKRequestHistory, + onSDKRequestUsers, + onSDKRequestSendNotify, + onSDKRequestRestore, + onSDKRequestHistoryData, + onDocumentStateChange, + onMetaChange, + onMakeActionLink, - selectFileDialogFileTypeDetection, + createUrl, + documentReady, + usersInRoom, + setDocTitle, + } = useEditorEvents({ + user, + successAuth, + fileInfo, + config, + doc, + t, + }); - selectFileDialogVisible, - } = useSelectFileDialog({ instanceId }); - - const onDocumentReady = (): void => { - throw new Error("Function not implemented."); - }; + useInit({ + config, + successAuth, + fileInfo, + user, + documentReady, + setDocTitle, + t, + }); const newConfig: IConfig = { document: config.document, documentType: config.documentType, - editorConfig: config.editorConfig, token: config.token, type: config.type, - events: {}, }; - if (successAuth && newConfig.events) { + newConfig.editorConfig = { ...config.editorConfig }; + + if (view && newConfig.editorConfig) newConfig.editorConfig.mode = "view"; + if (isMobile) config.type = "mobile"; + + let goBack: TGoBack = {} as TGoBack; + + if (fileInfo) { + const search = typeof window !== "undefined" ? window.location.search : ""; + const editorGoBack = new URLSearchParams(search).get("editorGoBack"); + + if (editorGoBack === "false") { + } else if (editorGoBack === "event") { + goBack = { + requestClose: true, + text: t?.("FileLocation"), + }; + } else { + goBack = { + requestClose: + typeof window !== "undefined" + ? window.DocSpaceConfig?.editor?.requestClose ?? false + : false, + text: t?.("FileLocation"), + }; + if ( + typeof window !== "undefined" && + !window.DocSpaceConfig?.editor?.requestClose + ) { + goBack.blank = + typeof window !== "undefined" + ? window.DocSpaceConfig?.editor?.openOnNewPage ?? true + : false; + goBack.url = getBackUrl(fileInfo.rootFolderId, fileInfo.folderId); + } + } + } + + if (newConfig.editorConfig) + newConfig.editorConfig.customization = { + ...newConfig.editorConfig.customization, + goback: { ...goBack }, + uiTheme: getEditorTheme(user?.theme), + }; + + if (newConfig.document && newConfig.document.info) + newConfig.document.info.favorite = false; + + // const url = window.location.href; + + // if (url.indexOf("anchor") !== -1) { + // const splitUrl = url.split("anchor="); + // const decodeURI = decodeURIComponent(splitUrl[1]); + // const obj = JSON.parse(decodeURI); + + // config.editorConfig.actionLink = { + // action: obj.action, + // }; + // } + + newConfig.events = { + onDocumentReady, + onRequestHistoryClose: onSDKRequestHistoryClose, + onRequestEditRights: () => onSDKRequestEditRights(fileInfo), + onAppReady: onSDKAppReady, + onInfo: onSDKInfo, + onWarning: onSDKWarning, + onError: onSDKError, + onRequestHistoryData: onSDKRequestHistoryData, + onDocumentStateChange, + onMetaChange, + onMakeActionLink, + }; + + if (successAuth) { + if (fileInfo?.rootFolderType !== FolderType.USER) { + //TODO: remove condition for share in my + newConfig.events.onRequestUsers = onSDKRequestUsers; + newConfig.events.onRequestSendNotify = onSDKRequestSendNotify; + } + if (!user.isVisitor) { + newConfig.events.onRequestSaveAs = onSDKRequestSaveAs; + if ( + IS_DESKTOP_EDITOR || + (typeof window !== "undefined" && + window.DocSpaceConfig?.editor?.openOnNewPage === false) + ) { + newConfig.events.onRequestCreateNew = onSDKRequestCreateNew; + } + } + newConfig.events.onRequestInsertImage = onSDKRequestInsertImage; - // restore for 1.4 editor version - // newConfig.events.onRequestSelectSpreadsheet = onSDKRequestSelectSpreadsheet; - // newConfig.events.onRequestSelectDocument = onSDKRequestSelectDocument; - // newConfig.events.onRequestReferenceSource = onSDKRequestReferenceSource; - if (!user.isVisitor) newConfig.events.onRequestSaveAs = onSDKRequestSaveAs; + newConfig.events.onRequestSelectSpreadsheet = onSDKRequestSelectSpreadsheet; + newConfig.events.onRequestSelectDocument = onSDKRequestSelectDocument; + newConfig.events.onRequestReferenceSource = onSDKRequestReferenceSource; + } + + if (!fileInfo.providerKey) { + newConfig.events.onRequestReferenceData = onSDKRequestReferenceData; + const isZoom = getIsZoom(); + + if (!isZoom) { + newConfig.events.onRequestOpen = onSDKRequestOpen; + } + } + + if (fileInfo.security.Rename) { + newConfig.events.onRequestRename = (obj: object) => + onSDKRequestRename(obj, fileInfo.id); + } + + if (fileInfo.security.ReadHistory) { + newConfig.events.onRequestHistory = onSDKRequestHistory; + } + + if (fileInfo.security.EditHistory) { + newConfig.events.onRequestRestore = onSDKRequestRestore; + } + + if ( + typeof window !== "undefined" && + window.DocSpaceConfig?.editor?.requestClose + ) { + newConfig.events.onRequestClose = onSDKRequestClose; } return ( - <> - {" "} - - {theme && i18n && ( - - - {isVisibleSelectFolderDialog && !!socketHelper && ( - - )} - {selectFileDialogVisible && !!socketHelper && ( - - )} - - - )} - + ); }; diff --git a/packages/doceditor/src/components/Root.tsx b/packages/doceditor/src/components/Root.tsx new file mode 100644 index 0000000000..b1f0790d14 --- /dev/null +++ b/packages/doceditor/src/components/Root.tsx @@ -0,0 +1,186 @@ +"use client"; + +import React from "react"; +import { I18nextProvider } from "react-i18next"; +import { toast } from "react-toastify"; + +import { Toast } from "@docspace/shared/components/toast"; +import { TFile } from "@docspace/shared/api/files/types"; +import { ThemeProvider } from "@docspace/shared/components/theme-provider"; +import ErrorBoundary from "@docspace/shared/components/error-boundary/ErrorBoundary"; +import ErrorContainer from "@docspace/shared/components/error-container/ErrorContainer"; +import FirebaseHelper from "@docspace/shared/utils/firebase"; +import { TFirebaseSettings } from "@docspace/shared/api/settings/types"; +import { TUser } from "@docspace/shared/api/people/types"; + +import { TResponse } from "@/types"; +import useError from "@/hooks/useError"; +import useI18N from "@/hooks/useI18N"; +import useTheme from "@/hooks/useTheme"; +import useDeviceType from "@/hooks/useDeviceType"; +import useWhiteLabel from "@/hooks/useWhiteLabel"; +import useRootInit from "@/hooks/useRootInit"; +import useDeepLink from "@/hooks/useDeepLink"; +import useSelectFileDialog from "@/hooks/useSelectFileDialog"; +import useSelectFolderDialog from "@/hooks/useSelectFolderDialog"; +import useSocketHelper from "@/hooks/useSocketHelper"; + +import pkgFile from "../../package.json"; + +import DeepLink from "./deep-link"; + +import SelectFileDialog from "./SelectFileDialog"; +import SelectFolderDialog from "./SelectFolderDialog"; +import Editor from "./Editor"; +import { IS_VIEW } from "@/utils/constants"; + +toast.configure(); + +const Root = ({ + settings, + config, + successAuth, + user, + error, + isSharingAccess, + editorUrl, + doc, +}: TResponse) => { + const documentserverUrl = editorUrl?.docServiceUrl; + const fileInfo = config?.file; + const firebaseHelper = new FirebaseHelper( + settings?.firebase ?? ({} as TFirebaseSettings), + ); + const instanceId = config?.document?.referenceData.instanceId; + + useRootInit({ + documentType: config?.documentType, + fileType: config?.file.fileType, + }); + const { i18n } = useI18N({ settings, user }); + + const t = i18n.t ? i18n.t.bind(i18n) : null; + const { onError, getErrorMessage } = useError({ + error, + editorUrl: documentserverUrl, + t, + }); + const { theme, currentColorTheme } = useTheme({ user }); + const { currentDeviceType } = useDeviceType(); + const { logoUrls } = useWhiteLabel(); + const { isShowDeepLink, setIsShowDeepLink } = useDeepLink({ + settings, + fileInfo, + email: user?.email, + }); + + const { socketHelper } = useSocketHelper({ + socketUrl: settings?.socketUrl ?? "", + }); + const { + onSDKRequestSaveAs, + onCloseSelectFolderDialog, + onSubmitSelectFolderDialog, + getIsDisabledSelectFolderDialog, + isVisibleSelectFolderDialog, + titleSelectorFolderDialog, + } = useSelectFolderDialog({}); + const { + onSDKRequestInsertImage, + onSDKRequestReferenceSource, + onSDKRequestSelectDocument, + onSDKRequestSelectSpreadsheet, + onCloseSelectFileDialog, + onSubmitSelectFileDialog, + getIsDisabledSelectFileDialog, + + selectFileDialogFileTypeDetection, + + selectFileDialogVisible, + } = useSelectFileDialog({ instanceId: instanceId ?? "" }); + + return ( + + + + {isShowDeepLink ? ( + + ) : error && error.message !== "unauthorized" ? ( + + ) : ( + <> + {config && user && documentserverUrl && fileInfo && ( + + )} + + {isVisibleSelectFolderDialog && !!socketHelper && ( + + )} + {selectFileDialogVisible && !!socketHelper && ( + + )} + + )} + + + + ); +}; + +export default Root; diff --git a/packages/doceditor/src/components/SelectFileDialog.tsx b/packages/doceditor/src/components/SelectFileDialog.tsx index 780beb3aae..19e29f85fb 100644 --- a/packages/doceditor/src/components/SelectFileDialog.tsx +++ b/packages/doceditor/src/components/SelectFileDialog.tsx @@ -1,11 +1,10 @@ import React from "react"; -import { useTranslation } from "react-i18next"; + import FilesSelectorWrapper from "@docspace/shared/selectors/Files/FilesSelector.wrapper"; import { DeviceType, FilesSelectorFilterTypes } from "@docspace/shared/enums"; import { SelectFileDialogProps } from "@/types"; -import { useTheme } from "styled-components"; const SelectFileDialog = ({ socketHelper, @@ -15,23 +14,23 @@ const SelectFileDialog = ({ onClose, onSubmit, fileInfo, + t, + i18n, }: SelectFileDialogProps) => { - const { t, i18n } = useTranslation(["Common", "Editor"]); - const sessionPath = sessionStorage.getItem("filesSelectorPath"); const headerLabel = fileTypeDetection.filterParam - ? t("Common:SelectFile") - : t("Common:SelectAction"); + ? t?.("Common:SelectFile") ?? "" + : t?.("Common:SelectAction") ?? ""; const getFileTypeTranslation = React.useCallback(() => { switch (fileTypeDetection.filterParam) { case FilesSelectorFilterTypes.XLSX: - return t("Editor:MailMergeFileType"); + return t?.("Editor:MailMergeFileType") ?? ""; case FilesSelectorFilterTypes.IMG: - return t("Editor:ImageFileType"); + return t?.("Editor:ImageFileType") ?? ""; case FilesSelectorFilterTypes.DOCX: - return t("Editor:DocumentsFileType"); + return t?.("Editor:DocumentsFileType") ?? ""; default: return ""; } @@ -41,16 +40,13 @@ const SelectFileDialog = ({ const type = getFileTypeTranslation(); return fileTypeDetection.filterParam === FilesSelectorFilterTypes.XLSX ? type - : t("Editor:SelectFilesType", { fileType: type }); + : t?.("Editor:SelectFilesType", { fileType: type }) ?? ""; }, [fileTypeDetection.filterParam, getFileTypeTranslation, t]); const listTitle = selectFilesListTitle(); - const theme = useTheme(); - return ( { - const { t, i18n } = useTranslation(["Common", "Editor"]); - const sessionPath = sessionStorage.getItem("filesSelectorPath"); const cancelButtonProps: TSelectorCancelButton = { withCancelButton: true, onCancel: onClose, - cancelButtonLabel: t("CancelButton"), + cancelButtonLabel: t?.("Common:CancelButton") ?? "", cancelButtonId: "select-file-modal-cancel", }; - const theme = useTheme(); - return ( ; + +let docEditor: TDocEditor | null = null; + +const useEditorEvents = ({ + user, + successAuth, + fileInfo, + config, + doc, + t, +}: UseEventsProps) => { + const [events, setEvents] = React.useState({}); + const [documentReady, setDocumentReady] = React.useState(false); + const [createUrl, setCreateUrl] = React.useState>(null); + const [usersInRoom, setUsersInRoom] = React.useState([]); + const [docTitle, setDocTitle] = React.useState(""); + const [docSaved, setDocSaved] = React.useState(false); + + const onSDKRequestReferenceData = React.useCallback(async (event: object) => { + const currEvent = event as TEvent; + const referenceData = await getReferenceData( + currEvent.data.referenceData ?? + (currEvent.data as unknown as TGetReferenceData), + ); + + docEditor?.setReferenceData?.(referenceData); + }, []); + + const onSDKRequestOpen = React.useCallback( + async (event: object) => { + const currEvent = event as TEvent; + const windowName = currEvent.data.windowName; + const reference = currEvent.data; + + try { + const data = { + fileKey: reference.referenceData + ? reference.referenceData.fileKey + : "", + instanceId: reference.referenceData + ? reference.referenceData.instanceId + : "", + fileId: fileInfo.id, + path: reference.path || "", + }; + + const result = await getReferenceData(data); + + if (result.error) throw new Error(result.error); + + var link = result.link; + window.open(link, windowName); + } catch (e) { + var winEditor = window.open("", windowName); + + winEditor?.close(); + docEditor?.showMessage?.( + (e as { message?: string })?.message ?? + t?.("ErrorConnectionLost") ?? + "", + ); + } + }, + [fileInfo.id, t], + ); + + const onSDKAppReady = React.useCallback(() => { + docEditor = window.DocEditor.instances[EDITOR_ID]; + + console.log("ONLYOFFICE Document Editor is ready", docEditor); + const url = window.location.href; + + const index = url.indexOf("#message/"); + + if (index > -1) { + const splitUrl = url.split("#message/"); + + if (splitUrl.length === 2) { + const message = decodeURIComponent(splitUrl[1]).replace(/\+/g, " "); + + docEditor?.showMessage?.(message); + history.pushState({}, "", url.substring(0, index)); + } else { + if (config?.Error) docEditor?.showMessage?.(config.Error); + } + } + }, [config.Error]); + + const onDocumentReady = React.useCallback(() => { + // console.log("onDocumentReady", arguments, { docEditor }); + setDocumentReady(true); + + frameCallCommand("setIsLoaded"); + + if (config?.errorMessage) docEditor?.showMessage?.(config.errorMessage); + + // if (config?.file?.canShare) { + // loadUsersRightsList(docEditor); + // } + + if (docEditor) + assign( + window as unknown as { [key: string]: {} }, + ["ASC", "Files", "Editor", "docEditor"], + docEditor, + ); //Do not remove: it's for Back button on Mobile App + }, [config.errorMessage]); + + const getBackUrl = React.useCallback(() => { + if (!fileInfo) return; + const search = window.location.search; + const shareIndex = search.indexOf("share="); + const key = shareIndex > -1 ? search.substring(shareIndex + 6) : null; + + let backUrl = ""; + + if (fileInfo.rootFolderType === FolderType.Rooms) { + if (key) { + backUrl = `/rooms/share?key=${key}`; + } else { + backUrl = `/rooms/shared/${fileInfo.folderId}/filter?folder=${fileInfo.folderId}`; + } + } else { + backUrl = `/rooms/personal/filter?folder=${fileInfo.folderId}`; + } + + const url = window.location.href; + const origin = url.substring(0, url.indexOf("/doceditor")); + + return `${combineUrl(origin, backUrl)}`; + }, [fileInfo]); + + const onSDKRequestClose = React.useCallback(() => { + const search = window.location.search; + const editorGoBack = new URLSearchParams(search).get("editorGoBack"); + + if (editorGoBack === "event") { + frameCallEvent({ event: "onEditorCloseCallback" }); + } else { + const backUrl = getBackUrl(); + if (backUrl) window.location.replace(backUrl); + } + }, [getBackUrl]); + + const getDefaultFileName = React.useCallback( + (withExt = false) => { + const documentType = config?.documentType; + + const fileExt = + documentType === "word" + ? "docx" + : documentType === "slide" + ? "pptx" + : documentType === "cell" + ? "xlsx" + : "docxf"; + + let fileName = t?.("Common:NewDocument"); + + switch (fileExt) { + case "xlsx": + fileName = t?.("Common:NewSpreadsheet"); + break; + case "pptx": + fileName = t?.("Common:NewPresentation"); + break; + case "docxf": + fileName = t?.("Common:NewMasterForm"); + break; + default: + break; + } + + if (withExt) { + fileName = `${fileName}.${fileExt}`; + } + + return fileName; + }, + [config?.documentType, t], + ); + + const onSDKRequestCreateNew = React.useCallback(() => { + const defaultFileName = getDefaultFileName(true); + + createFile(fileInfo.folderId, defaultFileName ?? "") + ?.then((newFile) => { + const newUrl = combineUrl( + window.DocSpaceConfig?.proxy?.url, + `/doceditor?fileId=${encodeURIComponent(newFile.id)}`, + ); + window.open( + newUrl, + window.DocSpaceConfig?.editor?.openOnNewPage ? "_blank" : "_self", + ); + }) + .catch((e) => { + toastr.error(e); + }); + }, [fileInfo.folderId, getDefaultFileName]); + + const getDocumentHistory = React.useCallback( + (fileHistory: TEditHistory[], historyLength: number) => { + let result = []; + + for (let i = 0; i < historyLength; i++) { + const changes = fileHistory[i].changes; + const serverVersion = fileHistory[i].serverVersion; + const version = fileHistory[i].version; + const versionGroup = fileHistory[i].versionGroup; + + let obj = { + ...(changes.length !== 0 && { changes }), + created: `${new Date(fileHistory[i].created).toLocaleString( + config.editorConfig.lang, + )}`, + ...(serverVersion && { serverVersion }), + key: fileHistory[i].key, + user: { + id: fileHistory[i].user.id, + name: fileHistory[i].user.name, + }, + version, + versionGroup, + }; + + result.push(obj); + } + return result; + }, + [config.editorConfig.lang], + ); + + const onSDKRequestRestore = React.useCallback( + async (event: object) => { + const restoreVersion = (event as TEvent).data.version; + + try { + const updateVersions = await restoreDocumentsVersion( + fileInfo.id, + restoreVersion ?? 0, + doc ?? "", + ); + const historyLength = updateVersions.length; + docEditor?.refreshHistory?.({ + currentVersion: getCurrentDocumentVersion( + updateVersions, + historyLength, + ), + history: getDocumentHistory(updateVersions, historyLength), + }); + } catch (error) { + let errorMessage = ""; + + const typedError = error as TCatchError; + if (typeof typedError === "object") { + errorMessage = + ("response" in typedError && + typedError?.response?.data?.error?.message) || + ("statusText" in typedError && typedError?.statusText) || + ("message" in typedError && typedError?.message) || + ""; + } else { + errorMessage = error as string; + } + + docEditor?.refreshHistory?.({ + error: `${errorMessage}`, //TODO: maybe need to display something else. + }); + } + }, + [doc, fileInfo.id, getDocumentHistory], + ); + + const onSDKRequestHistory = React.useCallback(async () => { + try { + // const search = window.location.search; + // const shareIndex = search.indexOf("share="); + // const requestToken = + // shareIndex > -1 ? search.substring(shareIndex + 6) : null; + // const docIdx = search.indexOf("doc="); + + const fileHistory = await getEditHistory(fileInfo.id, doc ?? ""); + const historyLength = fileHistory.length; + + docEditor?.refreshHistory?.({ + currentVersion: getCurrentDocumentVersion(fileHistory, historyLength), + history: getDocumentHistory(fileHistory, historyLength), + }); + } catch (error) { + let errorMessage = ""; + const typedError = error as TCatchError; + if (typeof typedError === "object") { + errorMessage = + ("response" in typedError && + typedError?.response?.data?.error?.message) || + ("statusText" in typedError && typedError?.statusText) || + ("message" in typedError && typedError?.message) || + ""; + } else { + errorMessage = error as string; + } + docEditor?.refreshHistory?.({ + error: `${errorMessage}`, //TODO: maybe need to display something else. + }); + } + }, [doc, fileInfo.id, getDocumentHistory]); + + const onSDKRequestSendNotify = React.useCallback( + async (event: object) => { + const currEvent = event as TEvent; + + const actionData = currEvent.data.actionLink; + const comment = currEvent.data.message; + const emails = currEvent.data.emails; + + try { + await sendEditorNotify( + fileInfo.id, + actionData ?? "", + emails ?? [], + comment ?? "", + ); + + if (usersInRoom.length === 0) return; + + const usersNotFound = emails?.filter((row) => + usersInRoom.every((value) => { + return row !== value.email; + }), + ); + + usersNotFound && + usersNotFound.length > 0 && + docEditor?.showMessage?.( + t?.("UsersWithoutAccess", { + users: usersNotFound, + }) ?? "", + ); + } catch (e) { + toastr.error(e as TData); + } + }, + [fileInfo.id, t, usersInRoom], + ); + + const onSDKRequestUsers = React.useCallback( + async (event: object) => { + try { + const currEvent = event as TEvent; + const c = currEvent?.data?.c; + const users = await (c == "protect" + ? getProtectUsers(fileInfo.id) + : getSharedUsers(fileInfo.id)); + + if (c !== "protect") { + const usersArray = users.map( + (item) => + ({ + email: item.email, + name: item.name, + }) as unknown as TUser, + ); + setUsersInRoom(usersArray); + } + + docEditor?.setUsers?.({ + c: c ?? "", + users, + }); + } catch (e) { + docEditor?.showMessage?.( + ((e as { message?: string })?.message || + t?.("ErrorConnectionLost")) ?? + "", + ); + } + }, + [fileInfo.id, t], + ); + + const onSDKRequestHistoryData = React.useCallback( + async (event: object) => { + const version = (event as { data: number }).data; + + try { + // const search = window.location.search; + // const shareIndex = search.indexOf("share="); + // const requestToken = + // shareIndex > -1 ? search.substring(shareIndex + 6) : null; + + const versionDifference = await getEditDiff( + fileInfo.id, + version, + doc ?? "", + // requestToken, + ); + const changesUrl = versionDifference.changesUrl; + const previous = versionDifference.previous; + const token = versionDifference.token; + + const obj: THistoryData = { + url: versionDifference.url, + version, + key: versionDifference.key, + fileType: versionDifference.fileType, + }; + + if (changesUrl) obj.changesUrl = changesUrl; + if (previous) + obj.previous = { + fileType: previous.fileType, + key: previous.key, + url: previous.url, + }; + if (token) obj.token = token; + + docEditor?.setHistoryData?.(obj); + } catch (error) { + let errorMessage = ""; + const typedError = error as TCatchError; + if (typeof typedError === "object") { + errorMessage = + ("response" in typedError && + typedError?.response?.data?.error?.message) || + ("statusText" in typedError && typedError?.statusText) || + ("message" in typedError && typedError?.message) || + ""; + } else { + errorMessage = error as string; + } + + docEditor?.setHistoryData?.({ + error: `${errorMessage}`, //TODO: maybe need to display something else. + version, + }); + } + }, + [doc, fileInfo.id], + ); + + const onDocumentStateChange = React.useCallback( + (event: object) => { + if (!documentReady) return; + + setDocSaved(!(event as { data: boolean }).data); + + setTimeout(() => { + docSaved + ? setDocumentTitle( + docTitle, + config.document.fileType, + documentReady, + successAuth, + setDocTitle, + ) + : setDocumentTitle( + `*${docTitle}`, + config.document.fileType, + documentReady, + successAuth, + setDocTitle, + ); + }, 500); + }, + [config.document.fileType, docSaved, docTitle, documentReady, successAuth], + ); + + const onMetaChange = React.useCallback( + (event: object) => { + const newTitle = (event as { data: { title: string } }).data.title; + //const favorite = event.data.favorite; + + if (newTitle && newTitle !== docTitle) { + setDocumentTitle( + newTitle, + config.document.fileType, + documentReady, + successAuth, + setDocTitle, + ); + setDocTitle(newTitle); + } + }, + [config.document.fileType, docTitle, documentReady, successAuth], + ); + + const onMakeActionLink = React.useCallback((event: object) => { + const url = window.location.href; + + const actionData = (event as { data: {} }).data; + + const link = generateLink(actionData); + + const urlFormation = url.split("&anchor=")[0]; + + const linkFormation = `${urlFormation}&anchor=${link}`; + + docEditor?.setActionLink?.(linkFormation); + }, []); + + const generateLink = (actionData: {}) => { + return encodeURIComponent(JSON.stringify(actionData)); + }; + + React.useEffect(() => { + const tempEvents: IConfigEvents = {}; + + setEvents(tempEvents); + }, [successAuth, user.isVisitor, config?.documentType, fileInfo]); + + React.useEffect(() => { + if ( + IS_DESKTOP_EDITOR || + (typeof window !== "undefined" && + window.DocSpaceConfig?.editor?.openOnNewPage === false) + ) + return; + + //FireFox security issue fix (onRequestCreateNew will be blocked) + const documentType = config?.documentType || "word"; + const defaultFileName = getDefaultFileName(); + const url = new URL( + combineUrl( + window.location.origin, + window.DocSpaceConfig?.proxy?.url, + "/filehandler.ashx", + ), + ); + url.searchParams.append("action", "create"); + url.searchParams.append("doctype", documentType); + url.searchParams.append("title", defaultFileName ?? ""); + setCreateUrl(url.toString()); + }, [config?.documentType, getDefaultFileName]); + + return { + events, + createUrl, + documentReady, + usersInRoom, + + onDocumentReady, + onSDKRequestOpen, + onSDKRequestReferenceData, + onSDKAppReady, + onSDKRequestClose, + onSDKRequestCreateNew, + onSDKRequestHistory, + onSDKRequestUsers, + onSDKRequestSendNotify, + onSDKRequestRestore, + onSDKRequestHistoryData, + onDocumentStateChange, + onMetaChange, + onMakeActionLink, + + setDocTitle, + }; +}; + +export default useEditorEvents; diff --git a/packages/doceditor/src/hooks/useInit.ts b/packages/doceditor/src/hooks/useInit.ts new file mode 100644 index 0000000000..f5f1c3a953 --- /dev/null +++ b/packages/doceditor/src/hooks/useInit.ts @@ -0,0 +1,85 @@ +import React from "react"; +import { isIOS, deviceType } from "react-device-detect"; + +import { FolderType } from "@docspace/shared/enums"; + +import { UseInitProps } from "@/types"; +import { initForm, setDocumentTitle, showDocEditorMessage } from "@/utils"; +import initDesktop from "@/utils/initDesktop"; +import { IS_DESKTOP_EDITOR, IS_VIEW } from "@/utils/constants"; + +const useInit = ({ + config, + successAuth, + fileInfo, + user, + t, + setDocTitle, + documentReady, +}: UseInitProps) => { + React.useEffect(() => { + if (isIOS && deviceType === "tablet") { + const vh = window.innerHeight * 0.01; + document.documentElement.style.setProperty("--vh", `${vh}px`); + } + }, []); + + React.useEffect(() => { + if ( + !IS_VIEW && + fileInfo && + fileInfo.viewAccessibility.WebRestrictedEditing && + fileInfo.security.FillForms && + fileInfo.rootFolderType === FolderType.Rooms && + !fileInfo.security.Edit && + !config.document.isLinkedForMe + ) { + try { + initForm(fileInfo.id); + } catch (err) { + console.error(err); + } + } + }, [config.document.isLinkedForMe, fileInfo]); + + React.useEffect(() => { + if (!config) return; + + setDocumentTitle( + config.document.title, + config.document.fileType, + documentReady, + successAuth, + setDocTitle, + ); + }, [config, documentReady, fileInfo, setDocTitle, successAuth]); + + React.useEffect(() => { + if (config && IS_DESKTOP_EDITOR) { + initDesktop(config, user, fileInfo.id, t); + } + }, [config, fileInfo.id, t, user]); + + React.useEffect(() => { + try { + const url = window.location.href; + + if ( + successAuth && + url.indexOf("#message/") > -1 && + fileInfo && + fileInfo?.fileExst && + fileInfo?.viewAccessibility?.MustConvert && + fileInfo?.security?.Convert + ) { + showDocEditorMessage(url, fileInfo.id); + } + } catch (err) { + console.error(err); + } + }, [fileInfo, successAuth]); + + return {}; +}; + +export default useInit; diff --git a/packages/doceditor/src/hooks/useSelectFolderDialog.ts b/packages/doceditor/src/hooks/useSelectFolderDialog.ts index 73d3c3dea3..d97d5f45e3 100644 --- a/packages/doceditor/src/hooks/useSelectFolderDialog.ts +++ b/packages/doceditor/src/hooks/useSelectFolderDialog.ts @@ -25,9 +25,10 @@ const useSelectFolderDialog = ({}: UseSelectFolderDialogProps) => { const onSDKRequestSaveAs = useCallback((event: object) => { if ("data" in event) { const data = event.data as TEventData; - setTitle(data.title); - setUrl(data.url); - setExtension(data.fileType); + + setTitle(data.title ?? ""); + setUrl(data.url ?? ""); + setExtension(data.fileType ?? ""); setIsVisible(true); } diff --git a/packages/doceditor/src/hooks/useWhiteLabel.ts b/packages/doceditor/src/hooks/useWhiteLabel.ts index 5e8d06be7e..007c0c9813 100644 --- a/packages/doceditor/src/hooks/useWhiteLabel.ts +++ b/packages/doceditor/src/hooks/useWhiteLabel.ts @@ -15,7 +15,7 @@ const useWhiteLabel = () => { requestRunning.current = true; const urls = await getLogoUrls(); requestRunning.current = false; - console.log("====", urls); + setLogoUrls(urls); alreadyFetched.current = true; }, []); diff --git a/packages/doceditor/src/utils/constants.ts b/packages/doceditor/src/utils/constants.ts new file mode 100644 index 0000000000..340c642aa4 --- /dev/null +++ b/packages/doceditor/src/utils/constants.ts @@ -0,0 +1,12 @@ +export const IZ_ZOOM = + typeof window !== "undefined" && + (window?.navigator?.userAgent?.includes("ZoomWebKit") || + window?.navigator?.userAgent?.includes("ZoomApps")); +export const IS_DESKTOP_EDITOR = + typeof window !== "undefined" + ? window["AscDesktopEditor"] !== undefined + : false; +export const IS_VIEW = + typeof window !== "undefined" + ? window.location.search.indexOf("action=view") !== -1 + : false; diff --git a/packages/doceditor/src/utils/events.ts b/packages/doceditor/src/utils/events.ts new file mode 100644 index 0000000000..ef8eee3046 --- /dev/null +++ b/packages/doceditor/src/utils/events.ts @@ -0,0 +1,82 @@ +import { TFile } from "@docspace/shared/api/files/types"; +import { frameCallCommand } from "@docspace/shared/utils/common"; + +import { convertDocumentUrl } from "."; +import { updateFile } from "@docspace/shared/api/files"; + +export type TInfoEvent = { data: { mode: string } }; + +export const onSDKInfo = (event: object) => { + const data = (event as TInfoEvent).data; + + console.log("ONLYOFFICE Document Editor is opened in mode " + data.mode); +}; + +export type TWarningEvent = { + data: { warningCode: string; warningDescription: string }; +}; + +export const onSDKWarning = (event: object) => { + const data = (event as TWarningEvent).data; + frameCallCommand("setIsLoaded"); + console.log( + "ONLYOFFICE Document Editor reports a warning: code " + + data.warningCode + + ", description " + + data.warningDescription, + ); +}; + +export type TErrorEvent = { + data: { errorCode: string; errorDescription: string }; +}; + +export const onSDKError = (event: object) => { + const data = (event as TErrorEvent).data; + frameCallCommand("setIsLoaded"); + console.log( + "ONLYOFFICE Document Editor reports an error: code " + + data.errorCode + + ", description " + + data.errorDescription, + ); +}; + +export const onSDKRequestHistoryClose = () => { + document.location.reload(); +}; + +export const onSDKRequestEditRights = async (fileInfo: TFile) => { + console.log("ONLYOFFICE Document Editor requests editing rights"); + const url = window.location.href; + + const index = url.indexOf("&action=view"); + + if (index) { + let convertUrl = url.substring(0, index); + + if ( + fileInfo?.viewAccessibility?.MustConvert && + fileInfo?.security?.Convert + ) { + const newUrl = await convertDocumentUrl(fileInfo.id); + if (newUrl) { + convertUrl = newUrl.webUrl; + } + } + history.pushState({}, "", convertUrl); + document.location.reload(); + } +}; + +export type TRenameEvent = { + data: string; +}; + +export const onSDKRequestRename = async ( + event: object, + id: string | number, +) => { + const title = (event as TRenameEvent).data; + await updateFile(id, title); +}; diff --git a/packages/doceditor/src/utils/index.ts b/packages/doceditor/src/utils/index.ts index e677811c81..cdda822ead 100644 --- a/packages/doceditor/src/utils/index.ts +++ b/packages/doceditor/src/utils/index.ts @@ -1,6 +1,61 @@ import { toUrlParams } from "@docspace/shared/utils/common"; import { combineUrl } from "@docspace/shared/utils/combineUrl"; import { request } from "@docspace/shared/api/client"; +import { checkFillFormDraft, convertFile } from "@docspace/shared/api/files"; +import { TEditHistory } from "@docspace/shared/api/files/types"; +import { FolderType } from "@docspace/shared/enums"; + +export const getBackUrl = ( + rootFolderType: FolderType, + folderId: string | number, +) => { + const search = window.location.search; + const shareIndex = search.indexOf("share="); + const key = shareIndex > -1 ? search.substring(shareIndex + 6) : null; + + let backUrl = ""; + + if (rootFolderType === FolderType.Rooms) { + if (key) { + backUrl = `/rooms/share?key=${key}`; + } else { + backUrl = `/rooms/shared/${folderId}/filter?folder=${folderId}`; + } + } else { + backUrl = `/rooms/personal/filter?folder=${folderId}`; + } + + const url = window.location.href; + const origin = url.substring(0, url.indexOf("/doceditor")); + + return `${combineUrl(origin, backUrl)}`; +}; + +export const initForm = async (id: string | number) => { + const formUrl = await checkFillFormDraft(id); + history.pushState({}, "", formUrl); + + document.location.reload(); +}; + +export const showDocEditorMessage = async ( + url: string, + id: string | number, +) => { + const result = await convertDocumentUrl(id); + const splitUrl = url.split("#message/"); + + if (result) { + const newUrl = `${result.webUrl}#message/${splitUrl[1]}`; + + history.pushState({}, "", newUrl); + } +}; + +export const convertDocumentUrl = async (fileId: number | string) => { + const convert = await convertFile(fileId, null, true); + return convert && convert[0]?.result; +}; export const getDataSaveAs = async (params: string) => { try { @@ -47,3 +102,76 @@ export const saveAs = ( window.open(handlerUrl, "_blank"); } }; + +export const constructTitle = ( + firstPart: string, + secondPart: string, + reverse = false, +) => { + return !reverse + ? `${firstPart} - ${secondPart}` + : `${secondPart} - ${firstPart}`; +}; + +export const checkIfFirstSymbolInStringIsRtl = (str: string | null) => { + if (!str) return; + + const rtlRegexp = new RegExp( + /[\u04c7-\u0591\u05D0-\u05EA\u05F0-\u05F4\u0600-\u06FF]/, + ); + + return rtlRegexp.test(str[0]); +}; + +export const setDocumentTitle = ( + subTitle: string | null = null, + fileType: string, + documentReady: boolean, + successAuth: boolean, + callback?: (value: string) => void, +) => { + const organizationName = "ONLYOFFICE"; //TODO: Replace to API variant + const moduleTitle = "Documents"; //TODO: Replace to API variant + + let newSubTitle = subTitle; + + const isSubTitleRtl = checkIfFirstSymbolInStringIsRtl(subTitle); + + // needs to reverse filename and extension for rtl mode + if (newSubTitle && fileType && isSubTitleRtl) { + newSubTitle = `${fileType}.${newSubTitle.replace(`.${fileType}`, "")}`; + } + + let title; + + if (newSubTitle) { + if (successAuth && moduleTitle) { + title = constructTitle(newSubTitle, moduleTitle, isSubTitleRtl); + } else { + title = constructTitle(newSubTitle, organizationName, isSubTitleRtl); + } + } else if (moduleTitle && organizationName) { + title = constructTitle(moduleTitle, organizationName); + } else { + title = organizationName; + } + + if (documentReady) { + callback?.(title); + } + document.title = title; +}; + +export const getCurrentDocumentVersion = ( + fileHistory: TEditHistory[], + historyLength: number, +) => { + return window.location.search.indexOf("&version=") !== -1 + ? +window.location.search.split("&version=")[1] + : fileHistory[historyLength - 1].version; +}; + +export const getIsZoom = () => + typeof window !== "undefined" && + (window?.navigator?.userAgent?.includes("ZoomWebKit") || + window?.navigator?.userAgent?.includes("ZoomApps")); diff --git a/packages/doceditor/src/utils/initDesktop.ts b/packages/doceditor/src/utils/initDesktop.ts new file mode 100644 index 0000000000..c71c4e1ed7 --- /dev/null +++ b/packages/doceditor/src/utils/initDesktop.ts @@ -0,0 +1,48 @@ +import { IInitialConfig } from "@/types"; +import { + setEncryptionKeys, + getEncryptionAccess, +} from "@docspace/shared/api/files"; +import { TUser } from "@docspace/shared/api/people/types"; +import { toastr } from "@docspace/shared/components/toast"; +import { Nullable, TTranslation } from "@docspace/shared/types"; +import { regDesktop } from "@docspace/shared/utils/desktop"; + +const initDesktop = ( + cfg: IInitialConfig, + user: TUser, + fileId: string | number, + t: Nullable, +) => { + const encryptionKeys = cfg?.editorConfig?.encryptionKeys; + regDesktop( + user, + !!encryptionKeys, + encryptionKeys, + (keys) => { + setEncryptionKeys(keys); + }, + true, + (callback) => { + getEncryptionAccess?.(fileId) + ?.then((keys) => { + var data = { + keys, + }; + + callback?.(data); + }) + .catch((error) => { + toastr.error( + typeof error === "string" ? error : error.message, + "", + 0, + true, + ); + }); + }, + t, + ); +}; + +export default initDesktop;