sample-ui-component-library 0.0.50-dev → 0.0.52-dev

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sample-ui-component-library",
3
- "version": "0.0.50-dev",
3
+ "version": "0.0.52-dev",
4
4
  "description": "A library which contains sample UI elements that can be used for populating layouts.",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -10,7 +10,8 @@ import React, {
10
10
  useImperativeHandle,
11
11
  useMemo,
12
12
  useReducer,
13
- useContext
13
+ useContext,
14
+ useRef,
14
15
  } from "react";
15
16
 
16
17
  import { EditorContext } from "./EditorContext";
@@ -27,14 +28,19 @@ const MODES = {
27
28
  *
28
29
  * @return {JSX}
29
30
  */
30
- export const Editor = forwardRef(({ }, ref) => {
31
+ export const Editor = forwardRef(({ onSelectAbstraction, onSelectTab }, ref) => {
31
32
  const [state, dispatch] = useReducer(editorReducer, initialState);
33
+ const editorRef = useRef();
32
34
 
33
35
  const selectTab = useCallback((id) => {
34
36
  dispatch({ type: "SELECT_TAB", payload: id });
35
- }, []);
37
+ if (onSelectTab) {
38
+ onSelectTab(id);
39
+ }
40
+ }, [onSelectTab]);
36
41
 
37
42
  const closeTab = useCallback((id) => {
43
+ editorRef.current && editorRef.current.clearModel(id);
38
44
  dispatch({ type: "CLOSE_TAB", payload: id });
39
45
  }, []);
40
46
 
@@ -59,18 +65,50 @@ export const Editor = forwardRef(({ }, ref) => {
59
65
  dispatch({ type: "SET_MODE", payload: mode });
60
66
  }, []);
61
67
 
68
+ const setMappedIds = useCallback((ids) => {
69
+ dispatch({ type: "SET_MAPPED_IDS", payload: ids });
70
+ }, []);
71
+
72
+ const getTabs = useCallback(() => {
73
+ return state.tabs;
74
+ }, [state.tabs]);
75
+
76
+ const getTab = useCallback((id) => {
77
+ return state.tabs.find((tab) => tab.uid === id);
78
+ }, [state.tabs]);
79
+
80
+ const getActiveTab = useCallback(() => {
81
+ return state.tabs.find((tab) => tab.id === state.activeTabId);
82
+ }, [state.tabs, state.activeTabId]);
83
+
84
+ const setUpdatedContent = useCallback((tab, content) => {
85
+ dispatch({ type: "SET_UPDATED_CONTENT", payload: {tab, content} });
86
+ }, []);
87
+
88
+ const setContent = useCallback((tab, content) => {
89
+ dispatch({ type: "SET_CONTENT", payload: {tab, content} });
90
+ }, []);
91
+
92
+ const api_entries = {
93
+ state,
94
+ addTab,
95
+ setTabGroupId,
96
+ selectTab,
97
+ closeTab,
98
+ moveTab,
99
+ setMapping,
100
+ setMode,
101
+ setMappedIds,
102
+ getTab,
103
+ getTabs,
104
+ getActiveTab,
105
+ setUpdatedContent,
106
+ setContent
107
+ }
108
+
62
109
  const api = useMemo(() => {
63
- return {
64
- state,
65
- addTab,
66
- setTabGroupId,
67
- selectTab,
68
- closeTab,
69
- moveTab,
70
- setMapping,
71
- setMode
72
- };
73
- }, [state, addTab, selectTab, closeTab, moveTab, setTabGroupId, setMapping, setMode]);
110
+ return api_entries;
111
+ }, Object.values(api_entries));
74
112
 
75
113
  useImperativeHandle(ref, () => api, [api]);
76
114
 
@@ -81,7 +119,9 @@ export const Editor = forwardRef(({ }, ref) => {
81
119
  <Tabs />
82
120
  </div>
83
121
  <div className="monacoContainer">
84
- <MonacoInstance />
122
+ <MonacoInstance
123
+ ref={editorRef}
124
+ onSelectAbstraction={onSelectAbstraction}/>
85
125
  </div>
86
126
  </div>
87
127
  </EditorContext.Provider>
@@ -6,7 +6,9 @@ export const initialState = {
6
6
  activeTab: null,
7
7
  mode: EDITOR_MODES.DESIGN,
8
8
  mapping: new Map(),
9
- parentTabGroupId: null
9
+ parentTabGroupId: null,
10
+ mappedIds: [],
11
+ modifiedIndicator: false,
10
12
  };
11
13
 
12
14
  export const editorReducer = (state, action) => {
@@ -23,6 +25,11 @@ export const editorReducer = (state, action) => {
23
25
  activeTab: tabInfo
24
26
  };
25
27
  }
28
+
29
+ // Initialize updatedContent and isDirty properties for the tab.
30
+ // These are used to track whether the content of the tab has been modified since it was opened.
31
+ tab.updatedContent = tab.content;
32
+ tab.isDirty = false;
26
33
 
27
34
  // Insert tab at specific location if it was provided.
28
35
  let tabs = [...state.tabs];
@@ -121,10 +128,41 @@ export const editorReducer = (state, action) => {
121
128
  };
122
129
  }
123
130
 
131
+ case "SET_MAPPED_IDS": {
132
+ return {
133
+ ...state,
134
+ mappedIds: action.payload
135
+ };
136
+ }
137
+
124
138
  case "RESET_STATE": {
125
139
  return initialState;
126
140
  }
127
141
 
142
+ case "SET_UPDATED_CONTENT": {
143
+ const { tab, content } = action.payload;
144
+ tab.updatedContent = content;
145
+ tab.isDirty = (tab.content !== tab.updatedContent);
146
+
147
+ return {
148
+ ...state,
149
+ modifiedIndicator: !state.modifiedIndicator,
150
+ tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
151
+ };
152
+ }
153
+
154
+ case "SET_CONTENT": {
155
+ const { tab, content } = action.payload;
156
+ tab.content = content;
157
+ tab.isDirty = (tab.content !== tab.updatedContent);
158
+
159
+ return {
160
+ ...state,
161
+ modifiedIndicator: !state.modifiedIndicator,
162
+ tabs: state.tabs.map(t => t.uid === tab.uid ? tab : t)
163
+ };
164
+ }
165
+
128
166
  default: {
129
167
  return state;
130
168
  }
@@ -0,0 +1,184 @@
1
+ import React, { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+
4
+ import EDITOR_MODES from '../EDITOR_MODES';
5
+
6
+ import Editor from '@monaco-editor/react';
7
+
8
+ import "./MonacoInstance.scss"
9
+
10
+ import { useEditor } from "../Editor";
11
+
12
+ export const MonacoInstance = ({onSelectAbstraction, updateContent}) => {
13
+ const { state } = useEditor();
14
+ const [editorContent, setEditorContent] = useState("Loading content...");
15
+ const [showEditor, setShowEditor] = useState(false);
16
+ const activeTabRef = useRef(state.activeTab);
17
+
18
+ const editorRef = useRef(null);
19
+ const content = useRef();
20
+ const containerRef = useRef(null);
21
+ const frameRef = useRef(0);
22
+
23
+ const [overlayDivs, setOverlayDivs] = useState();
24
+
25
+ // When active tab changes, update the editor content and add overlays for the new active tab.
26
+ useLayoutEffect(() => {
27
+ if (state.activeTab) {
28
+ console.log(state.activeTab)
29
+ activeTabRef.current = state.activeTab;
30
+ setEditorContent(state.activeTab.updatedContent);
31
+ setShowEditor(true);
32
+ addOverlays();
33
+ } else {
34
+ setShowEditor(false);
35
+ }
36
+ }, [state.activeTab, editorRef]);
37
+
38
+ // When the editor content changes, update the content in the context for the active tab.
39
+ // Setup content change listener for the editor to update the content in the context
40
+ // and track dirty state of the tab.
41
+ useEffect(() => {
42
+ content.current = editorContent;
43
+ if (editorRef?.current && content.current !== undefined && content.current !== null) {
44
+ editorRef.current.setValue(content.current);
45
+ editorRef.current.getModel().onDidChangeContent((content) => {
46
+ updateContent(activeTabRef.current, editorRef.current.getValue());
47
+ });
48
+ }
49
+ }, [editorContent]);
50
+
51
+ /**
52
+ * Callback for when the Monaco Editor is mounted.
53
+ * @param {Object} editor
54
+ * @param {Object} monaco
55
+ */
56
+ const handleEditorDidMount = useCallback((editor, monaco) => {
57
+ editorRef.current = editor;
58
+ if (content?.current) {
59
+ editorRef.current.setValue(content.current);
60
+ }
61
+ editorRef.current.layout();
62
+ addOverlays();
63
+
64
+ }, []);
65
+
66
+ // Add overlays to editor for the given line ranges.
67
+ const addOverlays = useCallback(() => {
68
+ if (!editorRef.current) return;
69
+
70
+ if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
71
+ setOverlayDivs([]);
72
+ return;
73
+ }
74
+
75
+ const ranges = state.mapping.get(state.activeTab?.name);
76
+ const lineCount = editorRef.current.getModel().getLineCount();
77
+ const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
78
+ const divs = [];
79
+
80
+ ranges.forEach((entry) => {
81
+ const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
82
+ let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
83
+ if (entry.end_line >= lineCount) {
84
+ bottom = bottom + lineHeight;
85
+ }
86
+
87
+ const style= {
88
+ top: top + "px",
89
+ height: (bottom - top) + "px"
90
+ }
91
+ if (state.mappedIds.includes(entry.uid)) {
92
+ style["backgroundColor"] = "rgba(255, 0, 0, 0.3)";
93
+ }
94
+ const overlayDiv = <div
95
+ className="line-block-overlay"
96
+ onClick={(e) => onSelectAbstraction(entry)}
97
+ style={style}>
98
+ </div>;
99
+ divs.push(overlayDiv);
100
+
101
+ });
102
+ setOverlayDivs(divs);
103
+ }, [editorRef?.current, state, onSelectAbstraction]);
104
+
105
+ // Scroll the editor and update overlays on wheel event.
106
+ const handleWheel = useCallback((e) => {
107
+ if (!editorRef.current) return;
108
+ const deltaY = e.deltaY;
109
+ const currentScrollTop = editorRef.current.getScrollTop();
110
+ editorRef.current.setScrollTop(currentScrollTop + deltaY);
111
+ addOverlays();
112
+ }, [state.activeTab, addOverlays]);
113
+
114
+ // Editor options for Monaco Editor.
115
+ const editorOptions = {
116
+ fontSize: "13px",
117
+ minimap: {
118
+ enabled: false
119
+ },
120
+ padding: {
121
+ top: 10
122
+ },
123
+ renderWhitespace: "none",
124
+ wordWrap: "on",
125
+ scrollBeyondLastLine: false,
126
+ automaticLayout: false
127
+ }
128
+
129
+ // Disable automatic layout and Manually layout the editor to avoid resize observer loops
130
+ useEffect(() => {
131
+ if (!containerRef.current) return;
132
+
133
+ const ro = new ResizeObserver(() => {
134
+ cancelAnimationFrame(frameRef.current);
135
+ frameRef.current = requestAnimationFrame(() => {
136
+ editorRef.current?.layout();
137
+ addOverlays();
138
+ });
139
+ });
140
+
141
+ ro.observe(containerRef.current);
142
+
143
+ return () => {
144
+ cancelAnimationFrame(frameRef.current);
145
+ ro.disconnect();
146
+ };
147
+ }, [state.activeTab, addOverlays]);
148
+
149
+ /**
150
+ * Render the editor if there is an active tab, otherwise render a placeholder message.
151
+ * @returns JSX
152
+ */
153
+ const renderEditor = () => {
154
+ if (showEditor) {
155
+ return (
156
+ <Editor
157
+ defaultLanguage="python"
158
+ defaultValue={editorContent}
159
+ onMount={handleEditorDidMount}
160
+ theme="vs-dark"
161
+ options={editorOptions}
162
+ />
163
+ );
164
+ } else {
165
+ return <div className="no-tab">Select file or drag and drop to view.</div>;
166
+ }
167
+ }
168
+
169
+ return (
170
+ <div className="editor-container" ref={containerRef}>
171
+ {renderEditor()}
172
+ {
173
+ state.mode === EDITOR_MODES.MAPPING &&
174
+ <div className="overlay-layer" onWheel={handleWheel}>
175
+ {overlayDivs}
176
+ </div>
177
+ }
178
+ </div>
179
+ )
180
+ }
181
+
182
+ MonacoInstance.propTypes = {
183
+ editorContent: PropTypes.string,
184
+ }
@@ -1,161 +1,145 @@
1
- import React, { useCallback, useLayoutEffect, useEffect, useRef, useState } from 'react';
2
- import PropTypes from 'prop-types';
1
+ import React, {
2
+ useCallback,
3
+ forwardRef,
4
+ useLayoutEffect,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ useMemo,
9
+ useImperativeHandle
10
+ } from "react";
11
+ import PropTypes from "prop-types";
3
12
 
4
- import EDITOR_MODES from '../EDITOR_MODES';
13
+ import { useEditor } from "../Editor";
5
14
 
6
- import Editor from '@monaco-editor/react';
15
+ import EDITOR_MODES from "../EDITOR_MODES";
7
16
 
8
- import "./MonacoInstance.scss"
17
+ import Editor from "@monaco-editor/react";
9
18
 
10
- import { useEditor } from "../Editor";
19
+ import "./MonacoInstance.scss";
20
+
21
+ export const MonacoInstance = forwardRef(({ onSelectAbstraction }, ref) => {
22
+ const { state, setUpdatedContent } = useEditor();
23
+ const [showEditor, setShowEditor] = useState(false);
24
+ const activeTabRef = useRef(state.activeTab);
11
25
 
12
- export const MonacoInstance = ({ }) => {
13
- const { state } = useEditor();
14
- const [editorContent, setEditorContent] = useState("Loading content...");
15
- const [showEditor, setShowEditor] = useState(false);
16
-
17
- const editorRef = useRef(null);
18
- const content = useRef();
19
- const containerRef = useRef(null);
20
- const frameRef = useRef(0);
21
-
22
- const [overlayDivs, setOverlayDivs] = useState();
23
-
24
- useLayoutEffect(() => {
25
- if (state.activeTab) {
26
- setEditorContent(state.activeTab.content);
27
- setShowEditor(true);
28
- addOverlays();
29
- } else {
30
- setShowEditor(false);
31
- }
32
- }, [state.activeTab, editorRef]);
33
-
34
- useEffect(() => {
35
- content.current = editorContent;
36
- if (editorRef?.current && content.current !== undefined && content.current !== null) {
37
- editorRef.current.setValue(content.current);
38
- }
39
- }, [editorContent]);
40
-
41
- /**
42
- * Callback for when the Monaco Editor is mounted.
43
- * @param {Object} editor
44
- * @param {Object} monaco
45
- */
46
- const handleEditorDidMount = useCallback((editor, monaco) => {
47
- editorRef.current = editor;
48
- if (content?.current) {
49
- editorRef.current.setValue(content.current);
50
- }
51
- editorRef.current.layout();
52
- addOverlays();
53
- }, [state.activeTab]);
54
-
55
- // Add overlays to editor for the given line ranges.
56
- const addOverlays = useCallback(() => {
57
- if (!editorRef.current) return;
58
-
59
- if (state.mode !== EDITOR_MODES.MAPPING || !state.mapping.get(state.activeTab?.name)) {
60
- setOverlayDivs([]);
61
- return;
62
- }
63
-
64
- const ranges = state.mapping.get(state.activeTab?.name);
65
- const lineCount = editorRef.current.getModel().getLineCount();
66
- const lineHeight = editorRef.current.getOption(monaco.editor.EditorOption.lineHeight);
67
- const divs = [];
68
-
69
- ranges.forEach((entry) => {
70
- const top = editorRef.current.getTopForLineNumber(entry.start_line) - editorRef.current.getScrollTop();
71
- let bottom = editorRef.current.getTopForLineNumber(entry.end_line + 1) - editorRef.current.getScrollTop();
72
- if (entry.end_line >= lineCount) {
73
- bottom = bottom + lineHeight;
26
+ const editorRef = useRef(null);
27
+ const modelsRef = useRef(new Map());
28
+
29
+ // When active tab changes, update the editor content and add overlays for the new active tab.
30
+ useEffect(() => {
31
+ if (state.activeTab) {
32
+ activeTabRef.current = state.activeTab;
33
+ editorRef.current && editorRef.current.setModel(getModel(state.activeTab));
34
+ setShowEditor(true);
35
+ } else {
36
+ activeTabRef.current = null;
37
+ setShowEditor(false);
38
+ }
39
+ }, [state.activeTab, editorRef]);
40
+
41
+ useEffect(() => {
42
+ return () => {
43
+ // Dispose all models on unmount to prevent memory leaks.
44
+ modelsRef.current.forEach((model) => model.dispose());
45
+ modelsRef.current.clear();
46
+ }
47
+ }, []);
48
+
49
+ // Get the model given the tab.
50
+ const getModel = useCallback((activeTab) => {
51
+ let model;
52
+ if (modelsRef.current.has(activeTab.uid)) {
53
+ model = modelsRef.current.get(activeTab.uid);
54
+ } else {
55
+ const uri = monaco.Uri.parse(`file:///${activeTab.uid}`);
56
+ model = monaco.editor.createModel(activeTab.content, "python", uri);
57
+ modelsRef.current.set(activeTab.uid, model);
58
+ }
59
+ model.onDidChangeContent((content) => {
60
+ setUpdatedContent(activeTabRef.current, editorRef.current.getValue());
61
+ });
62
+ return model;
63
+ }, [editorRef, modelsRef, setUpdatedContent]);
64
+
65
+ // Setup editor ref, and clear existing models on mount to prevent mem leak from old models.
66
+ const handleEditorDidMount = useCallback((editor, monaco) => {
67
+ editorRef.current = editor;
68
+ editor.layout();
69
+ modelsRef.current = new Map();
70
+ monaco.editor.getModels().forEach((model) => model.dispose());
71
+
72
+ if (activeTabRef.current) {
73
+ editorRef.current.setModel(getModel(activeTabRef.current));
74
+ setShowEditor(true);
74
75
  }
75
- const overlayDiv = <div className="line-block-overlay" style={{ top: top + "px", height: (bottom - top) + "px" }}></div>;
76
- divs.push(overlayDiv);
77
- });
78
- setOverlayDivs(divs);
79
- }, [editorRef?.current, state]);
80
-
81
- // Scroll the editor and update overlays on wheel event.
82
- const handleWheel = useCallback((e) => {
83
- if (!editorRef.current) return;
84
- const deltaY = e.deltaY;
85
- const currentScrollTop = editorRef.current.getScrollTop();
86
- editorRef.current.setScrollTop(currentScrollTop + deltaY);
87
- addOverlays();
88
- }, [state.activeTab, addOverlays]);
89
-
90
- // Editor options for Monaco Editor.
91
- const editorOptions = {
92
- fontSize: "13px",
93
- minimap: {
94
- enabled: false
95
- },
96
- padding: {
97
- top: 10
98
- },
99
- renderWhitespace: "none",
100
- wordWrap: "on",
101
- scrollBeyondLastLine: false,
102
- readOnly: true,
103
- automaticLayout: false
104
- }
105
-
106
- // Disable automatic layout and Manually layout the editor to avoid resize observer loops
107
- useEffect(() => {
108
- if (!containerRef.current) return;
109
-
110
- const ro = new ResizeObserver(() => {
111
- cancelAnimationFrame(frameRef.current);
112
- frameRef.current = requestAnimationFrame(() => {
113
- editorRef.current?.layout();
114
- addOverlays();
76
+ }, []);
77
+
78
+ // Disable automatic layout and Manually layout the editor to avoid resize observer loops
79
+ const containerRef = useRef(null);
80
+ const frameRef = useRef(0);
81
+ useEffect(() => {
82
+ if (!containerRef.current) return;
83
+ const ro = new ResizeObserver(() => {
84
+ cancelAnimationFrame(frameRef.current);
85
+ frameRef.current = requestAnimationFrame(() => {
86
+ editorRef.current?.layout();
87
+ });
115
88
  });
116
- });
117
-
118
- ro.observe(containerRef.current);
119
-
120
- return () => {
121
- cancelAnimationFrame(frameRef.current);
122
- ro.disconnect();
123
- };
124
- }, [state.activeTab, addOverlays]);
125
-
126
- /**
127
- * Render the editor if there is an active tab, otherwise render a placeholder message.
128
- * @returns JSX
129
- */
130
- const renderEditor = () => {
131
- if (showEditor) {
132
- return (
133
- <Editor
134
- defaultLanguage="python"
135
- defaultValue={editorContent}
136
- onMount={handleEditorDidMount}
137
- theme="vs-dark"
138
- options={editorOptions}
139
- />
140
- );
141
- } else {
142
- return <div className="no-tab">Select file or drag and drop to view.</div>;
143
- }
144
- }
145
-
146
- return (
147
- <div className="editor-container" ref={containerRef}>
148
- {renderEditor()}
149
- {
150
- state.mode === EDITOR_MODES.MAPPING &&
151
- <div className="overlay-layer" onWheel={handleWheel}>
152
- {overlayDivs}
153
- </div>
89
+ ro.observe(containerRef.current);
90
+ return () => {
91
+ cancelAnimationFrame(frameRef.current);
92
+ ro.disconnect();
93
+ };
94
+ }, [state.activeTab]);
95
+
96
+ // Setup imperative handle to clear model when tab is closed to prevent mem leak
97
+ const clearModel = useCallback((id) => {
98
+ if (modelsRef.current.has(id)) {
99
+ const model = modelsRef.current.get(id);
100
+ model.dispose();
101
+ modelsRef.current.delete(id);
154
102
  }
155
- </div>
156
- )
157
- }
103
+ }, []);
104
+
105
+ const api = useMemo(() => {
106
+ return {
107
+ clearModel,
108
+ };
109
+ }, [clearModel]);
110
+
111
+ useImperativeHandle(ref, () => api, [api]);
112
+
113
+ return (
114
+ <div className="editor-container" ref={containerRef}>
115
+ {
116
+ showEditor ?
117
+ <Editor
118
+ defaultLanguage="python"
119
+ defaultValue={"Loading content..."}
120
+ onMount={handleEditorDidMount}
121
+ theme="vs-dark"
122
+ options={{
123
+ minimap: { enabled: false },
124
+ padding: { top: 10 },
125
+ renderWhitespace: "none",
126
+ wordWrap: "on",
127
+ scrollBeyondLastLine: false,
128
+ automaticLayout: false,
129
+ }}
130
+ />:
131
+ <div className="no-tab">Select file or drag and drop to view.</div>
132
+ }
133
+ {state.mode === EDITOR_MODES.MAPPING && (
134
+ <div className="overlay-layer" onWheel={handleWheel}>
135
+ {overlayDivs}
136
+ </div>
137
+ )}
138
+ </div>
139
+ );
140
+ },
141
+ );
158
142
 
159
143
  MonacoInstance.propTypes = {
160
144
  editorContent: PropTypes.string,
161
- }
145
+ };
@@ -11,6 +11,7 @@
11
11
  position:relative;
12
12
  width:100%;
13
13
  height: 100%;
14
+ background: #1e1e1e;
14
15
  }
15
16
 
16
17
  .overlay-layer {