@finos/legend-application 9.0.1 → 10.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/lib/application/LegendApplication.d.ts.map +1 -1
  2. package/lib/application/LegendApplication.js +7 -0
  3. package/lib/application/LegendApplication.js.map +1 -1
  4. package/lib/components/ActionAlert.d.ts.map +1 -1
  5. package/lib/components/ActionAlert.js +4 -5
  6. package/lib/components/ActionAlert.js.map +1 -1
  7. package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts +10 -0
  8. package/lib/components/LegendApplicationComponentFrameworkProvider.d.ts.map +1 -1
  9. package/lib/components/LegendApplicationComponentFrameworkProvider.js +53 -2
  10. package/lib/components/LegendApplicationComponentFrameworkProvider.js.map +1 -1
  11. package/lib/components/VirtualAssistant.js +3 -4
  12. package/lib/components/VirtualAssistant.js.map +1 -1
  13. package/lib/components/execution-plan-viewer/ExecutionPlanViewer.d.ts.map +1 -1
  14. package/lib/components/execution-plan-viewer/ExecutionPlanViewer.js +2 -2
  15. package/lib/components/execution-plan-viewer/ExecutionPlanViewer.js.map +1 -1
  16. package/lib/components/shared/TextInputEditor.d.ts +5 -5
  17. package/lib/components/shared/TextInputEditor.d.ts.map +1 -1
  18. package/lib/components/shared/TextInputEditor.js +17 -16
  19. package/lib/components/shared/TextInputEditor.js.map +1 -1
  20. package/lib/components/{ApplicationNavigationContextServiceUtils.d.ts → useApplicationNavigationContext.d.ts} +1 -1
  21. package/lib/components/useApplicationNavigationContext.d.ts.map +1 -0
  22. package/lib/components/{ApplicationNavigationContextServiceUtils.js → useApplicationNavigationContext.js} +1 -1
  23. package/lib/components/useApplicationNavigationContext.js.map +1 -0
  24. package/lib/components/useCommands.d.ts +18 -0
  25. package/lib/components/useCommands.d.ts.map +1 -0
  26. package/lib/components/useCommands.js +23 -0
  27. package/lib/components/useCommands.js.map +1 -0
  28. package/lib/index.css +2 -2
  29. package/lib/index.css.map +1 -1
  30. package/lib/index.d.ts +3 -1
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +3 -1
  33. package/lib/index.js.map +1 -1
  34. package/lib/stores/ApplicationEvent.d.ts +2 -0
  35. package/lib/stores/ApplicationEvent.d.ts.map +1 -1
  36. package/lib/stores/ApplicationEvent.js +2 -0
  37. package/lib/stores/ApplicationEvent.js.map +1 -1
  38. package/lib/stores/ApplicationStore.d.ts +11 -0
  39. package/lib/stores/ApplicationStore.d.ts.map +1 -1
  40. package/lib/stores/ApplicationStore.js +33 -1
  41. package/lib/stores/ApplicationStore.js.map +1 -1
  42. package/lib/stores/CommandCenter.d.ts +45 -0
  43. package/lib/stores/CommandCenter.d.ts.map +1 -0
  44. package/lib/stores/CommandCenter.js +54 -0
  45. package/lib/stores/CommandCenter.js.map +1 -0
  46. package/lib/stores/DocumentationService.d.ts +3 -3
  47. package/lib/stores/DocumentationService.d.ts.map +1 -1
  48. package/lib/stores/DocumentationService.js.map +1 -1
  49. package/lib/stores/KeyboardShortcutsService.d.ts +55 -0
  50. package/lib/stores/KeyboardShortcutsService.d.ts.map +1 -0
  51. package/lib/stores/KeyboardShortcutsService.js +113 -0
  52. package/lib/stores/KeyboardShortcutsService.js.map +1 -0
  53. package/lib/stores/LegendApplicationPlugin.d.ts +5 -0
  54. package/lib/stores/LegendApplicationPlugin.d.ts.map +1 -1
  55. package/lib/stores/LegendApplicationPlugin.js.map +1 -1
  56. package/lib/stores/PureLanguageSupport.d.ts.map +1 -1
  57. package/lib/stores/PureLanguageSupport.js +10 -1
  58. package/lib/stores/PureLanguageSupport.js.map +1 -1
  59. package/lib/stores/WebApplicationNavigator.d.ts +37 -26
  60. package/lib/stores/WebApplicationNavigator.d.ts.map +1 -1
  61. package/lib/stores/WebApplicationNavigator.js +58 -32
  62. package/lib/stores/WebApplicationNavigator.js.map +1 -1
  63. package/lib/stores/WebApplicationRouter.d.ts +1 -1
  64. package/lib/stores/WebApplicationRouter.d.ts.map +1 -1
  65. package/lib/stores/WebApplicationRouter.js +1 -1
  66. package/lib/stores/WebApplicationRouter.js.map +1 -1
  67. package/package.json +10 -10
  68. package/src/application/LegendApplication.tsx +8 -0
  69. package/src/components/ActionAlert.tsx +4 -5
  70. package/src/components/LegendApplicationComponentFrameworkProvider.tsx +88 -3
  71. package/src/components/VirtualAssistant.tsx +3 -3
  72. package/src/components/execution-plan-viewer/ExecutionPlanViewer.tsx +5 -9
  73. package/src/components/shared/TextInputEditor.tsx +14 -21
  74. package/src/components/{ApplicationNavigationContextServiceUtils.tsx → useApplicationNavigationContext.tsx} +0 -0
  75. package/src/components/useCommands.tsx +25 -0
  76. package/src/index.ts +3 -1
  77. package/src/stores/ApplicationEvent.ts +3 -0
  78. package/src/stores/ApplicationStore.ts +34 -1
  79. package/src/stores/CommandCenter.ts +89 -0
  80. package/src/stores/DocumentationService.ts +3 -3
  81. package/src/stores/KeyboardShortcutsService.ts +142 -0
  82. package/src/stores/LegendApplicationPlugin.ts +6 -0
  83. package/src/stores/PureLanguageSupport.ts +10 -0
  84. package/src/stores/WebApplicationNavigator.ts +102 -62
  85. package/src/stores/WebApplicationRouter.ts +1 -0
  86. package/tsconfig.json +4 -1
  87. package/lib/components/ApplicationNavigationContextServiceUtils.d.ts.map +0 -1
  88. package/lib/components/ApplicationNavigationContextServiceUtils.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@finos/legend-application",
