diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp.tsx index 944b63976..35b6d84b0 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp.tsx @@ -1,375 +1 @@ -import { EmptyContent } from "components/EmptyContent"; -import { HelpText } from "components/HelpText"; -import { Tabs } from "components/Tabs"; -import { - clearMockWindow, - clearStyleEval, - ConstructorToComp, - evalFunc, - evalStyle, - RecordConstructorToComp, -} from "lowcoder-core"; -import { CodeTextControl } from "comps/controls/codeTextControl"; -import SimpleStringControl from "comps/controls/simpleStringControl"; -import { MultiCompBuilder, withPropertyViewFn } from "comps/generators"; -import { list } from "comps/generators/list"; -import { BaseSection, CustomModal, PlusIcon, ScrollBar } from "lowcoder-design"; -import React, { useContext, useEffect, useState } from "react"; -import styled from "styled-components"; -import { ExternalEditorContext } from "util/context/ExternalEditorContext"; -import { runScriptInHost } from "util/commonUtils"; -import { getGlobalSettings } from "comps/utils/globalSettings"; -import { trans } from "i18n"; -import log from "loglevel"; -import { JSLibraryModal } from "components/JSLibraryModal"; -import { JSLibraryTree } from "components/JSLibraryTree"; -import { fetchJSLibrary } from "util/jsLibraryUtils"; - -export interface ExternalPreload { - css?: string; - libs?: string[]; - script?: string; - runJavaScriptInHost?: boolean; -} - -interface RunAndClearable { - run(id: string, externalPreload?: T): Promise; - - clear(): Promise; -} - -class LibsCompBase extends list(SimpleStringControl) implements RunAndClearable { - success: Record = {}; - globalVars: Record = {}; - externalLibs: string[] = []; - runInHost: boolean = false; - - getAllLibs() { - return this.externalLibs.concat(this.getView().map((i) => i.getView())); - } - - async loadScript(url: string) { - if (this.success[url]) { - return; - } - return fetchJSLibrary(url).then((code) => { - evalFunc( - code, - {}, - {}, - { - scope: "function", - disableLimit: this.runInHost, - onSetGlobalVars: (v: string) => { - this.globalVars[url] = this.globalVars[url] || []; - if (!this.globalVars[url].includes(v)) { - this.globalVars[url].push(v); - } - }, - } - ); - this.success[url] = true; - }); - } - - async loadAllLibs() { - const scriptRunners = this.getAllLibs().map((url) => - this.loadScript(url).catch((e) => { - log.warn(e); - }) - ); - - try { - await Promise.all(scriptRunners); - } catch (e) { - log.warn("load preload libs error:", e); - } - } - - async run(id: string, externalLibs: string[] = [], runInHost: boolean = false) { - this.externalLibs = externalLibs; - this.runInHost = runInHost; - return this.loadAllLibs(); - } - - async clear(): Promise { - clearMockWindow(); - } -} - -const LibsComp = withPropertyViewFn(LibsCompBase, (comp) => { - useEffect(() => { - comp.loadAllLibs(); - }, [comp.getView().length]); - return ( - - {comp.getAllLibs().length === 0 && ( - - )} - ({ - url: i.getView(), - deletable: true, - exportedAs: comp.globalVars[i.getView()]?.[0], - })) - .concat( - comp.externalLibs.map((l) => ({ - url: l, - deletable: false, - exportedAs: comp.globalVars[l]?.[0], - })) - )} - onDelete={(idx) => { - comp.dispatch(comp.deleteAction(idx)); - }} - /> - - ); -}); - -function runScript(code: string, inHost?: boolean) { - if (inHost) { - runScriptInHost(code); - return; - } - try { - evalFunc(code, {}, {}); - } catch (e) { - log.error(e); - } -} - -class ScriptComp extends CodeTextControl implements RunAndClearable { - runInHost: boolean = false; - - runPreloadScript() { - const code = this.getView(); - if (!code) { - return; - } - runScript(code, this.runInHost); - } - - async run(id: string, externalScript: string = "", runInHost: boolean = false) { - this.runInHost = runInHost; - if (externalScript) { - runScript(externalScript, runInHost); - } - this.runPreloadScript(); - } - - async clear(): Promise { - clearMockWindow(); - } -} - -class CSSComp extends CodeTextControl implements RunAndClearable { - id = ""; - externalCSS: string = ""; - - async applyAllCSS() { - const css = this.getView(); - evalStyle(this.id, [this.externalCSS, css]); - } - - async run(id: string, externalCSS: string = "") { - this.id = id; - this.externalCSS = externalCSS; - return this.applyAllCSS(); - } - - async clear() { - clearStyleEval(this.id); - } -} - -class GlobalCSSComp extends CodeTextControl implements RunAndClearable { - id = ""; - externalCSS: string = ""; - - async applyAllCSS() { - const css = this.getView(); - evalStyle(this.id, [this.externalCSS, css], true); - } - - async run(id: string, externalCSS: string = "") { - this.id = id; - this.externalCSS = externalCSS; - return this.applyAllCSS(); - } - - async clear() { - clearStyleEval(this.id); - } -} - -const childrenMap = { - libs: LibsComp, - script: ScriptComp, - css: CSSComp, - globalCSS: GlobalCSSComp, -}; - -type ChildrenInstance = RecordConstructorToComp; - -function JavaScriptTabPane(props: { comp: ConstructorToComp }) { - useEffect(() => { - props.comp.runPreloadScript(); - }, [props.comp]); - - const codePlaceholder = `window.name = 'Tom';\nwindow.greet = () => "hello world";`; - - return ( - <> - {trans("preLoad.jsHelpText")} - {props.comp.propertyView({ - expandable: false, - styleName: "window", - codeType: "Function", - language: "javascript", - placeholder: codePlaceholder, - })} - - ); -} - -function CSSTabPane(props: { comp: CSSComp, isGlobal?: boolean }) { - useEffect(() => { - props.comp.applyAllCSS(); - }, [props.comp]); - - const codePlaceholder = `.top-header {\n background-color: red; \n}`; - - return ( - <> - {trans("preLoad.cssHelpText")} - {props.comp.propertyView({ - expandable: false, - placeholder: codePlaceholder, - styleName: "window", - language: "css", - })} - - ); -} - -enum TabKey { - JavaScript = "js", - CSS = "css", - GLOBAL_CSS = "global_css", -} - -function PreloadConfigModal(props: ChildrenInstance) { - const [activeKey, setActiveKey] = useState(TabKey.JavaScript); - const { showScriptsAndStyleModal, changeExternalState } = useContext(ExternalEditorContext); - - const tabItems = [ - { - key: TabKey.JavaScript, - label: 'JavaScript', - children: - }, - { - key: TabKey.CSS, - label: 'CSS', - children: - }, - { - key: TabKey.GLOBAL_CSS, - label: 'Global CSS', - children: - }, - ] - return ( - changeExternalState?.({ showScriptsAndStyleModal: false })} - showOkButton={false} - showCancelButton={false} - width="600px" - > - setActiveKey(k as TabKey)} - style={{ marginBottom: 8, marginTop: 4 }} - activeKey={activeKey} - items={ tabItems } - > - - - ); -} - -const PreloadCompBase = new MultiCompBuilder(childrenMap, () => {}) - .setPropertyViewFn((children) => ) - .build(); - -const AddJSLibraryButton = styled.div` - cursor: pointer; - margin-right: 16px; - - g g { - stroke: #8b8fa3; - } - - &:hover { - g g { - stroke: #222222; - } - } -`; - -const JSLibraryWrapper = styled.div` - position: relative; -`; - -export class PreloadComp extends PreloadCompBase { - async clear() { - return Promise.allSettled(Object.values(this.children).map((i) => i.clear())); - } - - async run(id: string) { - const { orgCommonSettings = {} } = getGlobalSettings(); - const { preloadCSS,preloadGlobalCSS, preloadJavaScript, preloadLibs, runJavaScriptInHost } = orgCommonSettings; - await this.children.css.run(id, preloadCSS || ""); - await this.children.globalCSS.run('body', preloadGlobalCSS || ""); - await this.children.libs.run(id, preloadLibs || [], !!runJavaScriptInHost); - await this.children.script.run(id, preloadJavaScript || "", !!runJavaScriptInHost); - } - - getJSLibraryPropertyView() { - const libs = this.children.libs; - return ( - - - } - onCheck={(url) => !libs.getAllLibs().includes(url)} - onLoad={(url) => libs.loadScript(url)} - onSuccess={(url) => libs.dispatch(libs.pushAction(url))} - /> - - } - > - {this.children.libs.getPropertyView()} - - - ); - } -} +export { PreloadComp } from "./preLoadComp/preLoadComp"; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/ACTION_SYSTEM.md b/client/packages/lowcoder/src/comps/comps/preLoadComp/ACTION_SYSTEM.md new file mode 100644 index 000000000..a25f64d01 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/ACTION_SYSTEM.md @@ -0,0 +1,199 @@ +## Architecture + +### Core Components + +1. **ActionConfig Interface** - Defines the structure of an action +2. **ActionRegistry** - Central registry of all available actions +3. **ActionInputSection** - Main UI component that renders based on action configurations + +### Key Benefits + +- **Scalable**: Add new actions by simply adding a configuration object +- **Type Safe**: Full TypeScript support with proper interfaces +- **Validation**: Built-in input validation support +- **Categorized**: Actions are organized into logical categories +- **Flexible**: Support for different input types and requirements + +## Adding New Actions + +### Step 1: Define the Action Configuration + +Add a new action configuration in `actionConfigs.ts`: + +```typescript +const myNewAction: ActionConfig = { + key: 'my-new-action', + label: 'My New Action', + category: 'my-category', + requiresEditorComponentSelection: true, // if it needs a component from editor + requiresInput: true, // if it needs user input + inputPlaceholder: 'Enter your input here', + inputType: 'text', // 'text', 'number', 'textarea', 'json' + validation: (value: string) => { + if (!value.trim()) return 'Input is required'; + return null; // null means no error + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue, editorState } = params; + + // Your action logic here + console.log('Executing my new action:', selectedEditorComponent, actionValue); + + // Show success message + message.success('Action executed successfully!'); + } +}; +``` + +### Step 2: Add to Category + +Add your action to an existing category or create a new one: + +```typescript +export const actionCategories: ActionCategory[] = [ + // ... existing categories + { + key: 'my-category', + label: 'My Category', + actions: [myNewAction] + } +]; +``` + +### Step 3: Register the Action + +The action is automatically registered when added to a category, but you can also register it manually: + +```typescript +actionRegistry.set('my-new-action', myNewAction); +``` + +## Action Configuration Options + +### Basic Properties + +- `key`: Unique identifier for the action +- `label`: Display name in the UI +- `category`: Category for organization + +### UI Requirements + +- `requiresComponentSelection`: Shows component dropdown for adding new components +- `requiresEditorComponentSelection`: Shows dropdown of existing components in editor +- `requiresInput`: Shows input field for user data +- `inputPlaceholder`: Placeholder text for input field +- `inputType`: Type of input ('text', 'number', 'textarea', 'json') + +### Validation + +- `validation`: Function that returns error message or null + +### Execution + +- `execute`: Async function that performs the actual action + +## Example Actions + +### Component Management +- **Add Component**: Places new components in the editor +- **Move Component**: Moves existing components +- **Delete Component**: Removes components from editor +- **Resize Component**: Changes component dimensions + +### Component Configuration +- **Configure Component**: Updates component properties + +### Layout +- **Change Layout**: Modifies the overall layout type + +### Data +- **Bind Data**: Connects data sources to components + +### Events +- **Add Event Handler**: Attaches event handlers to components + +### Styling +- **Apply Style**: Applies CSS styles to components + +## Input Types + +### Text Input +```typescript +inputType: 'text' +``` + +### Number Input +```typescript +inputType: 'number' +``` + +### Textarea +```typescript +inputType: 'textarea' +``` + +### JSON Input +```typescript +inputType: 'json' +validation: (value: string) => { + try { + JSON.parse(value); + return null; + } catch { + return 'Invalid JSON format'; + } +} +``` + +## Validation Examples + +### Required Field +```typescript +validation: (value: string) => { + if (!value.trim()) return 'This field is required'; + return null; +} +``` + +### Numeric Range +```typescript +validation: (value: string) => { + const num = parseInt(value); + if (isNaN(num) || num < 1 || num > 100) { + return 'Please enter a number between 1 and 100'; + } + return null; +} +``` + +### Custom Format +```typescript +validation: (value: string) => { + const pattern = /^[A-Za-z0-9]+$/; + if (!pattern.test(value)) { + return 'Only alphanumeric characters are allowed'; + } + return null; +} +``` + +## Best Practices + +1. **Use Descriptive Keys**: Make action keys self-documenting +2. **Provide Clear Labels**: Use user-friendly action names +3. **Validate Input**: Always validate user input when required +4. **Handle Errors**: Provide meaningful error messages +5. **Show Feedback**: Use success/error messages to inform users +6. **Group Related Actions**: Use categories to organize actions logically + +## Migration from Old System + +The old hardcoded action handling has been replaced with the configuration-driven approach. All existing functionality is preserved, but now it's much easier to extend and maintain. + +## Future Enhancements + +- **Action History**: Track executed actions for undo/redo +- **Action Templates**: Predefined action configurations +- **Custom Validators**: Reusable validation functions +- **Action Dependencies**: Actions that depend on other actions +- **Batch Actions**: Execute multiple actions together \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/README.md b/client/packages/lowcoder/src/comps/comps/preLoadComp/README.md new file mode 100644 index 000000000..2e7b764ff --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/README.md @@ -0,0 +1,94 @@ +## File Structure + +``` +preLoadComp/ +├── index.ts # Main exports +├── preLoadComp.tsx # Main PreloadComp class +├── types.ts # TypeScript interfaces and types +├── styled.tsx # Styled components +├── utils.ts # Utility functions +├── components.tsx # Component classes (LibsComp, ScriptComp, etc.) +├── tabPanes.tsx # Tab pane components +├── preloadConfigModal.tsx # Modal configuration component +├── actionInputSection.tsx # Component placement functionality +├── actionConfigs.ts # Action configurations (scalable action system) +├── ACTION_SYSTEM.md # Action system documentation +└── README.md # This documentation +``` + +## Components + +### Core Components +- **`preLoadComp.tsx`**: Main `PreloadComp` class that orchestrates all functionality +- **`components.tsx`**: Contains all component classes (`LibsComp`, `ScriptComp`, `CSSComp`, `GlobalCSSComp`) + +### UI Components +- **`preloadConfigModal.tsx`**: Modal with tabs for JavaScript, CSS, and Global CSS +- **`tabPanes.tsx`**: Individual tab pane components for JavaScript and CSS +- **`actionInputSection.tsx`**: Component placement functionality with dropdowns + +### Supporting Files +- **`types.ts`**: TypeScript interfaces and enums +- **`styled.tsx`**: Styled-components for consistent styling +- **`utils.ts`**: Utility functions for component generation and script execution +- **`index.ts`**: Centralized exports for easy importing + +## Key Features + +### Component Placement +The `ActionInputSection` component provides: +- Dropdown selection of available components +- Categorized component listing +- Automatic component placement in the editor +- Success/error feedback + +### Scalable Action System +The action system has been completely refactored to be configuration-driven: +- **Easy to Extend**: Add new actions by simply adding configuration objects +- **Type Safe**: Full TypeScript support with proper interfaces +- **Validation**: Built-in input validation support +- **Categorized**: Actions organized into logical categories +- **Flexible**: Support for different input types and requirements + +See `ACTION_SYSTEM.md` for detailed documentation on adding new actions. + +### Script and Style Management +- JavaScript library loading and management +- CSS and Global CSS application +- Script execution in host or sandbox environment + +### Modular Architecture +- **Separation of Concerns**: Each file has a single responsibility +- **Reusability**: Components can be imported and used independently +- **Maintainability**: Easy to locate and modify specific functionality +- **Type Safety**: Comprehensive TypeScript interfaces + +## Usage + +```typescript +// Import the main component +import { PreloadComp } from "./preLoadComp"; + +// Import specific components +import { ActionInputSection } from "./preLoadComp/actionInputSection"; +import { PreloadConfigModal } from "./preLoadComp/preloadConfigModal"; + +// Import utilities +import { generateComponentActionItems } from "./preLoadComp/utils"; + +// Import types +import type { ExternalPreload, RunAndClearable } from "./preLoadComp/types"; +``` + +## Benefits of Restructuring + +1. **Maintainability**: Each file is focused and easier to understand +2. **Reusability**: Components can be used independently +3. **Testing**: Individual components can be tested in isolation +4. **Collaboration**: Multiple developers can work on different parts simultaneously +5. **Code Organization**: Clear separation of concerns +6. **Type Safety**: Better TypeScript support with dedicated type files + +## Migration Notes + +The original `preLoadComp.tsx` file now simply exports from the new modular structure, ensuring backward compatibility while providing the benefits of the new organization. \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts new file mode 100644 index 000000000..2eae082a1 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -0,0 +1,74 @@ +import { ActionCategory } from "./types"; +import { + addComponentAction, + moveComponentAction, + renameComponentAction, + deleteComponentAction, + resizeComponentAction, + configureComponentAction, + changeLayoutAction, + addEventHandlerAction, + applyStyleAction +} from "./actions"; + +export const actionCategories: ActionCategory[] = [ + { + key: 'component-management', + label: 'Component Management', + actions: [ + addComponentAction, + moveComponentAction, + deleteComponentAction, + resizeComponentAction, + renameComponentAction + ] + }, + { + key: 'component-configuration', + label: 'Component Configuration', + actions: [configureComponentAction] + }, + { + key: 'layout', + label: 'Layout', + actions: [changeLayoutAction] + }, + { + key: 'events', + label: 'Events', + actions: [addEventHandlerAction] + }, + { + key: 'styling', + label: 'Styling', + actions: [applyStyleAction] + } +]; + +export const actionRegistry = new Map(); +actionCategories.forEach(category => { + category.actions.forEach(action => { + actionRegistry.set(action.key, action); + }); +}); + +export const getAllActionItems = () => { + return actionCategories.flatMap(category => { + if (category.actions.length === 1) { + const action = category.actions[0]; + return [{ + label: action.label, + key: action.key + }]; + } + + return [{ + label: category.label, + key: `category-${category.key}`, + children: category.actions.map(action => ({ + label: action.label, + key: action.key + })) + }]; + }); + }; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx new file mode 100644 index 000000000..148dac167 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -0,0 +1,315 @@ +import React, { + useContext, + useState, + useCallback, + useRef, + useMemo +} from "react"; +import { default as Button } from "antd/es/button"; +import { default as Input } from "antd/es/input"; +import { default as Menu } from "antd/es/menu"; +import { default as Space } from "antd/es/space"; +import { default as Flex } from "antd/es/flex"; +import type { InputRef } from 'antd'; +import { default as DownOutlined } from "@ant-design/icons/DownOutlined"; +import { BaseSection } from "lowcoder-design"; +import { EditorContext } from "comps/editorState"; +import { message } from "antd"; +import { CustomDropdown } from "./styled"; +import { generateComponentActionItems, getComponentCategories } from "./utils"; +import { actionRegistry, getAllActionItems } from "./actionConfigs"; + +export function ActionInputSection() { + const [actionValue, setActionValue] = useState(""); + const [selectedActionKey, setSelectedActionKey] = useState(null); + const [placeholderText, setPlaceholderText] = useState(""); + const [selectedComponent, setSelectedComponent] = useState(null); + const [showComponentDropdown, setShowComponentDropdown] = useState(false); + const [showEditorComponentsDropdown, setShowEditorComponentsDropdown] = useState(false); + const [selectedEditorComponent, setSelectedEditorComponent] = useState(null); + const [validationError, setValidationError] = useState(null); + const inputRef = useRef(null); + const editorState = useContext(EditorContext); + + const categories = useMemo(() => { + return getComponentCategories(); + }, []); + + const componentActionItems = useMemo(() => { + return generateComponentActionItems(categories); + }, [categories]); + + const allActionItems = useMemo(() => { + return getAllActionItems(); + }, []); + + const editorComponents = useMemo(() => { + if (!editorState) return []; + + const compInfos = editorState.uiCompInfoList(); + return compInfos.map(comp => ({ + label: comp.name, + key: comp.name + })); + }, [editorState]); + + const currentAction = useMemo(() => { + return selectedActionKey ? actionRegistry.get(selectedActionKey) : null; + }, [selectedActionKey]); + + const handleActionSelection = useCallback((key: string) => { + if (key.startsWith('category-')) { + return; + } + + setSelectedActionKey(key); + setValidationError(null); + + const action = actionRegistry.get(key); + if (!action) { + console.warn(`Action not found: ${key}`); + return; + } + + setShowComponentDropdown(false); + setShowEditorComponentsDropdown(false); + setSelectedComponent(null); + setSelectedEditorComponent(null); + setActionValue(""); + + if (action.requiresComponentSelection) { + setShowComponentDropdown(true); + setPlaceholderText("Select a component to add"); + } else if (action.requiresEditorComponentSelection) { + setShowEditorComponentsDropdown(true); + setPlaceholderText(`Select a component to ${action.label.toLowerCase()}`); + } else if (action.requiresInput) { + setPlaceholderText(action.inputPlaceholder || `Enter ${action.label.toLowerCase()} value`); + } else { + setPlaceholderText(`Execute ${action.label.toLowerCase()}`); + } + }, []); + + const handleComponentSelection = useCallback((key: string) => { + if (key.startsWith('comp-')) { + const compName = key.replace('comp-', ''); + setSelectedComponent(compName); + setPlaceholderText(`Configure ${compName} component`); + } + }, []); + + const handleEditorComponentSelection = useCallback((key: string) => { + setSelectedEditorComponent(key); + if (currentAction) { + setPlaceholderText(`${currentAction.label}`); + } + }, [currentAction]); + + const validateInput = useCallback((value: string): string | null => { + if (!currentAction?.validation) return null; + return currentAction.validation(value); + }, [currentAction]); + + const handleInputChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + setActionValue(value); + + if (validationError) { + setValidationError(null); + } + }, [validationError]); + + const handleApplyAction = useCallback(async () => { + if (!editorState) { + message.error('Editor state not available'); + return; + } + + if (!selectedActionKey || !currentAction) { + message.error('No action selected'); + return; + } + + if (currentAction.requiresInput && currentAction.validation) { + const error = validateInput(actionValue); + if (error) { + setValidationError(error); + message.error(error); + return; + } + } + + if (currentAction.requiresComponentSelection && !selectedComponent) { + message.error('Please select a component'); + return; + } + + if (currentAction.requiresEditorComponentSelection && !selectedEditorComponent) { + message.error('Please select a component from the editor'); + return; + } + + try { + await currentAction.execute({ + actionKey: selectedActionKey, + actionValue, + selectedComponent, + selectedEditorComponent, + editorState + }); + + // Clear the form on success + setActionValue(""); + setSelectedComponent(null); + setSelectedActionKey(null); + setShowComponentDropdown(false); + setShowEditorComponentsDropdown(false); + setSelectedEditorComponent(null); + setPlaceholderText(""); + setValidationError(null); + + } catch (error) { + console.error('Error executing action:', error); + message.error('Failed to execute action. Please try again.'); + } + }, [ + selectedActionKey, + actionValue, + selectedComponent, + selectedEditorComponent, + editorState, + currentAction, + validateInput + ]); + + const isApplyDisabled = useMemo(() => { + if (!selectedActionKey || !currentAction) return true; + + if (currentAction.requiresComponentSelection && !selectedComponent) return true; + if (currentAction.requiresEditorComponentSelection && !selectedEditorComponent) return true; + if (currentAction.requiresInput && !actionValue.trim()) return true; + + return false; + }, [selectedActionKey, currentAction, selectedComponent, selectedEditorComponent, actionValue]); + + const shouldShowInput = useMemo(() => { + if (!currentAction) return false; + return currentAction.requiresInput && ( + !currentAction.requiresEditorComponentSelection || selectedEditorComponent + ); + }, [currentAction, selectedEditorComponent]); + + return ( + +
+ + ( + { + handleActionSelection(key); + }} + /> + )} + > + + + + {showComponentDropdown && ( + ( + { + handleComponentSelection(key); + }} + /> + )} + > + + + )} + + {showEditorComponentsDropdown && ( + ( + { + handleEditorComponentSelection(key); + }} + /> + )} + > + + + )} + + {shouldShowInput && ( + + )} + + {validationError && ( +
+ {validationError} +
+ )} + + + +
+
+ ); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts new file mode 100644 index 000000000..2106b1eda --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts @@ -0,0 +1,34 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +export const configureComponentAction: ActionConfig = { + key: 'configure-components', + label: 'Configure a component', + category: 'component-configuration', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter configuration (JSON format)', + inputType: 'json', + validation: (value: string) => { + if (!value.trim()) return 'Configuration is required'; + try { + JSON.parse(value); + return null; + } catch { + return 'Invalid JSON format'; + } + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue } = params; + + try { + const config = JSON.parse(actionValue); + console.log('Configuring component:', selectedEditorComponent, 'with config:', config); + message.info(`Configure action for component "${selectedEditorComponent}"`); + + // TODO: Implement actual configuration logic + } catch (error) { + message.error('Invalid configuration format'); + } + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts new file mode 100644 index 000000000..cf007c5af --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts @@ -0,0 +1,20 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +export const addEventHandlerAction: ActionConfig = { + key: 'add-event-handler', + label: 'Add event handler', + category: 'events', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter event handler code (JavaScript)', + inputType: 'textarea', + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue } = params; + + console.log('Adding event handler to component:', selectedEditorComponent, 'with code:', actionValue); + message.info(`Event handler added to component "${selectedEditorComponent}"`); + + // TODO: Implement actual event handler logic + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentLayout.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentLayout.ts new file mode 100644 index 000000000..e4afa15ca --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentLayout.ts @@ -0,0 +1,19 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +export const changeLayoutAction: ActionConfig = { + key: 'change-layout', + label: 'Change layout', + category: 'layout', + requiresInput: true, + inputPlaceholder: 'Enter layout type (grid, flex, absolute)', + inputType: 'text', + execute: async (params: ActionExecuteParams) => { + const { actionValue } = params; + + console.log('Changing layout to:', actionValue); + message.info(`Layout changed to: ${actionValue}`); + + // TODO: Implement actual layout change logic + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts new file mode 100644 index 000000000..f86358b16 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts @@ -0,0 +1,441 @@ +import { message } from "antd"; +import { genRandomKey } from "comps/utils/idGenerator"; +import { parseCompType } from "comps/utils/remote"; +import { defaultLayout, GridItemDataType } from "comps/comps/gridItemComp"; +import { addMapChildAction } from "comps/generators/sameTypeMap"; +import { uiCompRegistry, UICompType } from "comps/uiCompRegistry"; +import { ActionConfig, ActionExecuteParams } from "../types"; +import { + multiChangeAction, + wrapActionExtraInfo, + changeValueAction, + wrapChildAction, + deleteCompAction +} from "lowcoder-core"; +import { getEditorComponentInfo } from "../utils"; + +export const addComponentAction: ActionConfig = { + key: 'add-components', + label: 'Place a component', + category: 'component-management', + requiresComponentSelection: true, + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { selectedComponent, editorState } = params; + + if (!selectedComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + try { + const uiComp = editorState.getUIComp(); + const container = uiComp.getComp(); + + if (!container) { + message.error('No container available to add component'); + return; + } + + const simpleContainer = container.realSimpleContainer(); + if (!simpleContainer) { + message.error('No grid container available'); + return; + } + + const nameGenerator = editorState.getNameGenerator(); + const compInfo = parseCompType(selectedComponent); + const compName = nameGenerator.genItemName(compInfo.compName); + const key = genRandomKey(); + + const manifest = uiCompRegistry[selectedComponent]; + let defaultDataFn = undefined; + + if (manifest?.lazyLoad) { + const { defaultDataFnName, defaultDataFnPath } = manifest; + if (defaultDataFnName && defaultDataFnPath) { + const module = await import(`../../../${defaultDataFnPath}.tsx`); + defaultDataFn = module[defaultDataFnName]; + } + } else if (!compInfo.isRemote) { + defaultDataFn = manifest?.defaultDataFn; + } + + const widgetValue: GridItemDataType = { + compType: selectedComponent, + name: compName, + comp: defaultDataFn ? defaultDataFn(compName, nameGenerator, editorState) : undefined, + }; + + const currentLayout = simpleContainer.children.layout.getView(); + const layoutInfo = manifest?.layoutInfo || defaultLayout(selectedComponent as UICompType); + + let itemPos = 0; + if (Object.keys(currentLayout).length > 0) { + itemPos = Math.min(...Object.values(currentLayout).map((l: any) => l.pos || 0)) - 1; + } + + const layoutItem = { + i: key, + x: 0, + y: 0, + w: layoutInfo.w || 6, + h: layoutInfo.h || 5, + pos: itemPos, + isDragging: false, + }; + + simpleContainer.dispatch( + wrapActionExtraInfo( + multiChangeAction({ + layout: changeValueAction({ + ...currentLayout, + [key]: layoutItem, + }, true), + items: addMapChildAction(key, widgetValue), + }), + { compInfos: [{ compName: compName, compType: selectedComponent, type: "add" }] } + ) + ); + + editorState.setSelectedCompNames(new Set([compName]), "addComp"); + + message.success(`Component "${manifest?.name || selectedComponent}" added successfully!`); + } catch (error) { + console.error('Error adding component:', error); + message.error('Failed to add component. Please try again.'); + } + } +}; + +export const deleteComponentAction: ActionConfig = { + key: 'delete-components', + label: 'Delete a component', + category: 'component-management', + requiresEditorComponentSelection: true, + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, editorState } = params; + + if (!selectedEditorComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + try { + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + + if (!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey, currentLayout, simpleContainer, componentType } = componentInfo; + + if (!componentKey || !currentLayout[componentKey]) { + message.error(`Component "${selectedEditorComponent}" not found in layout`); + return; + } + + const newLayout = { ...currentLayout }; + delete newLayout[componentKey]; + + simpleContainer.dispatch( + wrapActionExtraInfo( + multiChangeAction({ + layout: changeValueAction(newLayout, true), + items: wrapChildAction(componentKey, deleteCompAction()), + }), + { + compInfos: [{ + compName: selectedEditorComponent, + compType: componentType || 'unknown', + type: "delete" + }] + } + ) + ); + + editorState.setSelectedCompNames(new Set(), "deleteComp"); + + message.success(`Component "${selectedEditorComponent}" deleted successfully`); + } catch (error) { + console.error('Error deleting component:', error); + message.error('Failed to delete component. Please try again.'); + } + } +}; + +export const moveComponentAction: ActionConfig = { + key: 'move-components', + label: 'Move a component', + category: 'component-management', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter move parameters (e.g., x:100, y:200)', + inputType: 'text', + validation: (value: string) => { + if (!value.trim()) return 'Move parameters are required'; + + const params = value.toLowerCase().split(',').map(p => p.trim()); + for (const param of params) { + if (!param.includes(':')) { + return 'Invalid format. Use "x:value, y:value"'; + } + const [key, val] = param.split(':').map(s => s.trim()); + if (!['x', 'y'].includes(key)) { + return 'Only x and y parameters are supported'; + } + const num = parseInt(val); + if (isNaN(num) || num < 0) { + return `${key} must be a positive number`; + } + } + return null; + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue, editorState } = params; + + if (!selectedEditorComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + try { + const moveParams: { x?: number; y?: number } = {}; + const params = actionValue.toLowerCase().split(',').map(p => p.trim()); + + for (const param of params) { + const [key, val] = param.split(':').map(s => s.trim()); + if (['x', 'y'].includes(key)) { + moveParams[key as 'x' | 'y'] = parseInt(val); + } + } + + if (!moveParams.x && !moveParams.y) { + message.error('No valid move parameters provided'); + return; + } + + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + + if (!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey, currentLayout, simpleContainer } = componentInfo; + + if (!componentKey || !currentLayout[componentKey]) { + message.error(`Component "${selectedEditorComponent}" not found in layout`); + return; + } + + const currentLayoutItem = currentLayout[componentKey]; + const items = simpleContainer.children.items.children; + + const newLayoutItem = { + ...currentLayoutItem, + x: moveParams.x !== undefined ? moveParams.x : currentLayoutItem.x, + y: moveParams.y !== undefined ? moveParams.y : currentLayoutItem.y, + }; + + const newLayout = { + ...currentLayout, + [componentKey]: newLayoutItem, + }; + + simpleContainer.dispatch( + wrapActionExtraInfo( + multiChangeAction({ + layout: changeValueAction(newLayout, true), + }), + { + compInfos: [{ + compName: selectedEditorComponent, + compType: (items[componentKey] as any).children.compType.getView(), + type: "layout" + }] + } + ) + ); + + editorState.setSelectedCompNames(new Set([selectedEditorComponent]), "moveComp"); + + const moveDescription = []; + if (moveParams.x !== undefined) moveDescription.push(`x: ${moveParams.x}`); + if (moveParams.y !== undefined) moveDescription.push(`y: ${moveParams.y}`); + + message.success(`Component "${selectedEditorComponent}" moved to ${moveDescription.join(', ')}`); + } catch (error) { + console.error('Error moving component:', error); + message.error('Failed to move component. Please try again.'); + } + } +}; + +export const renameComponentAction: ActionConfig = { + key: 'rename-components', + label: 'Rename a component', + category: 'component-management', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter new name', + inputType: 'text', + validation: (value: string, params?: ActionExecuteParams) => { + if (!value.trim()) return 'Name is required'; + + if (params?.editorState && params?.selectedEditorComponent) { + const error = params.editorState.checkRename(params.selectedEditorComponent, value); + return error || null; + } + return null; + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue, editorState } = params; + + if (!selectedEditorComponent || !actionValue) { + message.error('Component and name is required'); + return; + } + + try { + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + + if (!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey, currentLayout, items } = componentInfo; + + if (!componentKey) { + message.error(`Component "${selectedEditorComponent}" not found in layout`); + return; + } + + const componentItem = items[componentKey]; + if (!componentItem) { + message.error(`Component "${selectedEditorComponent}" not found in items`); + return; + } + + if (editorState.rename(selectedEditorComponent, actionValue)) { + editorState.setSelectedCompNames(new Set([actionValue]), "renameComp"); + message.success(`Component "${selectedEditorComponent}" renamed to "${actionValue}" successfully`); + } else { + message.error('Failed to rename component. The name might already exist or be invalid.'); + } + } catch(error) { + console.error('Error renaming component:', error); + message.error('Failed to rename component. Please try again.'); + } + } +}; + +export const resizeComponentAction: ActionConfig = { + key: 'resize-components', + label: 'Resize a component', + category: 'component-management', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter resize parameters (e.g., w:8, h:6)', + inputType: 'text', + validation: (value: string) => { + if (!value.trim()) return 'Resize parameters are required'; + + const params = value.toLowerCase().split(',').map(p => p.trim()); + for (const param of params) { + if (!param.includes(':')) { + return 'Invalid format. Use "w:value, h:value"'; + } + const [key, val] = param.split(':').map(s => s.trim()); + if (!['w', 'h'].includes(key)) { + return 'Only w (width) and h (height) parameters are supported'; + } + const num = parseInt(val); + if (isNaN(num) || num < 1) { + return `${key} must be a positive number greater than 0`; + } + } + return null; + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue, editorState } = params; + + if (!selectedEditorComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + try { + const resizeParams: { w?: number; h?: number } = {}; + const params = actionValue.toLowerCase().split(',').map(p => p.trim()); + + for (const param of params) { + const [key, val] = param.split(':').map(s => s.trim()); + if (['w', 'h'].includes(key)) { + resizeParams[key as 'w' | 'h'] = parseInt(val); + } + } + + if (!resizeParams.w && !resizeParams.h) { + message.error('No valid resize parameters provided'); + return; + } + + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + + if (!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey, currentLayout, simpleContainer, items } = componentInfo; + + if (!componentKey || !currentLayout[componentKey]) { + message.error(`Component "${selectedEditorComponent}" not found in layout`); + return; + } + + const currentLayoutItem = currentLayout[componentKey]; + + const newLayoutItem = { + ...currentLayoutItem, + w: resizeParams.w !== undefined ? resizeParams.w : currentLayoutItem.w, + h: resizeParams.h !== undefined ? resizeParams.h : currentLayoutItem.h, + }; + + const newLayout = { + ...currentLayout, + [componentKey]: newLayoutItem, + }; + + simpleContainer.dispatch( + wrapActionExtraInfo( + multiChangeAction({ + layout: changeValueAction(newLayout, true), + }), + { + compInfos: [{ + compName: selectedEditorComponent, + compType: (items[componentKey] as any).children.compType.getView(), + type: "layout" + }] + } + ) + ); + + editorState.setSelectedCompNames(new Set([selectedEditorComponent]), "resizeComp"); + + const resizeDescription = []; + if (resizeParams.w !== undefined) resizeDescription.push(`width: ${resizeParams.w}`); + if (resizeParams.h !== undefined) resizeDescription.push(`height: ${resizeParams.h}`); + + message.success(`Component "${selectedEditorComponent}" resized to ${resizeDescription.join(', ')}`); + } catch (error) { + console.error('Error resizing component:', error); + message.error('Failed to resize component. Please try again.'); + } + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts new file mode 100644 index 000000000..2a152713e --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts @@ -0,0 +1,34 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +export const applyStyleAction: ActionConfig = { + key: 'apply-style', + label: 'Apply style to component', + category: 'styling', + requiresEditorComponentSelection: true, + requiresInput: true, + inputPlaceholder: 'Enter CSS styles (JSON format)', + inputType: 'json', + validation: (value: string) => { + if (!value.trim()) return 'Styles are required'; + try { + JSON.parse(value); + return null; + } catch { + return 'Invalid JSON format'; + } + }, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, actionValue } = params; + + try { + const styles = JSON.parse(actionValue); + console.log('Applying styles to component:', selectedEditorComponent, 'with styles:', styles); + message.info(`Styles applied to component "${selectedEditorComponent}"`); + + // TODO: Implement actual style application logic + } catch (error) { + message.error('Invalid style format'); + } + } +}; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts new file mode 100644 index 000000000..840000050 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts @@ -0,0 +1,14 @@ +// Component Management Actions +export * from './componentManagement'; + +// Component Configuration Actions +export { configureComponentAction } from './componentConfiguration'; + +// Layout Actions +export { changeLayoutAction } from './componentLayout'; + +// Event Actions +export { addEventHandlerAction } from './componentEvents'; + +// Styling Actions +export { applyStyleAction } from './componentStyling'; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx new file mode 100644 index 000000000..28f4629d9 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/components.tsx @@ -0,0 +1,178 @@ +import { + clearMockWindow, + clearStyleEval, + ConstructorToComp, + evalFunc, + evalStyle, +} from "lowcoder-core"; +import { CodeTextControl } from "comps/controls/codeTextControl"; +import SimpleStringControl from "comps/controls/simpleStringControl"; +import { MultiCompBuilder, withPropertyViewFn } from "comps/generators"; +import { list } from "comps/generators/list"; +import { ScrollBar } from "lowcoder-design"; +import { EmptyContent } from "components/EmptyContent"; +import React, { useEffect } from "react"; +import { trans } from "i18n"; +import log from "loglevel"; +import { JSLibraryTree } from "components/JSLibraryTree"; +import { fetchJSLibrary } from "util/jsLibraryUtils"; +import { RunAndClearable } from "./types"; + +export class LibsCompBase extends list(SimpleStringControl) implements RunAndClearable { + success: Record = {}; + globalVars: Record = {}; + externalLibs: string[] = []; + runInHost: boolean = false; + + getAllLibs() { + return this.externalLibs.concat(this.getView().map((i) => i.getView())); + } + + async loadScript(url: string) { + if (this.success[url]) { + return; + } + return fetchJSLibrary(url).then((code) => { + evalFunc( + code, + {}, + {}, + { + scope: "function", + disableLimit: this.runInHost, + onSetGlobalVars: (v: string) => { + this.globalVars[url] = this.globalVars[url] || []; + if (!this.globalVars[url].includes(v)) { + this.globalVars[url].push(v); + } + }, + } + ); + this.success[url] = true; + }); + } + + async loadAllLibs() { + const scriptRunners = this.getAllLibs().map((url) => + this.loadScript(url).catch((e) => { + log.warn(e); + }) + ); + + try { + await Promise.all(scriptRunners); + } catch (e) { + log.warn("load preload libs error:", e); + } + } + + async run(id: string, externalLibs: string[] = [], runInHost: boolean = false) { + this.externalLibs = externalLibs; + this.runInHost = runInHost; + return this.loadAllLibs(); + } + + async clear(): Promise { + clearMockWindow(); + } +} + +export const LibsComp = withPropertyViewFn(LibsCompBase, (comp) => { + useEffect(() => { + comp.loadAllLibs(); + }, [comp.getView().length]); + return ( + + {comp.getAllLibs().length === 0 && ( + + )} + ({ + url: i.getView(), + deletable: true, + exportedAs: comp.globalVars[i.getView()]?.[0], + })) + .concat( + comp.externalLibs.map((l) => ({ + url: l, + deletable: false, + exportedAs: comp.globalVars[l]?.[0], + })) + )} + onDelete={(idx) => { + comp.dispatch(comp.deleteAction(idx)); + }} + /> + + ); +}); + +export class ScriptComp extends CodeTextControl implements RunAndClearable { + runInHost: boolean = false; + + runPreloadScript() { + const code = this.getView(); + if (!code) { + return; + } + // Import runScript from utils to avoid circular dependency + const { runScript } = require("./utils"); + runScript(code, this.runInHost); + } + + async run(id: string, externalScript: string = "", runInHost: boolean = false) { + this.runInHost = runInHost; + if (externalScript) { + const { runScript } = require("./utils"); + runScript(externalScript, runInHost); + } + this.runPreloadScript(); + } + + async clear(): Promise { + clearMockWindow(); + } +} + +export class CSSComp extends CodeTextControl implements RunAndClearable { + id = ""; + externalCSS: string = ""; + + async applyAllCSS() { + const css = this.getView(); + evalStyle(this.id, [this.externalCSS, css]); + } + + async run(id: string, externalCSS: string = "") { + this.id = id; + this.externalCSS = externalCSS; + return this.applyAllCSS(); + } + + async clear() { + clearStyleEval(this.id); + } +} + +export class GlobalCSSComp extends CodeTextControl implements RunAndClearable { + id = ""; + externalCSS: string = ""; + + async applyAllCSS() { + const css = this.getView(); + evalStyle(this.id, [this.externalCSS, css], true); + } + + async run(id: string, externalCSS: string = "") { + this.id = id; + this.externalCSS = externalCSS; + return this.applyAllCSS(); + } + + async clear() { + clearStyleEval(this.id); + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/index.ts new file mode 100644 index 000000000..15c398556 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/index.ts @@ -0,0 +1,43 @@ +// Main component +export { PreloadComp } from "./preLoadComp"; + +// Component classes +export { LibsComp, ScriptComp, CSSComp, GlobalCSSComp } from "./components"; + +// UI Components +export { PreloadConfigModal } from "./preloadConfigModal"; +export { ActionInputSection } from "./actionInputSection"; +export { JavaScriptTabPane, CSSTabPane } from "./tabPanes"; + +// Types and interfaces +export type { + ExternalPreload, + RunAndClearable, + ComponentActionState, + ActionConfig, + ActionExecuteParams, + ActionCategory, + ActionRegistry +} from "./types"; +export { TabKey } from "./types"; + +// Action configurations +export { + actionRegistry, + getAllActionItems, + actionCategories +} from "./actionConfigs"; + +// Styled components +export { + CustomDropdown, + AddJSLibraryButton, + JSLibraryWrapper +} from "./styled"; + +// Utility functions +export { + runScript, + generateComponentActionItems, + getComponentCategories +} from "./utils"; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/preLoadComp.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/preLoadComp.tsx new file mode 100644 index 000000000..be106608c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/preLoadComp.tsx @@ -0,0 +1,69 @@ +import { MultiCompBuilder } from "comps/generators"; +import { BaseSection, PlusIcon } from "lowcoder-design"; +import React from "react"; +import { getGlobalSettings } from "comps/utils/globalSettings"; +import { trans } from "i18n"; +import { JSLibraryModal } from "components/JSLibraryModal"; +import { LibsComp, ScriptComp, CSSComp, GlobalCSSComp } from "./components"; +import { PreloadConfigModal } from "./preloadConfigModal"; +import { AddJSLibraryButton, JSLibraryWrapper } from "./styled"; +import { ActionInputSection } from "./actionInputSection"; + +const childrenMap = { + libs: LibsComp, + script: ScriptComp, + css: CSSComp, + globalCSS: GlobalCSSComp, +}; + +const PreloadCompBase = new MultiCompBuilder(childrenMap, () => {}) + .setPropertyViewFn((children) => ) + .build(); + +export class PreloadComp extends PreloadCompBase { + async clear() { + return Promise.allSettled(Object.values(this.children).map((i) => i.clear())); + } + + async run(id: string) { + const { orgCommonSettings = {} } = getGlobalSettings(); + const { preloadCSS, preloadGlobalCSS, preloadJavaScript, preloadLibs, runJavaScriptInHost } = orgCommonSettings; + await this.children.css.run(id, preloadCSS || ""); + await this.children.globalCSS.run('body', preloadGlobalCSS || ""); + await this.children.libs.run(id, preloadLibs || [], !!runJavaScriptInHost); + await this.children.script.run(id, preloadJavaScript || "", !!runJavaScriptInHost); + } + + getJSLibraryPropertyView() { + const libs = this.children.libs; + return ( + <> + + + } + onCheck={(url) => !libs.getAllLibs().includes(url)} + onLoad={(url) => libs.loadScript(url)} + onSuccess={(url) => libs.dispatch(libs.pushAction(url))} + /> + + } + > + {this.children.libs.getPropertyView()} + + + + + ); + } +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/preloadConfigModal.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/preloadConfigModal.tsx new file mode 100644 index 000000000..b420b240d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/preloadConfigModal.tsx @@ -0,0 +1,60 @@ +import React, { useContext, useState } from "react"; +import { Tabs } from "components/Tabs"; +import { CustomModal } from "lowcoder-design"; +import { ExternalEditorContext } from "util/context/ExternalEditorContext"; +import { trans } from "i18n"; +import { RecordConstructorToComp } from "lowcoder-core"; +import { TabKey } from "./types"; +import { JavaScriptTabPane, CSSTabPane } from "./tabPanes"; +import { ScriptComp, CSSComp, GlobalCSSComp } from "./components"; + +type ChildrenInstance = RecordConstructorToComp<{ + libs: any; + script: typeof ScriptComp; + css: typeof CSSComp; + globalCSS: typeof GlobalCSSComp; +}>; + +export function PreloadConfigModal(props: ChildrenInstance) { + const [activeKey, setActiveKey] = useState(TabKey.JavaScript); + const { showScriptsAndStyleModal, changeExternalState } = useContext(ExternalEditorContext); + + const tabItems = [ + { + key: TabKey.JavaScript, + label: 'JavaScript', + children: + }, + { + key: TabKey.CSS, + label: 'CSS', + children: + }, + { + key: TabKey.GLOBAL_CSS, + label: 'Global CSS', + children: + }, + ]; + + return ( + changeExternalState?.({ showScriptsAndStyleModal: false })} + showOkButton={false} + showCancelButton={false} + width="600px" + > + setActiveKey(k as TabKey)} + style={{ marginBottom: 8, marginTop: 4 }} + activeKey={activeKey} + items={tabItems} + /> + + ); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/styled.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/styled.tsx new file mode 100644 index 000000000..211115e99 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/styled.tsx @@ -0,0 +1,29 @@ +import styled from "styled-components"; +import { default as Dropdown } from "antd/es/dropdown"; + +export const CustomDropdown = styled(Dropdown)` + .ant-dropdown-menu-item-icon { + width: 14px !important; + height: 14px !important; + max-width: 14px !important; + } +`; + +export const AddJSLibraryButton = styled.div` + cursor: pointer; + margin-right: 16px; + + g g { + stroke: #8b8fa3; + } + + &:hover { + g g { + stroke: #222222; + } + } +`; + +export const JSLibraryWrapper = styled.div` + position: relative; +`; \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/tabPanes.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/tabPanes.tsx new file mode 100644 index 000000000..a1229a96d --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/tabPanes.tsx @@ -0,0 +1,46 @@ +import { HelpText } from "components/HelpText"; +import React, { useEffect } from "react"; +import { trans } from "i18n"; +import { ConstructorToComp } from "lowcoder-core"; +import { ScriptComp, CSSComp } from "./components"; + +export function JavaScriptTabPane(props: { comp: ConstructorToComp }) { + useEffect(() => { + props.comp.runPreloadScript(); + }, [props.comp]); + + const codePlaceholder = `window.name = 'Tom';\nwindow.greet = () => "hello world";`; + + return ( + <> + {trans("preLoad.jsHelpText")} + {props.comp.propertyView({ + expandable: false, + styleName: "window", + codeType: "Function", + language: "javascript", + placeholder: codePlaceholder, + })} + + ); +} + +export function CSSTabPane(props: { comp: CSSComp, isGlobal?: boolean }) { + useEffect(() => { + props.comp.applyAllCSS(); + }, [props.comp]); + + const codePlaceholder = `.top-header {\n background-color: red; \n}`; + + return ( + <> + {trans("preLoad.cssHelpText")} + {props.comp.propertyView({ + expandable: false, + placeholder: codePlaceholder, + styleName: "window", + language: "css", + })} + + ); +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts new file mode 100644 index 000000000..7e84ab1da --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts @@ -0,0 +1,59 @@ +export interface ExternalPreload { + css?: string; + libs?: string[]; + script?: string; + runJavaScriptInHost?: boolean; +} + +export interface RunAndClearable { + run(id: string, externalPreload?: T): Promise; + clear(): Promise; +} + +export enum TabKey { + JavaScript = "js", + CSS = "css", + GLOBAL_CSS = "global_css", +} + +export interface ComponentActionState { + actionValue: string; + selectedActionKey: string | null; + placeholderText: string; + selectedComponent: string | null; + showComponentDropdown: boolean; + showEditorComponentsDropdown: boolean; + selectedEditorComponent: string | null; +} + +export interface ActionConfig { + key: string; + label: string; + category?: string; + requiresComponentSelection?: boolean; + requiresEditorComponentSelection?: boolean; + requiresInput?: boolean; + inputPlaceholder?: string; + inputType?: 'text' | 'number' | 'textarea' | 'json'; + validation?: (value: string) => string | null; + execute: (params: ActionExecuteParams) => Promise; +} + +export interface ActionExecuteParams { + actionKey: string; + actionValue: string; + selectedComponent: string | null; + selectedEditorComponent: string | null; + editorState: any; +} + +export interface ActionCategory { + key: string; + label: string; + actions: ActionConfig[]; +} + +export interface ActionRegistry { + categories: ActionCategory[]; + actions: Map; +} \ No newline at end of file diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts new file mode 100644 index 000000000..3f3ae1b73 --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts @@ -0,0 +1,139 @@ +import { evalFunc } from "lowcoder-core"; +import { runScriptInHost } from "util/commonUtils"; +import log from "loglevel"; +import { UICompCategory, UICompManifest, uiCompCategoryNames, uiCompRegistry } from "comps/uiCompRegistry"; +import { MenuProps } from "antd/es/menu"; +import React from "react"; +import { EditorState } from "@lowcoder-ee/comps/editorState"; + +export function runScript(code: string, inHost?: boolean) { + if (inHost) { + runScriptInHost(code); + return; + } + try { + evalFunc(code, {}, {}); + } catch (e) { + log.error(e); + } +} + +export function generateComponentActionItems(categories: Record) { + const componentItems: MenuProps['items'] = []; + + Object.entries(categories).forEach(([categoryKey, components]) => { + if (components.length > 0) { + componentItems.push({ + label: uiCompCategoryNames[categoryKey as UICompCategory], + key: `category-${categoryKey}`, + disabled: true, + style: { fontWeight: 'bold', color: '#666' } + }); + + components.forEach(([compName, manifest]) => { + componentItems.push({ + label: manifest.name, + key: `comp-${compName}`, + icon: React.createElement(manifest.icon, { width: 14, height: 14 }) + }); + }); + } + }); + + return componentItems; +} + +export function getComponentCategories() { + const cats: Record = Object.fromEntries( + Object.keys(uiCompCategoryNames).map((cat) => [cat, []]) + ); + Object.entries(uiCompRegistry).forEach(([name, manifest]) => { + manifest.categories.forEach((cat) => { + cats[cat].push([name, manifest]); + }); + }); + return cats; +} +export function getEditorComponentInfo(editorState: EditorState, componentName: string): { + componentKey: string | null; + currentLayout: any; + simpleContainer: any; + componentType?: string | null; + items: any; +} | null { + try { + // Get the UI component container + if (!editorState || !componentName) { + return null; + } + + const uiComp = editorState.getUIComp(); + const container = uiComp.getComp(); + if (!container) { + return null; + } + + const uiCompTree = uiComp.getTree(); + + // Get the simple container (the actual grid container) + const simpleContainer = container.realSimpleContainer(); + if (!simpleContainer) { + return null; + } + + // Get current layout and items + const currentLayout = simpleContainer.children.layout.getView(); + const items = getCombinedItems(uiCompTree); + + // Find the component by name and get its key + let componentKey: string | null = null; + let componentType: string | null = null; + + for (const [key, item] of Object.entries(items)) { + if ((item as any).children.name.getView() === componentName) { + componentKey = key; + componentType = (item as any).children.compType.getView(); + break + } + } + + return { + componentKey, + currentLayout, + simpleContainer, + componentType, + items, + }; + } catch(error) { + console.error('Error getting editor component key:', error); + return null; + } +} + +interface Container { + items?: Record; +} + +function getCombinedItems(uiCompTree: any) { + const combined: Record = {}; + + if (uiCompTree.items) { + Object.entries(uiCompTree.items).forEach(([itemKey, itemValue]) => { + combined[itemKey] = itemValue; + }); + } + + if (uiCompTree.children) { + Object.entries(uiCompTree.children).forEach(([parentKey, container]) => { + const typedContainer = container as Container; + if (typedContainer.items) { + Object.entries(typedContainer.items).forEach(([itemKey, itemValue]) => { + itemValue.parentContainer = parentKey; + combined[itemKey] = itemValue; + }); + } + }); + } + + return combined; +}