@finos/legend-studio
Version:
176 lines • 12.5 kB
JavaScript
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