3
- "version": "9.0.1",
3
+ "version": "10.0.1",
4
4
  "description": "Legend application core",
5
5
  "keywords": [
6
6
  "legend",
@@ -43,34 +43,34 @@
43
43
  "test:watch": "jest --watch"
44
44
  },
45
45
  "dependencies": {
46
- "@finos/legend-art": "4.0.1",
47
- "@finos/legend-graph": "18.0.1",
48
- "@finos/legend-shared": "6.1.4",
46
+ "@finos/legend-art": "4.1.0",
47
+ "@finos/legend-graph": "19.1.0",
48
+ "@finos/legend-shared": "6.1.5",
49
49
  "@types/css-font-loading-module": "0.0.7",
50
50
  "@types/react": "18.0.21",
51
51
  "@types/react-dom": "18.0.6",
52
52
  "@types/react-router-dom": "5.3.3",
53
- "date-fns": "2.29.3",
54
53
  "history": "5.3.0",
55
54
  "mobx": "6.6.2",
56
55
  "mobx-react-lite": "3.4.0",
57
- "monaco-editor": "0.34.0",
56
+ "monaco-editor": "0.34.1",
58
57
  "react": "18.2.0",
59
58
  "react-dnd": "16.0.1",
60
59
  "react-dnd-html5-backend": "16.0.1",
61
60
  "react-dom": "18.2.0",
62
61
  "react-draggable": "4.4.5",
62
+ "react-hotkeys": "2.0.0",
63
63
  "react-router": "5.3.4",
64
64
  "react-router-dom": "5.3.4",
65
- "serializr": "2.0.5",
65
+ "serializr": "3.0.1",
66
66
  "sql-formatter": "11.0.2"
67
67
  },
