UNPKG

@finos/legend-studio

Version:
176 lines 12.5 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Fragment, useState, useEffect, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useResizeDetector } from 'react-resize-detector'; import { Backdrop, buildReactHotkeysConfiguration, getControlledResizablePanelProps, ResizablePanel, ResizablePanelGroup, ResizablePanelSplitter, ResizablePanelSplitterLine, useStateWithCallback, } from '@finos/legend-art'; import { AuxiliaryPanel } from './aux-panel/AuxiliaryPanel.js'; import { SideBar } from './side-bar/SideBar.js'; import { EditPanel, EditPanelSplashScreen } from './edit-panel/EditPanel.js'; import { GlobalHotKeys } from 'react-hotkeys'; import { GrammarTextEditor } from './edit-panel/GrammarTextEditor.js'; import { StatusBar } from './StatusBar.js'; import { ActivityBar } from './ActivityBar.js'; import { useParams, Prompt } from 'react-router'; import { ProjectSearchCommand } from '../editor/command-center/ProjectSearchCommand.js'; import { isNonNullable } from '@finos/legend-shared'; import { flowResult } from 'mobx'; import { useEditorStore, withEditorStore } from './EditorStoreProvider.js'; import { ActionAlertType, ActionAlertActionType, useApplicationStore, useApplicationNavigationContext, } from '@finos/legend-application'; import { WorkspaceType } from '@finos/legend-server-sdlc'; import { WorkspaceSyncConflictResolver } from './side-bar/WorkspaceSyncConflictResolver.js'; import { LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY } from '../../stores/LegendStudioApplicationNavigationContext.js'; export const Editor = withEditorStore(observer(() => { const params = useParams(); const projectId = params.projectId; const workspaceType = params .groupWorkspaceId ? WorkspaceType.GROUP : WorkspaceType.USER; const workspaceId = workspaceType === WorkspaceType.GROUP ? params.groupWorkspaceId : params.workspaceId; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); // Extensions const extraEditorExtensionComponents = editorStore.pluginManager .getApplicationPlugins() .flatMap((plugin) => plugin.getExtraEditorExtensionComponentRendererConfigurations?.() ?? []) .filter(isNonNullable) .map((config) => (_jsx(Fragment, { children: config.renderer(editorStore) }, config.key))); // Resize const { ref, width, height } = useResizeDetector(); // These create snapping effect on panel resizing const resizeSideBar = (handleProps) => editorStore.sideBarDisplayState.setSize(handleProps.domElement.getBoundingClientRect() .width); const resizeAuxPanel = (handleProps) => editorStore.auxPanelDisplayState.setSize(handleProps.domElement.getBoundingClientRect() .height); useEffect(() => { if (ref.current) { editorStore.auxPanelDisplayState.setMaxSize(ref.current.offsetHeight); } }, [editorStore, ref, height, width]); // Hotkeys const [hotkeyMapping, hotkeyHandlers] = buildReactHotkeysConfiguration(editorStore.hotkeys); // Cleanup the editor useEffect(() => () => editorStore.cleanUp(), [editorStore]); // Initialize the app useEffect(() => { flowResult(editorStore.initialize(projectId, workspaceId, workspaceType)).catch(applicationStore.alertUnhandledError); }, [editorStore, applicationStore, projectId, workspaceId, workspaceType]); // Browser Navigation Blocking (reload, close tab, go to another URL) // NOTE: there is no way to customize the alert message for now since Chrome removed support for it // See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Browser_compatibility // There is also no way to customize this modal style-wise so we would only be able to do so for route navigation blocking // See https://medium.com/@jozollo/blocking-navigation-with-react-router-v4-a3f2e359d096 useEffect(() => { const onUnload = (event) => { /** * NOTE: one subtle trick here. Since we have to use `useEffect` to set the event listener for `beforeunload` event, * we have to be be careful, if we extract `editorStore.ignoreNavigationBlock` out to a variable such as `ignoreNavigationBlock` * and then make this `useEffect` be called in response to that, there is a chance that if we set `editorStore.ignoreNavigationBlock` * to `true` and then go on to call systematic refresh (i.e. `window.location.reload()`) immediately, the event listener on window will * become stale and still show the blocking popup. * * This is almost guaranteed to happen as `useEffect` occurs after rendering, and thus will defnitely be called after the immediate * `window.location.reload()`. As such, the best way is instead of expecting `useEffect` to watch out for the change in `ignoreNavigationBlock` * we will access the value of `ignoreNavigationBlock` in the body of the `onUnload` function to make it more dynamic. This ensures the * event listener will never go stale */ const showAlert = editorStore.isInConflictResolutionMode || editorStore.hasUnpushedChanges; if (!editorStore.ignoreNavigationBlocking && showAlert) { event.returnValue = ''; } }; window.removeEventListener('beforeunload', onUnload); window.addEventListener('beforeunload', onUnload); return () => window.removeEventListener('beforeunload', onUnload); }, [editorStore]); // Route Navigation Blocking // See https://medium.com/@michaelchan_13570/using-react-router-v4-prompt-with-custom-modal-component-ca839f5faf39 const [blockedLocation, setBlockedLocation] = useState(); const retryBlockedLocation = useCallback((allowedNavigation) => { if (allowedNavigation && blockedLocation) { applicationStore.navigator.goTo(blockedLocation.pathname); } }, [blockedLocation, applicationStore]); // NOTE: we have to use `useStateWithCallback` here because we want to guarantee that we call `history.push(blockedLocation.pathname)` // after confirmedAllowNavigation is flipped, otherwise we would end up in the `false` case of handleBlockedNavigation again! // Another way to go about this is to use `setTimeout(() => history.push(...), 0)` but it can potentially be more error-prone // See https://www.robinwieruch.de/react-usestate-callback const [confirmedAllowNavigation, setConfirmedAllowNavigation] = useStateWithCallback(false, retryBlockedLocation); const onNavigationChangeIndicator = Boolean(editorStore.changeDetectionState.workspaceLocalLatestRevisionState.changes .length); const handleRouteNavigationBlocking = (nextLocation) => { // NOTE: as long as we're in conflict resolution, we want this block to be present const showAlert = editorStore.isInConflictResolutionMode || editorStore.hasUnpushedChanges; if (!editorStore.ignoreNavigationBlocking && !confirmedAllowNavigation && showAlert) { editorStore.setActionAlertInfo({ message: editorStore.isInConflictResolutionMode ? 'You have not accepted the conflict resolution, the current resolution will be discarded. Leave anyway?' : 'You have unpushed changes. Leave anyway?', type: ActionAlertType.CAUTION, onEnter: () => editorStore.setBlockGlobalHotkeys(true), onClose: () => editorStore.setBlockGlobalHotkeys(false), actions: [ { label: 'Leave this page', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: () => setConfirmedAllowNavigation(true), }, { label: 'Stay on this page', type: ActionAlertActionType.PROCEED, default: true, handler: () => setBlockedLocation(undefined), }, ], }); setBlockedLocation(nextLocation); return false; } // Reset the confirm flag and the blocked location here setBlockedLocation(undefined); setConfirmedAllowNavigation(false); return true; }; const editable = editorStore.graphManagerState.graphBuildState.hasCompleted && editorStore.isInitialized; const isResolvingConflicts = editorStore.isInConflictResolutionMode && !editorStore.conflictResolutionState.hasResolvedAllConflicts; const promptComponent = (_jsx(Prompt, { when: onNavigationChangeIndicator, message: handleRouteNavigationBlocking })); useApplicationNavigationContext(LEGEND_STUDIO_APPLICATION_NAVIGATION_CONTEXT_KEY.EDITOR); return (_jsx(DndProvider, { backend: HTML5Backend, children: _jsx("div", { className: "app__page", children: _jsxs("div", { className: "editor", children: [promptComponent, _jsxs(GlobalHotKeys, { keyMap: hotkeyMapping, handlers: hotkeyHandlers, allowChanges: true, children: [_jsxs("div", { className: "editor__body", children: [_jsx(ActivityBar, {}), _jsx(Backdrop, { className: "backdrop", open: editorStore.backdrop }), _jsx("div", { ref: ref, className: "editor__content-container", children: _jsx("div", { className: "editor__content", children: _jsxs(ResizablePanelGroup, { orientation: "vertical", children: [_jsx(ResizablePanel, { ...getControlledResizablePanelProps(editorStore.sideBarDisplayState.size === 0, { onStopResize: resizeSideBar, size: editorStore.sideBarDisplayState.size, }), direction: 1, children: _jsx(SideBar, {}) }), _jsx(ResizablePanelSplitter, {}), _jsx(ResizablePanel, { minSize: 300, children: _jsxs(ResizablePanelGroup, { orientation: "horizontal", children: [_jsxs(ResizablePanel, { ...getControlledResizablePanelProps(editorStore.auxPanelDisplayState.isMaximized), children: [(isResolvingConflicts || editable) && editorStore.isInFormMode && _jsx(EditPanel, {}), editable && editorStore.isInGrammarTextMode && (_jsx(GrammarTextEditor, {})), !editable && _jsx(EditPanelSplashScreen, {})] }), _jsx(ResizablePanelSplitter, { children: _jsx(ResizablePanelSplitterLine, { color: editorStore.auxPanelDisplayState.isMaximized ? 'transparent' : 'var(--color-dark-grey-250)' }) }), _jsx(ResizablePanel, { ...getControlledResizablePanelProps(editorStore.auxPanelDisplayState.size === 0, { onStopResize: resizeAuxPanel, size: editorStore.auxPanelDisplayState.size, }), direction: -1, children: _jsx(AuxiliaryPanel, {}) })] }) })] }) }) })] }), extraEditorExtensionComponents, _jsx(StatusBar, { actionsDisabled: !editable }), editable && _jsx(ProjectSearchCommand, {}), editorStore.localChangesState.workspaceSyncState .workspaceSyncConflictResolutionState.showModal && (_jsx(WorkspaceSyncConflictResolver, {}))] })] }) }) })); })); //# sourceMappingURL=Editor.js.map