mirror of
https://github.com/The-Low-Code-Foundation/OpenNoodl.git
synced 2026-03-07 17:43:28 +01:00
Fixed Logic Builder node bugs, expression field bugs, code editor bugs, property panel bugs
This commit is contained in:
@@ -6,7 +6,8 @@ $_sidebar-hover-enter-offset: 250ms;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
max-width: 55vw;
|
||||
transition: width 0.3s ease-in-out;
|
||||
|
||||
&--expanded {
|
||||
|
||||
@@ -90,6 +90,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.CloseButton {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
background-color: var(--theme-color-bg-4);
|
||||
color: var(--theme-color-fg-default);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-color-bg-5);
|
||||
border-color: var(--theme-color-fg-default-shy);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Editor Container with CodeMirror */
|
||||
.EditorContainer {
|
||||
flex: 1;
|
||||
|
||||
@@ -27,6 +27,7 @@ export function JavaScriptEditor({
|
||||
value,
|
||||
onChange,
|
||||
onSave,
|
||||
onClose,
|
||||
validationType = 'expression',
|
||||
disabled = false,
|
||||
height,
|
||||
@@ -294,6 +295,23 @@ export function JavaScriptEditor({
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={() => {
|
||||
// Save before closing if onSave is available
|
||||
if (onSave) {
|
||||
const currentCode = editorViewRef.current?.state.doc.toString() || '';
|
||||
onSave(currentCode);
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
className={css['CloseButton']}
|
||||
title="Save and Close (Escape)"
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface JavaScriptEditorProps {
|
||||
/** Callback when user saves (Ctrl+S or Save button) */
|
||||
onSave?: (value: string) => void;
|
||||
|
||||
/** Callback when user closes the editor (Close button or Escape) */
|
||||
onClose?: () => void;
|
||||
|
||||
/** Validation type */
|
||||
validationType?: ValidationType;
|
||||
|
||||
|
||||
@@ -190,6 +190,7 @@ export function useSearchBar(
|
||||
closeAllCategories();
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchTerm]);
|
||||
|
||||
return setSearchTerm;
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
import { NodeType } from '@noodl-constants/NodeType';
|
||||
|
||||
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||
import { Collapsible } from '@noodl-core-ui/components/layout/Collapsible';
|
||||
import { Text, TextSize, TextType } from '@noodl-core-ui/components/typography/Text';
|
||||
import { Title, TitleSize, TitleVariant } from '@noodl-core-ui/components/typography/Title';
|
||||
@@ -83,12 +84,13 @@ export default function NodePickerCategory({
|
||||
</Text>
|
||||
</Collapsible>
|
||||
|
||||
<img
|
||||
className={classNames([
|
||||
<Icon
|
||||
icon={IconName.CaretRight}
|
||||
size={IconSize.Small}
|
||||
UNSAFE_className={classNames([
|
||||
css['Arrow'],
|
||||
isCollapsedState ? css['Arrow--is-collapsed'] : css['Arrow--is-not-collapsed']
|
||||
])}
|
||||
src="/assets/icons/editor/right_arrow_22.svg"
|
||||
/>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { createNodeIndex } from '@noodl-utils/createnodeindex';
|
||||
import { tracker } from '@noodl-utils/tracker';
|
||||
|
||||
import { HtmlRenderer } from '@noodl-core-ui/components/common/HtmlRenderer';
|
||||
import { Icon, IconName, IconSize } from '@noodl-core-ui/components/common/Icon';
|
||||
import { PrimaryButton, PrimaryButtonSize, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
import { SearchInput } from '@noodl-core-ui/components/inputs/SearchInput';
|
||||
import { Box } from '@noodl-core-ui/components/layout/Box';
|
||||
@@ -180,7 +181,7 @@ export function NodeLibrary({ model, parentModel, pos, attachToRoot, runtimeType
|
||||
createNewComment(model, pos);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={<img src="/assets/icons/comment.svg" />}
|
||||
icon={<Icon icon={IconName.Chat} size={IconSize.Default} />}
|
||||
/>
|
||||
</NodePickerSection>
|
||||
) : null}
|
||||
|
||||
@@ -183,6 +183,7 @@ export class NodeGraphEditor extends View {
|
||||
curtop = 0;
|
||||
inspectorsModel: DebugInspector.InspectorsModel;
|
||||
clearDeleteModeTimer: NodeJS.Timeout;
|
||||
lastBlocklyTabCloseTime: number = 0; // Track when Blockly tabs close to prevent accidental deletions
|
||||
|
||||
draggingNodes: NodeGraphEditorNode[] | null = null;
|
||||
|
||||
@@ -321,6 +322,8 @@ export class NodeGraphEditor extends View {
|
||||
'LogicBuilder.AllTabsClosed',
|
||||
() => {
|
||||
console.log('[NodeGraphEditor] All Logic Builder tabs closed - showing canvas');
|
||||
// Track close time to prevent accidental node deletions during focus transition
|
||||
this.lastBlocklyTabCloseTime = Date.now();
|
||||
this.setCanvasVisibility(true);
|
||||
},
|
||||
this
|
||||
@@ -393,27 +396,29 @@ export class NodeGraphEditor extends View {
|
||||
|
||||
// Load icons using webpack require to ensure proper bundling
|
||||
this.homeIcon = new Image();
|
||||
this.homeIcon.src = require('../../../assets/icons/core-ui-temp/home--nodegraph.svg');
|
||||
this.homeIcon.src = require('../../../assets/icons/core-ui-temp/home--nodegraph.svg').default;
|
||||
this.homeIcon.onload = () => this.repaint();
|
||||
this.homeIcon.onerror = (e) => console.error('Failed to load home icon:', e);
|
||||
|
||||
this.componentIcon = new Image();
|
||||
this.componentIcon.src = require('../../../assets/icons/core-ui-temp/component--nodegraph.svg');
|
||||
this.componentIcon.src = require('../../../assets/icons/core-ui-temp/component--nodegraph.svg').default;
|
||||
this.componentIcon.onload = () => this.repaint();
|
||||
this.componentIcon.onerror = (e) => console.error('Failed to load component icon:', e);
|
||||
|
||||
this.aiAssistantInnerIcon = new Image();
|
||||
this.aiAssistantInnerIcon.src = require('../../../assets/icons/core-ui-temp/aiAssistant--nodegraph-inner.svg');
|
||||
this.aiAssistantInnerIcon.src =
|
||||
require('../../../assets/icons/core-ui-temp/aiAssistant--nodegraph-inner.svg').default;
|
||||
this.aiAssistantInnerIcon.onload = () => this.repaint();
|
||||
this.aiAssistantInnerIcon.onerror = (e) => console.error('Failed to load AI assistant inner icon:', e);
|
||||
|
||||
this.aiAssistantOuterIcon = new Image();
|
||||
this.aiAssistantOuterIcon.src = require('../../../assets/icons/core-ui-temp/aiAssistant--nodegraph-outer.svg');
|
||||
this.aiAssistantOuterIcon.src =
|
||||
require('../../../assets/icons/core-ui-temp/aiAssistant--nodegraph-outer.svg').default;
|
||||
this.aiAssistantOuterIcon.onload = () => this.repaint();
|
||||
this.aiAssistantOuterIcon.onerror = (e) => console.error('Failed to load AI assistant outer icon:', e);
|
||||
|
||||
this.warningIcon = new Image();
|
||||
this.warningIcon.src = require('../../../assets/icons/core-ui-temp/warning_triangle.svg');
|
||||
this.warningIcon.src = require('../../../assets/icons/core-ui-temp/warning_triangle.svg').default;
|
||||
this.warningIcon.onload = () => this.repaint();
|
||||
this.warningIcon.onerror = (e) => console.error('Failed to load warning icon:', e);
|
||||
|
||||
@@ -1179,6 +1184,14 @@ export class NodeGraphEditor extends View {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Guard against accidental deletions during Blockly tab close transition
|
||||
// This prevents nodes from being deleted if a Blockly tab was just closed
|
||||
const timeSinceBlocklyClose = Date.now() - this.lastBlocklyTabCloseTime;
|
||||
if (timeSinceBlocklyClose < 200) {
|
||||
console.warn('[NodeGraphEditor] Ignoring delete during Blockly tab close transition');
|
||||
return false;
|
||||
}
|
||||
|
||||
const nodes = [...this.selector.nodes];
|
||||
|
||||
// Make sure all nodes can be deleted
|
||||
|
||||
@@ -196,8 +196,20 @@ export function useComponentsPanel(options: UseComponentsPanelOptions = {}) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// It's a folder
|
||||
setSelectedId(node.data.path);
|
||||
// Toggle folder if clicking on folder
|
||||
|
||||
// BUG-6 FIX: If it's a component-folder, open the component too
|
||||
// Component-folders are folders that also have an associated component
|
||||
// (e.g., App with App/Header as a child)
|
||||
if (node.data.isComponentFolder && node.data.component) {
|
||||
EventDispatcher.instance.notifyListeners('ComponentPanel.SwitchToComponent', {
|
||||
component: node.data.component,
|
||||
pushHistory: true
|
||||
});
|
||||
}
|
||||
|
||||
// Toggle folder expand/collapse
|
||||
toggleFolder(node.data.path);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -337,6 +337,11 @@ export class CodeEditorType extends TypeView {
|
||||
nodeId: nodeId
|
||||
});
|
||||
|
||||
// Create close handler to trigger popout close
|
||||
const closeHandler = () => {
|
||||
_this.parent.hidePopout();
|
||||
};
|
||||
|
||||
// Render JavaScriptEditor with proper sizing and history support
|
||||
// For read-only fields, don't pass nodeId/parameterName (no history tracking)
|
||||
this.popoutRoot.render(
|
||||
@@ -350,6 +355,7 @@ export class CodeEditorType extends TypeView {
|
||||
onSave: () => {
|
||||
save();
|
||||
},
|
||||
onClose: closeHandler,
|
||||
validationType,
|
||||
disabled: this.readOnly, // Enable read-only mode if port is marked readOnly
|
||||
width: props.initialSize?.x || 800,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { TypeView } from '../TypeView';
|
||||
import { getEditType } from '../utils';
|
||||
|
||||
/**
|
||||
* Hidden editor type for internal Logic Builder parameters
|
||||
* Renders nothing - used for parameters that need to be stored
|
||||
* but should not appear in the property panel
|
||||
*/
|
||||
export class LogicBuilderHiddenType extends TypeView {
|
||||
el: TSFixme;
|
||||
|
||||
static fromPort(args) {
|
||||
const view = new LogicBuilderHiddenType();
|
||||
|
||||
const p = args.port;
|
||||
const parent = args.parent;
|
||||
|
||||
view.port = p;
|
||||
view.displayName = p.displayName ? p.displayName : p.name;
|
||||
view.name = p.name;
|
||||
view.type = getEditType(p);
|
||||
view.group = null; // No group
|
||||
view.tooltip = p.tooltip;
|
||||
view.value = parent.model.getParameter(p.name);
|
||||
view.parent = parent;
|
||||
view.isConnected = parent.model.isPortConnected(p.name, 'target');
|
||||
view.isDefault = parent.model.parameters[p.name] === undefined;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Render an empty, invisible element
|
||||
// This is necessary because the property panel expects something to be returned
|
||||
// but we want it to take up no space
|
||||
this.el = $('<div style="display: none;"></div>');
|
||||
return this.el;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
import React from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
|
||||
import { EventDispatcher } from '../../../../../../shared/utils/EventDispatcher';
|
||||
import { GeneratedCodeModal } from '../GeneratedCodeModal';
|
||||
import { TypeView } from '../TypeView';
|
||||
import { getEditType } from '../utils';
|
||||
|
||||
@@ -10,6 +14,10 @@ import { getEditType } from '../utils';
|
||||
export class LogicBuilderWorkspaceType extends TypeView {
|
||||
el: TSFixme;
|
||||
editButton: JQuery;
|
||||
viewCodeButton: JQuery;
|
||||
modalContainer: HTMLDivElement | null = null;
|
||||
modalRoot: Root | null = null;
|
||||
isModalOpen: boolean = false;
|
||||
|
||||
static fromPort(args) {
|
||||
const view = new LogicBuilderWorkspaceType();
|
||||
@@ -42,7 +50,7 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
</style>
|
||||
`;
|
||||
|
||||
// Create a simple container with single button
|
||||
// Create a simple container with two buttons
|
||||
const html =
|
||||
hideEmptyGroupsCSS +
|
||||
`
|
||||
@@ -61,21 +69,42 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
"
|
||||
onmouseover="this.style.backgroundColor='var(--theme-color-primary-hover)'"
|
||||
onmouseout="this.style.backgroundColor='var(--theme-color-primary)'">
|
||||
View Logic Blocks
|
||||
Edit Logic Blocks
|
||||
</button>
|
||||
<button class="view-code-button"
|
||||
style="
|
||||
padding: 8px 16px;
|
||||
background: var(--theme-color-bg-3);
|
||||
color: var(--theme-color-fg-default);
|
||||
border: 1px solid var(--theme-color-border-default);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
"
|
||||
onmouseover="this.style.backgroundColor='var(--theme-color-bg-4)'"
|
||||
onmouseout="this.style.backgroundColor='var(--theme-color-bg-3)'">
|
||||
View Generated Code
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.el = this.bindView($(html), this);
|
||||
|
||||
// Get reference to button
|
||||
// Get references to buttons
|
||||
this.editButton = this.el.find('.edit-blocks-button');
|
||||
this.viewCodeButton = this.el.find('.view-code-button');
|
||||
|
||||
// Handle button click
|
||||
// Handle button clicks
|
||||
this.editButton.on('click', () => {
|
||||
this.onEditBlocksClicked();
|
||||
});
|
||||
|
||||
this.viewCodeButton.on('click', () => {
|
||||
this.onViewCodeClicked();
|
||||
});
|
||||
|
||||
// Call parent render for common functionality (tooltips, etc.)
|
||||
TypeView.prototype.render.call(this);
|
||||
|
||||
@@ -101,6 +130,46 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
});
|
||||
}
|
||||
|
||||
onViewCodeClicked() {
|
||||
const nodeName = this.parent?.model?.model?.label || this.parent?.model?.type?.displayName || 'Logic Builder';
|
||||
const generatedCode = this.parent?.model?.getParameter('generatedCode') || '';
|
||||
|
||||
console.log('[LogicBuilderWorkspaceType] Opening generated code modal for node:', nodeName);
|
||||
|
||||
this.showModal(nodeName, generatedCode);
|
||||
}
|
||||
|
||||
showModal(nodeName: string, code: string) {
|
||||
// Create modal container if it doesn't exist
|
||||
if (!this.modalContainer) {
|
||||
this.modalContainer = document.createElement('div');
|
||||
this.modalContainer.id = 'generated-code-modal-container';
|
||||
document.body.appendChild(this.modalContainer);
|
||||
this.modalRoot = createRoot(this.modalContainer);
|
||||
}
|
||||
|
||||
this.isModalOpen = true;
|
||||
this.renderModal(nodeName, code);
|
||||
}
|
||||
|
||||
hideModal() {
|
||||
this.isModalOpen = false;
|
||||
this.renderModal('', '');
|
||||
}
|
||||
|
||||
renderModal(nodeName: string, code: string) {
|
||||
if (!this.modalRoot) return;
|
||||
|
||||
this.modalRoot.render(
|
||||
React.createElement(GeneratedCodeModal, {
|
||||
isOpen: this.isModalOpen,
|
||||
nodeName: nodeName,
|
||||
code: code,
|
||||
onClose: () => this.hideModal()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateChangedDot() {
|
||||
const dot = this.el.find('.property-changed-dot');
|
||||
if (this.isDefault) {
|
||||
@@ -119,4 +188,16 @@ export class LogicBuilderWorkspaceType extends TypeView {
|
||||
this.isDefault = true;
|
||||
this.updateChangedDot();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Clean up modal when view is disposed
|
||||
if (this.modalRoot) {
|
||||
this.modalRoot.unmount();
|
||||
this.modalRoot = null;
|
||||
}
|
||||
if (this.modalContainer && this.modalContainer.parentNode) {
|
||||
this.modalContainer.parentNode.removeChild(this.modalContainer);
|
||||
this.modalContainer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { FontType } from './FontType';
|
||||
import { IconType } from './IconType';
|
||||
import { IdentifierType } from './IdentifierType';
|
||||
import { ImageType } from './ImageType';
|
||||
import { LogicBuilderHiddenType } from './LogicBuilderHiddenType';
|
||||
import { LogicBuilderWorkspaceType } from './LogicBuilderWorkspaceType';
|
||||
import { MarginPaddingType } from './MarginPaddingType';
|
||||
import { NumberWithUnits } from './NumberWithUnits';
|
||||
@@ -226,6 +227,11 @@ export class Ports extends View {
|
||||
return LogicBuilderWorkspaceType;
|
||||
}
|
||||
|
||||
// Hidden type for internal Logic Builder parameters (renders nothing)
|
||||
if (typeof type === 'object' && type.editorType === 'logic-builder-hidden') {
|
||||
return LogicBuilderHiddenType;
|
||||
}
|
||||
|
||||
// Align tools types
|
||||
function isOfAlignToolsType() {
|
||||
return NodeLibrary.nameForPortType(type) === 'enum' && typeof type === 'object' && type.alignComp !== undefined;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
.Overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.Modal {
|
||||
background-color: var(--theme-color-bg-2, #1a1a1a);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
width: 800px;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--theme-color-border-default, rgba(255, 255, 255, 0.1));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.Title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--theme-color-fg-highlight, #ffffff);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ReadOnlyBadge {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--theme-color-bg-3, rgba(255, 255, 255, 0.1));
|
||||
color: var(--theme-color-fg-default-shy, rgba(255, 255, 255, 0.6));
|
||||
}
|
||||
|
||||
.InfoBar {
|
||||
padding: 8px 20px;
|
||||
background-color: var(--theme-color-bg-3, rgba(255, 255, 255, 0.05));
|
||||
border-bottom: 1px solid var(--theme-color-border-default, rgba(255, 255, 255, 0.1));
|
||||
}
|
||||
|
||||
.InfoText {
|
||||
font-size: 12px;
|
||||
color: var(--theme-color-fg-default-shy, rgba(255, 255, 255, 0.6));
|
||||
}
|
||||
|
||||
.Body {
|
||||
padding: 16px 20px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.EditorWrapper {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.Footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--theme-color-border-default, rgba(255, 255, 255, 0.1));
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* GeneratedCodeModal
|
||||
*
|
||||
* A read-only modal for viewing generated code from Logic Builder.
|
||||
* Uses the CodeMirror-based JavaScriptEditor in read-only mode.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { JavaScriptEditor } from '@noodl-core-ui/components/code-editor';
|
||||
import { IconName } from '@noodl-core-ui/components/common/Icon';
|
||||
import { PrimaryButton, PrimaryButtonVariant } from '@noodl-core-ui/components/inputs/PrimaryButton';
|
||||
|
||||
import css from './GeneratedCodeModal.module.scss';
|
||||
|
||||
export interface GeneratedCodeModalProps {
|
||||
/** Whether the modal is open */
|
||||
isOpen: boolean;
|
||||
/** The node name for display */
|
||||
nodeName: string;
|
||||
/** The generated code to display */
|
||||
code: string;
|
||||
/** Called when modal is closed */
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only modal for viewing generated JavaScript code
|
||||
*/
|
||||
export function GeneratedCodeModal({ isOpen, nodeName, code, onClose }: GeneratedCodeModalProps) {
|
||||
const codeRef = useRef<string>(code);
|
||||
|
||||
// Keep code ref updated
|
||||
useEffect(() => {
|
||||
codeRef.current = code;
|
||||
}, [code]);
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[isOpen, onClose]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [handleKeyDown]);
|
||||
|
||||
// Handle copy to clipboard
|
||||
const handleCopy = useCallback(() => {
|
||||
if (codeRef.current) {
|
||||
navigator.clipboard.writeText(codeRef.current).then(
|
||||
() => {
|
||||
console.log('[GeneratedCodeModal] Code copied to clipboard');
|
||||
},
|
||||
(err) => {
|
||||
console.error('[GeneratedCodeModal] Failed to copy code:', err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const displayCode =
|
||||
code || '// No code generated yet.\n// Add some blocks in the Logic Builder and close the editor.';
|
||||
|
||||
// Render into portal to escape any z-index issues
|
||||
return ReactDOM.createPortal(
|
||||
<div className={css['Overlay']} onClick={onClose}>
|
||||
<div className={css['Modal']} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className={css['Header']}>
|
||||
<span className={css['Title']}>Generated Code: {nodeName}</span>
|
||||
<span className={css['ReadOnlyBadge']}>Read-Only</span>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className={css['InfoBar']}>
|
||||
<span className={css['InfoText']}>
|
||||
This is the JavaScript generated from your logic blocks. You can copy it but not edit it directly.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Body with editor */}
|
||||
<div className={css['Body']}>
|
||||
<div className={css['EditorWrapper']}>
|
||||
<JavaScriptEditor
|
||||
value={displayCode}
|
||||
onChange={() => {}} // No-op since read-only
|
||||
validationType="script"
|
||||
height={400}
|
||||
width={700}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with buttons */}
|
||||
<div className={css['Footer']}>
|
||||
<PrimaryButton
|
||||
label="Copy Code"
|
||||
variant={PrimaryButtonVariant.Muted}
|
||||
icon={IconName.Copy}
|
||||
onClick={handleCopy}
|
||||
isDisabled={!code}
|
||||
/>
|
||||
<PrimaryButton label="Close" variant={PrimaryButtonVariant.Cta} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { GeneratedCodeModal, type GeneratedCodeModalProps } from './GeneratedCodeModal';
|
||||
@@ -231,24 +231,25 @@ const LogicBuilderNode = {
|
||||
}
|
||||
},
|
||||
generatedCode: {
|
||||
// Internal storage - renders nothing in property panel
|
||||
type: {
|
||||
name: 'string',
|
||||
allowEditOnly: true,
|
||||
codeeditor: 'javascript',
|
||||
readOnly: true // ✅ Inside type object - this gets passed through to property panel!
|
||||
editorType: 'logic-builder-hidden' // Custom type that renders nothing
|
||||
},
|
||||
displayName: 'Generated code',
|
||||
group: 'Advanced',
|
||||
displayName: 'Generated Code',
|
||||
group: '', // Empty group
|
||||
set: function (value) {
|
||||
const internal = this._internal;
|
||||
internal.generatedCode = value;
|
||||
internal.compiledFunction = null; // Reset compiled function
|
||||
internal.compiledFunction = null; // Reset compiled function when code changes
|
||||
}
|
||||
},
|
||||
run: {
|
||||
type: 'signal',
|
||||
displayName: 'Run',
|
||||
group: 'Signals',
|
||||
editorName: 'hidden', // Hide from property panel - signal comes from dynamic ports
|
||||
valueChangedToTrue: function () {
|
||||
this._executeLogic('run');
|
||||
}
|
||||
@@ -260,6 +261,7 @@ const LogicBuilderNode = {
|
||||
group: 'Status',
|
||||
type: 'string',
|
||||
displayName: 'Error',
|
||||
editorName: 'hidden', // Hide from property panel
|
||||
getter: function () {
|
||||
return this._internal.executionError || '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user