68
68
  "devDependencies": {
69
- "@finos/legend-dev-utils": "2.0.20",
70
- "@jest/globals": "29.1.2",
69
+ "@finos/legend-dev-utils": "2.0.21",
70
+ "@jest/globals": "29.2.1",
71
71
  "cross-env": "7.0.3",
72
72
  "eslint": "8.25.0",
73
- "jest": "29.1.2",
73
+ "jest": "29.2.1",
74
74
  "npm-run-all": "4.1.5",
75
75
  "rimraf": "3.0.2",
76
76
  "sass": "1.55.0",
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { configure as configureMobx } from 'mobx';
18
18
  import { editor as monacoEditorAPI } from 'monaco-editor';
19
+ import { configure as configureReactHotkeys } from 'react-hotkeys';
19
20
  import { MONOSPACED_FONT_FAMILY } from '../const.js';
20
21
  import type {
21
22
  LegendApplicationConfig,
@@ -131,6 +132,13 @@ export const setupLegendApplicationUILibrary = async (
131
132
  enforceActions: 'observed',
132
133
  });
133
134
 
135
+ configureReactHotkeys({
136
+ // By default, `react-hotkeys` will avoid capturing keys from input tags like <input>, <textarea>, <select>
137
+ // We want to listen to hotkey from every where in the app so we disable that
138
+ // See https://github.com/greena13/react-hotkeys#ignoring-events
139
+ ignoreTags: [],
140
+ });
141
+
134
142
  configureComponents();
135
143
  };
136
144
 
@@ -48,10 +48,6 @@ const ActionAlertContent = observer((props: { info: ActionAlertInfo }) => {
48
48
  actions.find((action) => action.default)?.handler?.();
49
49
  handleClose();
50
50
  };
51
- const onSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
52
- event.preventDefault();
53
- handleSubmit();
54
- };
55
51
 
56
52
  return (
57
53
  <Dialog
@@ -62,7 +58,10 @@ const ActionAlertContent = observer((props: { info: ActionAlertInfo }) => {
62
58
  }}
63
59
  >
64
60
  <form
65
- onSubmit={onSubmit}
61
+ onSubmit={(event) => {
62
+ event.preventDefault();
63
+ handleSubmit();
64
+ }}
66
65
  className={`modal search-modal modal--dark blocking-alert blocking-alert--${(
67
66
  type ?? ActionAlertType.STANDARD
68
67
  ).toLowerCase()}`}
@@ -14,27 +14,112 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { Backdrop, LegendStyleProvider } from '@finos/legend-art';
17
+ import { Backdrop, LegendStyleProvider, Portal } from '@finos/legend-art';
18
18
  import { observer } from 'mobx-react-lite';
19
19
  import { DndProvider } from 'react-dnd';
20
20
  import { HTML5Backend } from 'react-dnd-html5-backend';
21
+ import { type KeyMap, GlobalHotKeys } from 'react-hotkeys';
21
22
  import { ActionAlert } from './ActionAlert.js';
22
23
  import { useApplicationStore } from './ApplicationStoreProvider.js';
23
24
  import { BlockingAlert } from './BlockingAlert.js';
24
25
  import { NotificationManager } from './NotificationManager.js';
25
26
 
27
+ const APP_CONTAINER_ID = 'app.container';
28
+ const APP_BACKDROP_CONTAINER_ID = 'app.backdrop-container';
29
+
30
+ const buildReactHotkeysConfiguration = (
31
+ commandKeyMap: Map<string, string | undefined>,
32
+ handlerCreator: (
33
+ keyCombination: string,
34
+ ) => (keyEvent?: KeyboardEvent) => void,
35
+ ): [KeyMap, { [key: string]: (keyEvent?: KeyboardEvent) => void }] => {
36
+ const keyMap: Record<PropertyKey, string[]> = {};
37
+ commandKeyMap.forEach((keyCombination, commandKey) => {
38
+ if (keyCombination) {
39
+ keyMap[commandKey] = [keyCombination];
40
+ }
41
+ });
42
+ const handlers: Record<PropertyKey, (keyEvent?: KeyboardEvent) => void> = {};
43
+ commandKeyMap.forEach((keyCombination, commandKey) => {
44
+ if (keyCombination) {
45
+ handlers[commandKey] = handlerCreator(keyCombination);
46
+ }
47
+ });
48
+ return [keyMap, handlers];
49
+ };
50
+
51
+ export const forceDispatchKeyboardEvent = (event: KeyboardEvent): void => {
52
+ document
53
+ .getElementById(APP_CONTAINER_ID)
54
+ ?.dispatchEvent(new KeyboardEvent(event.type, event));
55
+ };
56
+
57
+ /**
58
+ * Potential location to mount backdrop on
59
+ *
60
+ * NOTE: we usually want the backdrop container to be the first child of its immediate parent
61
+ * so that it properly lies under the content that we pick to show on top of the backdrop
62
+ */
63
+ export const BackdropContainer: React.FC<{ elementID: string }> = (props) => (
64
+ <div className="backdrop__container" id={props.elementID} />
65
+ );
66
+
26
67
  export const LegendApplicationComponentFrameworkProvider = observer(
27
68
  (props: { children: React.ReactNode }) => {
28
69
  const { children } = props;
29
70
  const applicationStore = useApplicationStore();
71
+ const backdropContainer = applicationStore.backdropContainerElementID
72
+ ? document.getElementById(applicationStore.backdropContainerElementID) ??
73
+ document.getElementById(APP_BACKDROP_CONTAINER_ID)
74
+ : document.getElementById(APP_BACKDROP_CONTAINER_ID);
75
+
76
+ const [keyMap, hotkeyHandlerMap] = buildReactHotkeysConfiguration(
77
+ applicationStore.keyboardShortcutsService.commandKeyMap,
78
+ (keyCombination: string) => (event?: KeyboardEvent) => {
79
+ // NOTE: Though tempting since it's a good way to simplify and potentially avoid conflicts,
80
+ // we should not call `preventDefault()` because if we have any hotkey which is too short, such as `r`, `a`
81
+ // we risk blocking some very common interaction, i.e. user typing, or even constructing longer
82
+ // key combinations
83
+ applicationStore.keyboardShortcutsService.dispatch(keyCombination);
84
+ },
85
+ );
30
86
 
31
87
  return (
32
88
  <LegendStyleProvider>
33
89
  <BlockingAlert />
34
90
  <ActionAlert />
35
91
  <NotificationManager />
36
- <Backdrop className="backdrop" open={applicationStore.showBackdrop} />
37
- <DndProvider backend={HTML5Backend}>{children}</DndProvider>
92
+ {applicationStore.showBackdrop && (
93
+ // We use <Portal> here to insert backdrop into different parts of the app
94
+ // as backdrop relies heavily on z-index mechanism so its location in the DOM
95
+ // really matters.
96
+ // For example, the default location of the backdrop works fine for most cases
97
+ // but if we want to use the backdrop for elements within modal dialogs, we would
98
+ // need to mount the backdrop at a different location
99
+ <Portal container={backdropContainer}>
100
+ <Backdrop
101
+ className="backdrop"
102
+ open={applicationStore.showBackdrop}
103
+ />
104
+ </Portal>
105
+ )}
106
+ <DndProvider backend={HTML5Backend}>
107
+ <GlobalHotKeys
108
+ keyMap={keyMap}
109
+ handlers={hotkeyHandlerMap}
110
+ allowChanges={true}
111
+ >
112
+ <div
113
+ className="app__container"
114
+ // NOTE: this `id` is used to quickly identify this DOM node so we could manually
115
+ // dispatch keyboard event here in order to be captured by our global hotkeys matchers
116
+ id={APP_CONTAINER_ID}
117
+ >
118
+ <BackdropContainer elementID={APP_BACKDROP_CONTAINER_ID} />
119
+ {children}
120
+ </div>
121
+ </GlobalHotKeys>
122
+ </DndProvider>
38
123
  </LegendStyleProvider>
39
124
  );
40
125
  },
@@ -42,10 +42,10 @@ import {
42
42
  ContentType,
43
43
  debounce,
44
44
  downloadFileUsingDataURI,
45
+ formatDate,
45
46
  isString,
46
47
  uuid,
47
48
  } from '@finos/legend-shared';
48
- import { format } from 'date-fns';
49
49
  import { observer } from 'mobx-react-lite';
50
50
  import { useEffect, useMemo, useRef, useState } from 'react';
51
51
  import { TAB_SIZE } from '../const.js';
@@ -284,7 +284,7 @@ const VirtualAssistantSearchPanel = observer(() => {
284
284
 
285
285
  const downloadDocRegistry = (): void => {
286
286
  downloadFileUsingDataURI(
287
- `documentation-registry_${format(
287
+ `documentation-registry_${formatDate(
288
288
  new Date(Date.now()),
289
289
  DATE_TIME_FORMAT,
290
290
  )}.json`,
@@ -298,7 +298,7 @@ const VirtualAssistantSearchPanel = observer(() => {
298
298
  };
299
299
  const downloadContextualDocIndex = (): void => {
300
300
  downloadFileUsingDataURI(
301
- `documentation-registry_${format(
301
+ `documentation-registry_${formatDate(
302
302
  new Date(Date.now()),
303
303
  DATE_TIME_FORMAT,
304
304
  )}.json`,
@@ -330,7 +330,8 @@ const ExecutionPlanViewPanel = observer(
330
330
  </button>
331
331
  </div>
332
332
  <DropdownMenu
333
- className="execution-plan-viewer__panel__view-mode"
333
+ className="execution-plan-viewer__panel__view-mode__type"
334
+ title="View as..."
334
335
  content={
335
336
  <MenuContent className="execution-plan-viewer__panel__view-mode__options execution-plan-viewer__panel__view-mode__options--with-group">
336
337
  <div className="execution-plan-viewer__panel__view-mode__option__group execution-plan-viewer__panel__view-mode__option__group--native">
@@ -358,14 +359,9 @@ const ExecutionPlanViewPanel = observer(
358
359
  transformOrigin: { vertical: 'top', horizontal: 'right' },
359
360
  }}
360
361
  >
361
- <button
362
- className="execution-plan-viewer__panel__view-mode__type"
363
- title="View as..."
364
- >
365
- <div className="execution-plan-viewer__panel__view-mode__type__label">
366
- {executionPlanState.viewMode}
367
- </div>
368
- </button>
362
+ <div className="execution-plan-viewer__panel__view-mode__type__label">
363
+ {executionPlanState.viewMode}
364
+ </div>
369
365
  </DropdownMenu>
370
366
  </div>
371
367
  <div className="panel__content execution-plan-viewer__panel__content">
@@ -22,7 +22,6 @@ import {
22
22
  } from 'monaco-editor';
23
23
  import {
24
24
  disposeEditor,
25
- disableEditorHotKeys,
26
25
  baseTextEditorSettings,
27
26
  resetLineNumberGutterWidth,
28
27
  getEditorValue,
@@ -31,10 +30,14 @@ import {
31
30
  } from '@finos/legend-art';
32
31
  import { type EDITOR_LANGUAGE, EDITOR_THEME, TAB_SIZE } from '../../const.js';
33
32
  import { useApplicationStore } from '../ApplicationStoreProvider.js';
33
+ import { forceDispatchKeyboardEvent } from '../LegendApplicationComponentFrameworkProvider.js';
34
34
 
35
- export type TextInputEditorOnKeyDownEventHandler = {
36
- matcher: (event: IKeyboardEvent) => boolean;
37
- action: (event: IKeyboardEvent) => void;
35
+ /**
36
+ * NOTE: `monaco-editor` does not bubble `keydown` event in natural order, we will
37
+ * have to manually do this in order to take advantage of application keyboard shortcuts service
38
+ */
39
+ export const createPassThroughOnKeyHandler = () => (event: IKeyboardEvent) => {
40
+ forceDispatchKeyboardEvent(event.browserEvent);
38
41
  };
39
42
 
40
43
  export const TextInputEditor: React.FC<{
@@ -47,7 +50,6 @@ export const TextInputEditor: React.FC<{
47
50
  | (monacoEditorAPI.IEditorOptions & monacoEditorAPI.IGlobalEditorOptions)
48
51
  | undefined;
49
52
  updateInput?: ((val: string) => void) | undefined;
50
- onKeyDownEventHandlers?: TextInputEditorOnKeyDownEventHandler[] | undefined;
51
53
  }> = (props) => {
52
54
  const {
53
55
  inputValue,
@@ -57,13 +59,11 @@ export const TextInputEditor: React.FC<{
57
59
  showMiniMap,
58
60
  hideGutter,
59
61
  extraEditorOptions,
60
- onKeyDownEventHandlers,
61
62
  } = props;
62
63
  const applicationStore = useApplicationStore();
63
64
  const [editor, setEditor] = useState<
64
65
  monacoEditorAPI.IStandaloneCodeEditor | undefined
65
66
  >();
66
- const onKeyDownEventDisposer = useRef<IDisposable | undefined>(undefined);
67
67
  const onDidChangeModelContentEventDisposer = useRef<IDisposable | undefined>(
68
68
  undefined,
69
69
  );
@@ -97,7 +97,13 @@ export const TextInputEditor: React.FC<{
97
97
  formatOnType: true,
98
98
  formatOnPaste: true,
99
99
  });
100
- disableEditorHotKeys(_editor);
100
+ // NOTE: if we ever set any hotkey explicitly, we would like to use the disposer partern instead
101
+ // else, we could risk triggering these hotkeys command multiple times
102
+ // e.g.
103
+ // const onKeyDownEventDisposer = useRef<IDisposable | undefined>(undefined);
104
+ // onKeyDownEventDisposer.current?.dispose();
105
+ // onKeyDownEventDisposer.current = editor.onKeyDown(() => ...)
106
+ _editor.onKeyDown(() => createPassThroughOnKeyHandler());
101
107
  setEditor(_editor);
102
108
  }
103
109
  }, [applicationStore, editor]);
@@ -124,19 +130,6 @@ export const TextInputEditor: React.FC<{
124
130
  }
125
131
  });
126
132
 
127
- // dispose to avoid trigger hotkeys multiple times
128
- // for a more extensive note on this, see `LambdaEditor`
129
- onKeyDownEventDisposer.current?.dispose();
130
- onKeyDownEventDisposer.current = editor.onKeyDown((event) => {
131
- onKeyDownEventHandlers?.forEach((handler) => {
132
- if (handler.matcher(event)) {
133
- event.preventDefault();
134
- event.stopPropagation();
135
- handler.action(event);
136
- }
137
- });
138
- });
139
-
140
133
  // Set the text value and editor options
141
134
  const currentValue = getEditorValue(editor);
142
135
  if (currentValue !== value) {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { useEffect } from 'react';
18
+ import type { CommandRegistrar } from '../stores/CommandCenter.js';
19
+
20
+ export const useCommands = (registrar: CommandRegistrar): void => {
21
+ useEffect(() => {
22
+ registrar.registerCommands();
23
+ return () => registrar.deregisterCommands();
24
+ }, [registrar]);
25
+ };
package/src/index.ts CHANGED
@@ -21,7 +21,8 @@ export * from './application/LegendApplication.js';
21
21
  export * from './components/ApplicationStoreProvider.js';
22
22
  export * from './components/WebApplicationNavigatorProvider.js';
23
23
  export * from './components/LegendApplicationComponentFrameworkProvider.js';
24
- export * from './components/ApplicationNavigationContextServiceUtils.js';
24
+ export * from './components/useApplicationNavigationContext.js';
25
+ export * from './components/useCommands.js';
25
26
  export * from './components/ApplicationStoreProviderTestUtils.js';
26
27
  export * from './components/WebApplicationNavigatorProviderTestUtils.js';
27
28
  // TODO: consider moving this to `LegendApplicationComponentFrameworkProvider`
@@ -34,6 +35,7 @@ export * from './stores/ApplicationEvent.js';
34
35
  export * from './application/LegendApplicationConfig.js';
35
36
  export { WebApplicationNavigator } from './stores/WebApplicationNavigator.js';
36
37
  export * from './stores/DocumentationService.js';
38
+ export * from './stores/CommandCenter.js';
37
39
  export * from './stores/EventService.js';
38
40
  export * from './stores/AssistantService.js';
39
41
  export * from './stores/ApplicationNavigationContextService.js';
@@ -24,8 +24,11 @@ export enum APPLICATION_EVENT {
24
24
  APPLICATION_DOCUMENTATION_FETCH_FAILURE = 'application.fetch.documentation.failure',
25
25
  APPLICATION_DOCUMENTATION_LOAD_SKIPPED = 'application.load.documentation.skipped',
26
26
  APPLICATION_DOCUMENTATION_REQUIREMENT_CHECK_FAILURE = 'application.load.documentation.requirement-check.failure',
27
+ APPLICATION_KEYBOARD_SHORTCUTS_CONFIGURATION_CHECK_FAILURE = 'application.load.keyboard-shortcuts.configuration-check.failure',
27
28
  APPLICATION_CONTEXTUAL_DOCUMENTATION_LOAD_SKIPPED = 'application.load.contextual-documentation.skipped',
28
29
 
30
+ APPLICATION_COMMAND_CENTER_REGISTRATION_FAILURE = 'application.command-center.registration.failure',
31
+
29
32
  APPLICATION_LOADED = 'application.load.success',
30
33
  APPLICATION_LOAD_FAILURE = 'application.load.failure',
31
34
 
@@ -35,6 +35,8 @@ import { AssistantService } from './AssistantService.js';
35
35
  import { EventService } from './EventService.js';
36
36
  import { ApplicationNavigationContextService } from './ApplicationNavigationContextService.js';
37
37
  import type { LegendApplicationPlugin } from './LegendApplicationPlugin.js';
38
+ import { CommandCenter } from './CommandCenter.js';
39
+ import { KeyboardShortcutsService } from './KeyboardShortcutsService.js';
38
40
 
39
41
  export enum ActionAlertType {
40
42
  STANDARD = 'STANDARD',
@@ -136,7 +138,15 @@ export class ApplicationStore<
136
138
  telemetryService = new TelemetryService();
137
139
  tracerService = new TracerService();
138
140
 
139
- // misc
141
+ // control and interactions
142
+ commandCenter: CommandCenter;
143
+ keyboardShortcutsService: KeyboardShortcutsService;
144
+
145
+ // TODO: config
146
+ // See https://github.com/finos/legend-studio/issues/407
147
+
148
+ // backdrop
149
+ backdropContainerElementID?: string | undefined;
140
150
  showBackdrop = false;
141
151
 
142
152
  // theme
@@ -153,7 +163,9 @@ export class ApplicationStore<
153
163
  blockingAlertInfo: observable,
154
164
  actionAlertInfo: observable,
155
165
  TEMPORARY__isLightThemeEnabled: observable,
166
+ backdropContainerElementID: observable,
156
167
  showBackdrop: observable,
168
+ setBackdropContainerElementID: action,
157
169
  setShowBackdrop: action,
158
170
  setBlockingAlert: action,
159
171
  setActionAlertInfo: action,
@@ -180,6 +192,8 @@ export class ApplicationStore<
180
192
  this.telemetryService.registerPlugins(
181
193
  pluginManager.getTelemetryServicePlugins(),
182
194
  );
195
+ this.commandCenter = new CommandCenter(this);
196
+ this.keyboardShortcutsService = new KeyboardShortcutsService(this);
183
197
  this.tracerService.registerPlugins(pluginManager.getTracerServicePlugins());
184
198
  this.eventService.registerEventNotifierPlugins(
185
199
  pluginManager.getEventNotifierPlugins(),
@@ -190,11 +204,25 @@ export class ApplicationStore<
190
204
  this.TEMPORARY__isLightThemeEnabled = val;
191
205
  }
192
206
 
207
+ /**
208
+ * Change the ID used to find the base element to mount the backdrop on.
209
+ * This is useful when we want to use backdrop with embedded application which
210
+ * requires its own backdrop usage.
211
+ */
212
+ setBackdropContainerElementID(val: string | undefined): void {
213
+ this.backdropContainerElementID = val;
214
+ }
215
+
193
216
  setShowBackdrop(val: boolean): void {
194
217
  this.showBackdrop = val;
195
218
  }
196
219
 
197
220
  setBlockingAlert(alertInfo: BlockingAlertInfo | undefined): void {
221
+ if (alertInfo) {
222
+ this.keyboardShortcutsService.blockGlobalHotkeys();
223
+ } else {
224
+ this.keyboardShortcutsService.unblockGlobalHotkeys();
225
+ }
198
226
  this.blockingAlertInfo = alertInfo;
199
227
  }
200
228
 
@@ -204,6 +232,11 @@ export class ApplicationStore<
204
232
  'Action alert is stacked: new alert is invoked while another one is being displayed',
205
233
  );
206
234
  }
235
+ if (alertInfo) {
236
+ this.keyboardShortcutsService.blockGlobalHotkeys();
237
+ } else {
238
+ this.keyboardShortcutsService.unblockGlobalHotkeys();
239
+ }
207
240
  this.actionAlertInfo = alertInfo;
208
241
  }
209
242
 
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { LogEvent } from '@finos/legend-shared';
18
+ import { action, makeObservable, observable } from 'mobx';
19
+ import { APPLICATION_EVENT } from './ApplicationEvent.js';
20
+ import type { GenericLegendApplicationStore } from './ApplicationStore.js';
21
+
22
+ export interface CommandRegistrar {
23
+ registerCommands(): void;
24
+ deregisterCommands(): void;
25
+ }
26
+ export type CommandConfigEntry = {
27
+ title?: string;
28
+ defaultKeyboardShortcut?: string;
29
+ when?: string;
30
+ };
31
+ export type KeyedCommandConfigEntry = {
32
+ key: string;
33
+ content: CommandConfigEntry;
34
+ };
35
+ export type CommandConfigData = Record<string, CommandConfigEntry>;
36
+ export const collectKeyedCommandConfigEntriesFromConfig = (
37
+ rawEntries: Record<string, CommandConfigEntry>,
38
+ ): KeyedCommandConfigEntry[] =>
39
+ Object.entries(rawEntries).map((entry) => ({
40
+ key: entry[0],
41
+ content: entry[1],
42
+ }));
43
+ export type Command = {
44
+ key: string;
45
+ trigger?: () => boolean;
46
+ action?: () => void;
47
+ };
48
+
49
+ export class CommandCenter {
50
+ readonly applicationStore: GenericLegendApplicationStore;
51
+ readonly commandRegistry = new Map<string, Command>();
52
+
53
+ constructor(applicationStore: GenericLegendApplicationStore) {
54
+ makeObservable(this, {
55
+ commandRegistry: observable,
56
+ registerCommand: action,
57
+ deregisterCommand: action,
58
+ });
59
+
60
+ this.applicationStore = applicationStore;
61
+ }
62
+
63
+ registerCommand(command: Command): void {
64
+ const commandKey = command.key;
65
+ if (this.commandRegistry.has(commandKey)) {
66
+ this.applicationStore.log.warn(
67
+ LogEvent.create(
68
+ APPLICATION_EVENT.APPLICATION_COMMAND_CENTER_REGISTRATION_FAILURE,
69
+ ),
70
+ `Can't register command: command is already registered`,
71
+ );
72
+ return;
73
+ }
74
+ this.commandRegistry.set(commandKey, command);
75
+ }
76
+
77
+ deregisterCommand(commandKey: string): void {
78
+ this.commandRegistry.delete(commandKey);
79
+ }
80
+
81
+ runCommand(commandKey: string): boolean {
82
+ const command = this.commandRegistry.get(commandKey);
83
+ if (command && (!command.trigger || command.trigger())) {
84
+ command.action?.();
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ }
@@ -122,10 +122,10 @@ export const collectContextualDocumnetationEntries = (
122
122
  }));
123
123
 
124
124
  export class DocumentationService {
125
- url?: string | undefined;
125
+ readonly url?: string | undefined;
126
126
 
127
- private docRegistry = new Map<string, DocumentationEntry>();
128
- private contextualDocIndex = new Map<string, DocumentationEntry>();
127
+ private readonly docRegistry = new Map<string, DocumentationEntry>();
128
+ private readonly contextualDocIndex = new Map<string, DocumentationEntry>();
129
129
 
130
130
  constructor(applicationStore: GenericLegendApplicationStore) {
131
131
  // set the main documenation site url