From d3142c1c35500f5974e994763f415c34ffd474e4 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Tue, 8 Jul 2025 12:54:58 +0500 Subject: [PATCH 1/9] Adds dynamic layer indexing --- .../comps/comps/preLoadComp/actionConfigs.ts | 5 +- .../comps/preLoadComp/actionInputSection.tsx | 77 ++++++++++++++- .../preLoadComp/actions/componentLayout.ts | 99 +++++++++++++++++++ .../comps/comps/preLoadComp/actions/index.ts | 2 +- .../src/comps/comps/preLoadComp/types.ts | 2 + .../src/comps/comps/preLoadComp/utils.ts | 25 ++++- 6 files changed, 201 insertions(+), 9 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index 02f0bbe73..2d4d130b6 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -9,7 +9,8 @@ import { changeLayoutAction, addEventHandlerAction, applyStyleAction, - nestComponentAction + nestComponentAction, + updateDynamicLayoutAction } from "./actions"; export const actionCategories: ActionCategory[] = [ @@ -33,7 +34,7 @@ export const actionCategories: ActionCategory[] = [ { key: 'layout', label: 'Layout', - actions: [changeLayoutAction] + actions: [changeLayoutAction, updateDynamicLayoutAction] }, { key: 'events', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx index a78ab6fb3..5fd4fe313 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -12,11 +12,11 @@ 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 { BaseSection, Dropdown } from "lowcoder-design"; import { EditorContext } from "comps/editorState"; import { message } from "antd"; import { CustomDropdown } from "./styled"; -import { generateComponentActionItems, getComponentCategories } from "./utils"; +import { generateComponentActionItems, getComponentCategories, getEditorComponentInfo, getLayoutItemsOrder } from "./utils"; import { actionRegistry, getAllActionItems } from "./actionConfigs"; export function ActionInputSection() { @@ -31,6 +31,8 @@ export function ActionInputSection() { const [showStylingInput, setShowStylingInput] = useState(false); const [selectedEditorComponent, setSelectedEditorComponent] = useState(null); const [validationError, setValidationError] = useState(null); + const [showDynamicLayoutDropdown, setShowDynamicLayoutDropdown] = useState(false); + const [selectedDynamicLayoutIndex, setSelectedDynamicLayoutIndex] = useState(null); const inputRef = useRef(null); const editorState = useContext(EditorContext); @@ -56,6 +58,25 @@ export function ActionInputSection() { })); }, [editorState]); + const simpleLayoutItems = useMemo(() => { + if(!editorComponents) return []; + + const editorComponentInfo = getEditorComponentInfo(editorState); + if(!editorComponentInfo) return []; + + const currentLayout = editorComponentInfo.currentLayout; + const items = editorComponentInfo.items; + + return Object.keys(currentLayout).map((key) => { + const item = items ? items[key] : null; + const componentName = item ? (item as any).children.name.getView() : key; + return { + label: componentName, + key: componentName + }; + }); + }, [editorState]); + const currentAction = useMemo(() => { return selectedActionKey ? actionRegistry.get(selectedActionKey) : null; }, [selectedActionKey]); @@ -81,7 +102,9 @@ export function ActionInputSection() { setSelectedEditorComponent(null); setIsNestedComponent(false); setSelectedNestComponent(null); + setShowDynamicLayoutDropdown(false); setActionValue(""); + setSelectedDynamicLayoutIndex(null); if (action.requiresComponentSelection) { setShowComponentDropdown(true); @@ -103,6 +126,9 @@ export function ActionInputSection() { if (action.isNested) { setIsNestedComponent(true); } + if(action.dynamicLayout) { + setShowDynamicLayoutDropdown(true); + } }, []); const handleComponentSelection = useCallback((key: string) => { @@ -175,6 +201,7 @@ export function ActionInputSection() { selectedComponent, selectedEditorComponent, selectedNestComponent, + selectedDynamicLayoutIndex, editorState }); @@ -189,6 +216,8 @@ export function ActionInputSection() { setValidationError(null); setIsNestedComponent(false); setSelectedNestComponent(null); + setShowDynamicLayoutDropdown(false); + setSelectedDynamicLayoutIndex(null); } catch (error) { console.error('Error executing action:', error); @@ -200,6 +229,7 @@ export function ActionInputSection() { selectedComponent, selectedEditorComponent, selectedNestComponent, + selectedDynamicLayoutIndex, editorState, currentAction, validateInput @@ -299,7 +329,7 @@ export function ActionInputSection() { popupRender={() => ( { + onClick={({key}) => { handleEditorComponentSelection(key); }} /> @@ -314,6 +344,47 @@ export function ActionInputSection() { )} + {showDynamicLayoutDropdown && ( + ( + { + handleEditorComponentSelection(key); + }} + /> + )} + > + + + )} + + {showDynamicLayoutDropdown && ( + { + setSelectedDynamicLayoutIndex(value); + }} + > + + + )} + {shouldShowInput && ( showStylingInput ? ( { + const { selectedDynamicLayoutIndex, selectedEditorComponent, editorState } = params; + + if (!selectedEditorComponent || !editorState || !selectedDynamicLayoutIndex) { + message.error('Component, editor state, and layout index are required'); + return; + } + + try { + 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 newPos = parseInt(selectedDynamicLayoutIndex); + + if (isNaN(newPos)) { + message.error('Invalid layout index provided'); + return; + } + + const currentPos = currentLayoutItem.pos || 0; + const layoutItems = Object.entries(currentLayout).map(([key, item]) => ({ + key, + item: item as any, + pos: (item as any).pos || 0 + })).sort((a, b) => a.pos - b.pos); + + const otherItems = layoutItems.filter(item => item.key !== componentKey); + const newLayout: any = {}; + + newLayout[componentKey] = { + ...currentLayoutItem, + pos: newPos + }; + + // Update other components with shifted positions + otherItems.forEach((item) => { + let adjustedPos = item.pos; + + // If moving to a position before the current position, shift items in between + if (newPos < currentPos && item.pos >= newPos && item.pos < currentPos) { + adjustedPos = item.pos + 1; + } + // If moving to a position after the current position, shift items in between + else if (newPos > currentPos && item.pos > currentPos && item.pos <= newPos) { + adjustedPos = item.pos - 1; + } + + newLayout[item.key] = { + ...item.item, + pos: adjustedPos + }; + }); + + 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]), "layoutComp"); + + message.success(`Component "${selectedEditorComponent}" moved to position ${newPos}`); + + } catch (error) { + console.error('Error updating dynamic layout:', error); + message.error('Failed to update dynamic layout. Please try again.'); + } + } }; \ 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 index 840000050..cee628918 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts @@ -5,7 +5,7 @@ export * from './componentManagement'; export { configureComponentAction } from './componentConfiguration'; // Layout Actions -export { changeLayoutAction } from './componentLayout'; +export { changeLayoutAction, updateDynamicLayoutAction } from './componentLayout'; // Event Actions export { addEventHandlerAction } from './componentEvents'; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts index b6318f965..51e34e9df 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts @@ -35,6 +35,7 @@ export interface ActionConfig { requiresInput?: boolean; requiresStyle?: boolean; isNested?: boolean; + dynamicLayout?: boolean; inputPlaceholder?: string; inputType?: 'text' | 'number' | 'textarea' | 'json'; validation?: (value: string) => string | null; @@ -48,6 +49,7 @@ export interface ActionExecuteParams { selectedComponent: string | null; selectedEditorComponent: string | null; selectedNestComponent: string | null; + selectedDynamicLayoutIndex: string | null; editorState: any; } diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts index b30f02fb2..5d9772211 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts @@ -54,7 +54,7 @@ export function getComponentCategories() { }); return cats; } -export function getEditorComponentInfo(editorState: EditorState, componentName: string): { +export function getEditorComponentInfo(editorState: EditorState, componentName?: string): { componentKey: string | null; currentLayout: any; simpleContainer: any; @@ -63,7 +63,7 @@ export function getEditorComponentInfo(editorState: EditorState, componentName: } | null { try { // Get the UI component container - if (!editorState || !componentName) { + if (!editorState) { return null; } @@ -85,6 +85,16 @@ export function getEditorComponentInfo(editorState: EditorState, componentName: const currentLayout = simpleContainer.children.layout.getView(); const items = getCombinedItems(uiCompTree); + // If no componentName is provided, return all items + if (!componentName) { + return { + componentKey: null, + currentLayout, + simpleContainer, + items, + }; + } + // Find the component by name and get its key let componentKey: string | null = null; let componentType: string | null = null; @@ -93,7 +103,7 @@ export function getEditorComponentInfo(editorState: EditorState, componentName: if ((item as any).children.name.getView() === componentName) { componentKey = key; componentType = (item as any).children.compType.getView(); - break + break; } } @@ -137,3 +147,12 @@ function getCombinedItems(uiCompTree: any) { return combined; } + +export function getLayoutItemsOrder(layoutItems: any[]){ + const maxIndex = layoutItems.length; + return Array.from({ length: maxIndex }, (_, index) => ({ + key: index, + label: `Position ${index}`, + value: index.toString() + })); +} From 86a12f01fc20bc0bffb1a7bd15e93069a4a70dfc Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Tue, 8 Jul 2025 18:17:59 +0500 Subject: [PATCH 2/9] fixed: Layout components issue, children components depth issues --- .../actions/componentManagement.ts | 66 ++++++---- .../src/comps/comps/preLoadComp/utils.ts | 117 +++++++++++++++--- 2 files changed, 138 insertions(+), 45 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts index a7783a8a0..647d48df1 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts @@ -12,7 +12,7 @@ import { wrapChildAction, deleteCompAction } from "lowcoder-core"; -import { getEditorComponentInfo } from "../utils"; +import { getEditorComponentInfo, findTargetComponent } from "../utils"; import { getPromiseAfterDispatch } from "@lowcoder-ee/util/promiseUtils"; import { hookCompCategory, HookCompType } from "@lowcoder-ee/comps/hooks/hookCompTypes"; @@ -314,17 +314,22 @@ export const deleteComponentAction: ActionConfig = { return; } - const { componentKey, currentLayout, simpleContainer, componentType } = componentInfo; - - if (!componentKey || !currentLayout[componentKey]) { - message.error(`Component "${selectedEditorComponent}" not found in layout`); + const { allAppComponents } = componentInfo; + const targetComponent = allAppComponents.find(comp => comp.name === selectedEditorComponent); + + if (!targetComponent) { + message.error(`Component "${selectedEditorComponent}" not found in application`); return; } - const newLayout = { ...currentLayout }; - delete newLayout[componentKey]; + const targetInfo = findTargetComponent(editorState, selectedEditorComponent); - simpleContainer.dispatch( + if (targetInfo) { + const { container, layout, componentKey } = targetInfo; + const newLayout = { ...layout }; + delete newLayout[componentKey]; + + container.dispatch( wrapActionExtraInfo( multiChangeAction({ layout: changeValueAction(newLayout, true), @@ -333,7 +338,7 @@ export const deleteComponentAction: ActionConfig = { { compInfos: [{ compName: selectedEditorComponent, - compType: componentType || 'unknown', + compType: targetComponent.compType || 'unknown', type: "delete" }] } @@ -343,6 +348,9 @@ export const deleteComponentAction: ActionConfig = { editorState.setSelectedCompNames(new Set(), "deleteComp"); message.success(`Component "${selectedEditorComponent}" deleted successfully`); + } else { + message.error(`Component "${selectedEditorComponent}" not found in any container`); + } } catch (error) { console.error('Error deleting component:', error); message.error('Failed to delete component. Please try again.'); @@ -401,6 +409,15 @@ export const moveComponentAction: ActionConfig = { return; } + const targetInfo = findTargetComponent(editorState, selectedEditorComponent); + + if (!targetInfo) { + message.error(`Component "${selectedEditorComponent}" not found in any container`); + return; + } + + const { container, layout, componentKey } = targetInfo; + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); if (!componentInfo) { @@ -408,15 +425,13 @@ export const moveComponentAction: ActionConfig = { return; } - const { componentKey, currentLayout, simpleContainer } = componentInfo; - - if (!componentKey || !currentLayout[componentKey]) { + if (!componentKey || !layout[componentKey]) { message.error(`Component "${selectedEditorComponent}" not found in layout`); return; } - const currentLayoutItem = currentLayout[componentKey]; - const items = simpleContainer.children.items.children; + const currentLayoutItem = layout[componentKey]; + const items = container.children.items.children; const newLayoutItem = { ...currentLayoutItem, @@ -425,11 +440,11 @@ export const moveComponentAction: ActionConfig = { }; const newLayout = { - ...currentLayout, + ...layout, [componentKey]: newLayoutItem, }; - simpleContainer.dispatch( + container.dispatch( wrapActionExtraInfo( multiChangeAction({ layout: changeValueAction(newLayout, true), @@ -568,21 +583,22 @@ export const resizeComponentAction: ActionConfig = { return; } - const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); - - if (!componentInfo) { - message.error(`Component "${selectedEditorComponent}" not found`); + const targetInfo = findTargetComponent(editorState, selectedEditorComponent); + + if (!targetInfo) { + message.error(`Component "${selectedEditorComponent}" not found in any container`); return; } - const { componentKey, currentLayout, simpleContainer, items } = componentInfo; + const { container, layout, componentKey } = targetInfo; - if (!componentKey || !currentLayout[componentKey]) { + if (!componentKey || !layout[componentKey]) { message.error(`Component "${selectedEditorComponent}" not found in layout`); return; } - const currentLayoutItem = currentLayout[componentKey]; + const currentLayoutItem = layout[componentKey]; + const items = container.children.items.children; const newLayoutItem = { ...currentLayoutItem, @@ -591,11 +607,11 @@ export const resizeComponentAction: ActionConfig = { }; const newLayout = { - ...currentLayout, + ...layout, [componentKey]: newLayoutItem, }; - simpleContainer.dispatch( + container.dispatch( wrapActionExtraInfo( multiChangeAction({ layout: changeValueAction(newLayout, true), diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts index 5d9772211..72919356c 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts @@ -5,6 +5,7 @@ import { UICompCategory, UICompManifest, uiCompCategoryNames, uiCompRegistry } f import { MenuProps } from "antd/es/menu"; import React from "react"; import { EditorState } from "@lowcoder-ee/comps/editorState"; +import { getAllCompItems } from "comps/comps/containerBase/utils"; export function runScript(code: string, inHost?: boolean) { if (inHost) { @@ -60,6 +61,7 @@ export function getEditorComponentInfo(editorState: EditorState, componentName?: simpleContainer: any; componentType?: string | null; items: any; + allAppComponents: any[]; } | null { try { // Get the UI component container @@ -83,7 +85,9 @@ export function getEditorComponentInfo(editorState: EditorState, componentName?: // Get current layout and items const currentLayout = simpleContainer.children.layout.getView(); + const items = getCombinedItems(uiCompTree); + const allAppComponents = getAllLayoutComponentsFromTree(uiCompTree); // If no componentName is provided, return all items if (!componentName) { @@ -92,6 +96,7 @@ export function getEditorComponentInfo(editorState: EditorState, componentName?: currentLayout, simpleContainer, items, + allAppComponents, }; } @@ -113,6 +118,7 @@ export function getEditorComponentInfo(editorState: EditorState, componentName?: simpleContainer, componentType, items, + allAppComponents, }; } catch(error) { console.error('Error getting editor component key:', error); @@ -120,31 +126,27 @@ export function getEditorComponentInfo(editorState: EditorState, componentName?: } } -interface Container { - items?: Record; -} - -function getCombinedItems(uiCompTree: any) { +function getCombinedItems(uiCompTree: any, parentPath: string[] = []): Record { const combined: Record = {}; - if (uiCompTree.items) { - Object.entries(uiCompTree.items).forEach(([itemKey, itemValue]) => { - combined[itemKey] = itemValue; - }); - } + function processContainer(container: any, currentPath: string[]) { + if (container.items) { + Object.entries(container.items).forEach(([itemKey, itemValue]) => { + (itemValue as any).parentPath = [...currentPath]; + 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; - }); - } - }); + if (container.children) { + Object.entries(container.children).forEach(([childKey, childContainer]) => { + const newPath = [...currentPath, childKey]; + processContainer(childContainer, newPath); + }); + } } + processContainer(uiCompTree, parentPath); + return combined; } @@ -156,3 +158,78 @@ export function getLayoutItemsOrder(layoutItems: any[]){ value: index.toString() })); } + +function getAllLayoutComponentsFromTree(compTree: any): any[] { + try { + const allCompItems = getAllCompItems(compTree); + + return Object.entries(allCompItems).map(([itemKey, item]) => { + const compItem = item as any; + if (compItem && compItem.children) { + return { + id: itemKey, + compType: compItem.children.compType?.getView(), + name: compItem.children.name?.getView(), + key: itemKey, + comp: compItem.children.comp, + autoHeight: compItem.autoHeight?.(), + hidden: compItem.children.comp?.children?.hidden?.getView(), + parentPath: compItem.parentPath || [] + }; + } + }); + } catch (error) { + console.error('Error getting all app components from tree:', error); + return []; + } +} + +export function getAllContainers(editorState: any) { + const containers: Array<{container: any, path: string[]}> = []; + + function findContainers(comp: any, path: string[] = []) { + if (!comp) return; + + if (comp.realSimpleContainer && typeof comp.realSimpleContainer === 'function') { + const simpleContainer = comp.realSimpleContainer(); + if (simpleContainer) { + containers.push({ container: simpleContainer, path }); + } + } + + if (comp.children) { + Object.entries(comp.children).forEach(([key, child]) => { + findContainers(child, [...path, key]); + }); + } + } + + const uiComp = editorState.getUIComp(); + const container = uiComp.getComp(); + if (container) { + findContainers(container); + } + + return containers; +} + +export function findTargetComponent(editorState: any, selectedEditorComponent: string) { + const allContainers = getAllContainers(editorState); + + for (const containerInfo of allContainers) { + const containerLayout = containerInfo.container.children.layout.getView(); + const containerItems = containerInfo.container.children.items.children; + + for (const [key, item] of Object.entries(containerItems)) { + if ((item as any).children.name.getView() === selectedEditorComponent) { + return { + container: containerInfo.container, + layout: containerLayout, + componentKey: key + }; + } + } + } + + return null; +} \ No newline at end of file From 79b5e83bbdda8d0e2bf11d1fe97f433678c4c0bc Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Wed, 9 Jul 2025 16:05:17 +0500 Subject: [PATCH 3/9] Event handlers for all components. --- .../comps/comps/preLoadComp/actionConfigs.ts | 2 +- .../preLoadComp/actions/componentEvents.ts | 176 +++++++++++++++++- 2 files changed, 170 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index 2d4d130b6..e2b037bde 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -34,7 +34,7 @@ export const actionCategories: ActionCategory[] = [ { key: 'layout', label: 'Layout', - actions: [changeLayoutAction, updateDynamicLayoutAction] + actions: [updateDynamicLayoutAction] }, { key: 'events', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts index cf007c5af..1d73c20b0 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentEvents.ts @@ -1,5 +1,29 @@ +/** + * Event Names: + * - click: Triggered when component is clicked + * - change: Triggered when component value changes + * - focus: Triggered when component gains focus + * - blur: Triggered when component loses focus + * - submit: Triggered when form is submitted + * - refresh: Triggered when component is refreshed + * + * Action Types: + * - executeQuery: Run a data query + * - message: Show a notification message + * - setTempState: Set a temporary state value + * - runScript: Execute JavaScript code + * - executeComp: Control another component + * - goToURL: Navigate to a URL + * - copyToClipboard: Copy data to clipboard + * - download: Download data as file + * - triggerModuleEvent: Trigger a module event + * - openAppPage: Navigate to another app page + */ + import { message } from "antd"; import { ActionConfig, ActionExecuteParams } from "../types"; +import { getEditorComponentInfo } from "../utils"; +import { pushAction } from "comps/generators/list"; export const addEventHandlerAction: ActionConfig = { key: 'add-event-handler', @@ -7,14 +31,152 @@ export const addEventHandlerAction: ActionConfig = { category: 'events', requiresEditorComponentSelection: true, requiresInput: true, - inputPlaceholder: 'Enter event handler code (JavaScript)', - inputType: 'textarea', + inputPlaceholder: 'Format: eventName: actionType (e.g., "click: message", "change: executeQuery", "focus: setTempState")', + inputType: 'text', + validation: (value: string) => { + const [eventName, actionType] = value.split(':').map(s => s.trim()); + if (!eventName || !actionType) { + return 'Please provide both event name and action type separated by colon (e.g., "click: message")'; + } + + const validActionTypes = [ + 'executeQuery', 'message', 'setTempState', 'runScript', + 'executeComp', 'goToURL', 'copyToClipboard', 'download', + 'triggerModuleEvent', 'openAppPage' + ]; + + if (!validActionTypes.includes(actionType)) { + return `Invalid action type. Valid types: ${validActionTypes.join(', ')}`; + } + + return null; + }, execute: async (params: ActionExecuteParams) => { - const { selectedEditorComponent, actionValue } = params; + const { selectedEditorComponent, actionValue, editorState } = params; + + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent as string); + + if (!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { allAppComponents } = componentInfo; + const targetComponent = allAppComponents.find(comp => comp.name === selectedEditorComponent); + + if (!targetComponent?.comp?.children?.onEvent) { + message.error(`Component "${selectedEditorComponent}" does not support event handlers`); + return; + } + + // ----- To be Removed after n8n integration ------ // + const [eventName, actionType] = actionValue.split(':').map(s => s.trim()); + + if (!eventName || !actionType) { + message.error('Please provide event name and action type in format: "eventName: actionType"'); + return; + } + const eventConfigs = targetComponent.comp.children.onEvent.getEventNames?.() || []; + const availableEvents = eventConfigs.map((config: any) => config.value); + + if (!availableEvents.includes(eventName)) { + const availableEventsList = availableEvents.length > 0 ? availableEvents.join(', ') : 'none'; + message.error(`Event "${eventName}" is not available for this component. Available events: ${availableEventsList}`); + return; + } + // ----- To be Removed after n8n integration ------ // + + + const eventHandler = { + name: eventName, + handler: { + compType: actionType, + comp: getActionConfig(actionType, editorState) + } + }; + + try { + targetComponent.comp.children.onEvent.dispatch(pushAction(eventHandler)); + message.success(`Event handler for "${eventName}" with action "${actionType}" added successfully!`); + } catch (error) { + console.error('Error adding event handler:', error); + message.error('Failed to add event handler. Please try again.'); + } + } +}; + +// A Hardcoded function to get action configuration based on action type +// This will be removed after n8n integration +function getActionConfig(actionType: string, editorState: any) { + switch (actionType) { + case 'executeQuery': + const queryVariables = editorState + ?.selectedOrFirstQueryComp() + ?.children.variables.toJsonValue(); + + return { + queryName: editorState + ?.selectedOrFirstQueryComp() + ?.children.name.getView(), + queryVariables: queryVariables?.map((variable: any) => ({...variable, value: ''})), + }; + + case 'message': + return { + text: "Event triggered!", + level: "info", + duration: 3000 + }; + + case 'setTempState': + return { + state: "tempState", + value: "{{eventData}}" + }; + + case 'runScript': + return { + script: "console.log('Event triggered:', eventData);" + }; + + case 'executeComp': + return { + compName: "", + methodName: "", + params: [] + }; + + case 'goToURL': + return { + url: "https://example.com", + openInNewTab: false + }; + + case 'copyToClipboard': + return { + value: "{{eventData}}" + }; + + case 'download': + return { + data: "{{eventData}}", + fileName: "download.txt", + fileType: "text/plain" + }; + + case 'triggerModuleEvent': + return { + name: "moduleEvent" + }; - console.log('Adding event handler to component:', selectedEditorComponent, 'with code:', actionValue); - message.info(`Event handler added to component "${selectedEditorComponent}"`); + case 'openAppPage': + return { + appId: "", + queryParams: [], + hashParams: [] + }; - // TODO: Implement actual event handler logic + default: + return {}; } -}; \ No newline at end of file +} \ No newline at end of file From d13b799692c92edfd07add0e635d0dbad3ef1ca7 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Thu, 10 Jul 2025 14:21:02 +0500 Subject: [PATCH 4/9] Update App meta data --- .../comps/comps/preLoadComp/actionConfigs.ts | 9 ++-- .../comps/preLoadComp/actionInputSection.tsx | 7 ++- .../preLoadComp/actions/appConfiguration.ts | 46 +++++++++++++++++++ .../actions/componentConfiguration.ts | 2 +- .../comps/comps/preLoadComp/actions/index.ts | 2 +- 5 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index e2b037bde..bd5fc12e3 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -5,8 +5,7 @@ import { renameComponentAction, deleteComponentAction, resizeComponentAction, - configureComponentAction, - changeLayoutAction, + configureAppMetaAction, addEventHandlerAction, applyStyleAction, nestComponentAction, @@ -27,9 +26,9 @@ export const actionCategories: ActionCategory[] = [ ] }, { - key: 'component-configuration', - label: 'Component Configuration', - actions: [configureComponentAction] + key: 'app-configuration', + label: 'App Configuration', + actions: [configureAppMetaAction] }, { key: 'layout', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx index 5fd4fe313..0b243219e 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -16,7 +16,12 @@ import { BaseSection, Dropdown } from "lowcoder-design"; import { EditorContext } from "comps/editorState"; import { message } from "antd"; import { CustomDropdown } from "./styled"; -import { generateComponentActionItems, getComponentCategories, getEditorComponentInfo, getLayoutItemsOrder } from "./utils"; +import { + generateComponentActionItems, + getComponentCategories, + getEditorComponentInfo, + getLayoutItemsOrder +} from "./utils"; import { actionRegistry, getAllActionItems } from "./actionConfigs"; export function ActionInputSection() { diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts new file mode 100644 index 000000000..10d4d518c --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -0,0 +1,46 @@ +import { message } from "antd"; +import { ActionConfig, ActionExecuteParams } from "../types"; + +export const configureAppMetaAction: ActionConfig = { + key: 'configure-app-meta', + label: 'Configure app meta data', + category: 'app-configuration', + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { editorState } = params; + const appSettingsComp = editorState.getAppSettingsComp(); + + try { + // TODO: Get config data from the user + let configData = { + title: "Test Title", + description: "Test Description", + category: "Test Category" + }; + + if (configData.title && appSettingsComp?.children?.title) { + appSettingsComp.children.title.dispatchChangeValueAction(configData.title); + } + + if (configData.description && appSettingsComp?.children?.description) { + appSettingsComp.children.description.dispatchChangeValueAction(configData.description); + } + + if (configData.category && appSettingsComp?.children?.category) { + appSettingsComp.children.category.dispatchChangeValueAction(configData.category); + } + + // Display error message if no valid configuration data is provided + const updatedFields = []; + if (configData.title) updatedFields.push('title'); + if (configData.description) updatedFields.push('description'); + if (configData.category) updatedFields.push('category'); + + !updatedFields.length && message.info('No valid configuration data provided'); + + } catch (error) { + console.error('Error updating app settings:', error); + message.error('Failed to update app configuration'); + } + } +}; \ 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 index fefca21d0..e691b818d 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentConfiguration.ts @@ -62,4 +62,4 @@ export const configureComponentAction: ActionConfig = { message.error('Invalid configuration format'); } } -}; \ No newline at end of file +}; \ 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 index cee628918..c9c6efa43 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts @@ -2,7 +2,7 @@ export * from './componentManagement'; // Component Configuration Actions -export { configureComponentAction } from './componentConfiguration'; +export * from './appConfiguration'; // Layout Actions export { changeLayoutAction, updateDynamicLayoutAction } from './componentLayout'; From 8afeae1ee275487d1eabc1ca8d1f1e972897a574 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Thu, 10 Jul 2025 20:47:47 +0500 Subject: [PATCH 5/9] Publish App, Test App, Apply Global JS and CSS --- .../comps/comps/preLoadComp/actionConfigs.ts | 16 +- .../preLoadComp/actions/appConfiguration.ts | 254 +++++++++++++++++- .../src/comps/comps/preLoadComp/tabPanes.tsx | 7 +- 3 files changed, 273 insertions(+), 4 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index bd5fc12e3..10da15479 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -9,7 +9,12 @@ import { addEventHandlerAction, applyStyleAction, nestComponentAction, - updateDynamicLayoutAction + updateDynamicLayoutAction, + publishAppAction, + shareAppAction, + testAllDatasourcesAction, + applyGlobalJSAction, + applyGlobalCSSAction } from "./actions"; export const actionCategories: ActionCategory[] = [ @@ -28,7 +33,14 @@ export const actionCategories: ActionCategory[] = [ { key: 'app-configuration', label: 'App Configuration', - actions: [configureAppMetaAction] + actions: [ + configureAppMetaAction, + publishAppAction, + shareAppAction, + testAllDatasourcesAction, + applyGlobalJSAction, + applyGlobalCSSAction + ] }, { key: 'layout', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts index 10d4d518c..1d0875175 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -1,5 +1,11 @@ import { message } from "antd"; import { ActionConfig, ActionExecuteParams } from "../types"; +import ApplicationApi from "api/applicationApi"; +import { getApplicationIdInReducer } from "comps/utils/reduceContext"; +import { executeQueryAction } from "lowcoder-core"; +import { getPromiseAfterDispatch } from "util/promiseUtils"; +import { runScript } from "../utils"; + export const configureAppMetaAction: ActionConfig = { key: 'configure-app-meta', @@ -43,4 +49,250 @@ export const configureAppMetaAction: ActionConfig = { message.error('Failed to update app configuration'); } } -}; \ No newline at end of file +}; + +export const publishAppAction: ActionConfig = { + key: 'publish-app', + label: 'Publish app', + category: 'app-configuration', + requiresInput: false, + execute: async () => { + try { + const applicationId = getApplicationIdInReducer(); + + if (!applicationId) { + message.error('Application ID not found'); + return; + } + + const response = await ApplicationApi.publishApplication({ applicationId }); + + if (response.data.success) { + message.success('Application published successfully'); + window.open(`/applications/${applicationId}/view`, '_blank'); + } else { + message.error('Failed to publish application'); + } + + } catch (error) { + console.error('Error publishing application:', error); + message.error('Failed to publish application'); + } + } +}; + +export const shareAppAction: ActionConfig = { + key: 'share-app', + label: 'Share app', + category: 'app-configuration', + requiresInput: false, + execute: async () => { + // TODO: Implement share app + console.log('Share app'); + } +}; + +export const testAllDatasourcesAction: ActionConfig = { + key: 'test-all-datasources', + label: 'Test all datasources', + category: 'app-configuration', + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { editorState } = params; + + try { + const allQueries = editorState.getQueriesComp().getView(); + + if (!allQueries || !allQueries.length) { + message.info('No queries found in the application'); + return; + } + + console.log(`Found ${allQueries.length} queries to test`); + + const results = { + total: allQueries.length, + successful: 0, + failed: 0, + errors: [] as Array<{ queryName: string; error: string }> + }; + + message.loading(`Testing ${allQueries.length} queries...`, 0); + + for (let i = 0; i < allQueries.length; i++) { + const query = allQueries[i]; + const queryName = query.children.name.getView(); + + try { + await getPromiseAfterDispatch( + query.dispatch, + executeQueryAction({ + // In some data queries, we need to pass args to the query + // Currently, we don't have a way to pass args to the query + // So we are passing an empty object + args: {}, + afterExecFunc: () => { + console.log(`Query ${queryName} executed successfully`); + } + }), + { + notHandledError: `Failed to execute query: ${queryName}` + } + ); + + results.successful++; + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Query ${queryName} failed:`, error); + + results.failed++; + results.errors.push({ + queryName, + error: errorMessage + }); + } + + if (i < allQueries.length - 1) { + await new Promise(resolve => setTimeout(resolve, 500)); + } + } + + message.destroy(); + + if (results.failed === 0) { + message.success(`All ${results.total} queries executed successfully!`); + } else if (results.successful === 0) { + message.error(`All ${results.total} queries failed. Check console for details.`); + } else { + message.warning( + `Query test completed: ${results.successful} successful, ${results.failed} failed` + ); + } + + console.group('Query Test Results'); + console.log(`Total queries: ${results.total}`); + console.log(`Successful: ${results.successful}`); + console.log(`Failed: ${results.failed}`); + + if (results.errors.length > 0) { + console.group('Failed Queries:'); + results.errors.forEach(({ queryName, error }) => { + console.error(`${queryName}: ${error}`); + }); + console.groupEnd(); + } + console.groupEnd(); + + } catch (error) { + message.destroy(); + console.error('Error during application testing:', error); + message.error('Failed to test application. Check console for details.'); + } + } +}; + +export const applyGlobalJSAction: ActionConfig = { + key: 'apply-global-js', + label: 'Apply global JS', + category: 'app-configuration', + requiresInput: true, + inputPlaceholder: 'Enter JavaScript code to apply globally...', + inputType: 'textarea', + validation: (value: string) => { + if (!value.trim()) { + return 'JavaScript code is required'; + } + try { + new Function(value); + return null; + } catch (error) { + return 'Invalid JavaScript syntax'; + } + }, + execute: async (params: ActionExecuteParams) => { + const { editorState, actionValue } = params; + + try { + const defaultJS = `console.log('Please provide a valid JavaScript code');`.trim(); + + const jsCode = actionValue.trim() || defaultJS; + + const preloadComp = editorState.rootComp.children.preload; + if (!preloadComp) { + message.error('Preload component not found'); + return; + } + + const scriptComp = preloadComp.children.script; + if (!scriptComp) { + message.error('Script component not found'); + return; + } + + scriptComp.dispatchChangeValueAction(jsCode); + runScript(jsCode, false); + + message.success('Global JavaScript applied successfully!'); + + } catch (error) { + console.error('Error applying global JavaScript:', error); + message.error('Failed to apply global JavaScript. Check console for details.'); + } + } +}; + +export const applyGlobalCSSAction: ActionConfig = { + key: 'apply-global-css', + label: 'Apply global CSS', + category: 'app-configuration', + requiresInput: true, + requiresStyle: true, + inputPlaceholder: 'Enter CSS code to apply globally...', + inputType: 'textarea', + validation: (value: string) => { + if (!value.trim()) { + return 'CSS code is required'; + } + const css = value.trim(); + if (!css.includes('{') || !css.includes('}')) { + return 'Invalid CSS syntax - missing braces'; + } + return null; + }, + execute: async (params: ActionExecuteParams) => { + const { editorState, actionValue } = params; + + try { + const defaultCSS = ` + body { + font-family: Arial, sans-serif; + } + `.trim(); + + const cssCode = actionValue.trim() || defaultCSS; + + const preloadComp = editorState.rootComp.children.preload; + if (!preloadComp) { + message.error('Preload component not found'); + return; + } + + const globalCSSComp = preloadComp.children.globalCSS; + if (!globalCSSComp) { + message.error('Global CSS component not found'); + return; + } + + globalCSSComp.dispatchChangeValueAction(cssCode); + + await globalCSSComp.run('global-css', cssCode); + + message.success('Global CSS applied successfully!'); + + } catch (error) { + console.error('Error applying global CSS:', error); + message.error('Failed to apply global CSS. Check console for details.'); + } + } +}; \ 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 index a1229a96d..ad5831bad 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/tabPanes.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/tabPanes.tsx @@ -3,10 +3,15 @@ import React, { useEffect } from "react"; import { trans } from "i18n"; import { ConstructorToComp } from "lowcoder-core"; import { ScriptComp, CSSComp } from "./components"; +import { runScript } from "./utils"; export function JavaScriptTabPane(props: { comp: ConstructorToComp }) { useEffect(() => { - props.comp.runPreloadScript(); + // Use the imported runScript function instead of the component's method to avoid require() issues + const code = props.comp.getView(); + if (code) { + runScript(code, false); + } }, [props.comp]); const codePlaceholder = `window.name = 'Tom';\nwindow.greet = () => "hello world";`; From 38e770c50fd67228a816a1f74ff2c15a83840b29 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Fri, 11 Jul 2025 18:48:00 +0500 Subject: [PATCH 6/9] Sharing App Permissions --- .../preLoadComp/actions/appConfiguration.ts | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts index 1d0875175..537118452 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -1,11 +1,11 @@ import { message } from "antd"; import { ActionConfig, ActionExecuteParams } from "../types"; import ApplicationApi from "api/applicationApi"; -import { getApplicationIdInReducer } from "comps/utils/reduceContext"; import { executeQueryAction } from "lowcoder-core"; import { getPromiseAfterDispatch } from "util/promiseUtils"; import { runScript } from "../utils"; - +import { updateAppPermissionInfo } from "redux/reduxActions/applicationActions"; +import { reduxStore } from "redux/store/store"; export const configureAppMetaAction: ActionConfig = { key: 'configure-app-meta', @@ -56,10 +56,12 @@ export const publishAppAction: ActionConfig = { label: 'Publish app', category: 'app-configuration', requiresInput: false, - execute: async () => { + execute: async (params: ActionExecuteParams) => { + const { editorState } = params; + const applicationIdEditor = editorState.rootComp.preloadId; + const applicationId = applicationIdEditor.replace('app-', ''); + try { - const applicationId = getApplicationIdInReducer(); - if (!applicationId) { message.error('Application ID not found'); return; @@ -86,9 +88,50 @@ export const shareAppAction: ActionConfig = { label: 'Share app', category: 'app-configuration', requiresInput: false, - execute: async () => { - // TODO: Implement share app - console.log('Share app'); + execute: async (params: ActionExecuteParams) => { + // TODO: Get app sharing from the user + const appSharing = { + public: true, + publishMarketplace: false + } + + const { editorState } = params; + const applicationIdEditor = editorState.rootComp.preloadId; + const applicationId = applicationIdEditor.replace('app-', ''); + + if (!applicationId) { + message.error('Application ID not found'); + return; + } + + try { + // Update Application Sharig Status + // Update Redux state to reflect the public change in UI + const publicResponse = await ApplicationApi.publicToAll(applicationId, appSharing.public); + + if (publicResponse.data.success) { + reduxStore.dispatch(updateAppPermissionInfo({ publicToAll: appSharing.public })); + message.success('Application is now public!'); + + // Update Application Marketplace Sharing Status + try { + const marketplaceResponse = await ApplicationApi.publicToMarketplace(applicationId, appSharing.publishMarketplace); + if (marketplaceResponse.data.success) { + reduxStore.dispatch(updateAppPermissionInfo({ publicToMarketplace: appSharing.publishMarketplace })); + message.success(`Application ${appSharing.publishMarketplace ? 'published to' : 'unpublished from'} marketplace successfully!`); + } + } catch (marketplaceError) { + console.error(`Error ${appSharing.publishMarketplace ? 'publishing to' : 'unpublishing from'} marketplace:`, marketplaceError); + message.warning(`Application is public but ${appSharing.publishMarketplace ? 'publishing to' : 'unpublishing from'} marketplace failed`); + } + + } else { + message.error('Failed to make application public'); + } + } catch (publicError) { + console.error('Error making application public:', publicError); + message.error('Failed to make application public'); + } } }; From caacd01c49ba5f772ebc47a1ce707c516fc98e7a Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Mon, 14 Jul 2025 15:49:04 +0500 Subject: [PATCH 7/9] multiple new actions added --- .../comps/comps/preLoadComp/actionConfigs.ts | 13 +- .../comps/preLoadComp/actionInputSection.tsx | 131 ++++++++++-- .../preLoadComp/actions/appConfiguration.ts | 196 ++++++++++++++++++ .../preLoadComp/actions/componentLayout.ts | 141 ++++++++++++- .../comps/comps/preLoadComp/actions/index.ts | 2 +- .../src/comps/comps/preLoadComp/types.ts | 4 + 6 files changed, 459 insertions(+), 28 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index 10da15479..f917a1311 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -14,7 +14,11 @@ import { shareAppAction, testAllDatasourcesAction, applyGlobalJSAction, - applyGlobalCSSAction + applyGlobalCSSAction, + applyThemeAction, + setCanvasSettingsAction, + setCustomShortcutsAction, + alignComponentAction } from "./actions"; export const actionCategories: ActionCategory[] = [ @@ -39,13 +43,16 @@ export const actionCategories: ActionCategory[] = [ shareAppAction, testAllDatasourcesAction, applyGlobalJSAction, - applyGlobalCSSAction + applyGlobalCSSAction, + applyThemeAction, + setCanvasSettingsAction, + setCustomShortcutsAction ] }, { key: 'layout', label: 'Layout', - actions: [updateDynamicLayoutAction] + actions: [updateDynamicLayoutAction, alignComponentAction] }, { key: 'events', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx index 0b243219e..559a1f6e0 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -23,6 +23,10 @@ import { getLayoutItemsOrder } from "./utils"; import { actionRegistry, getAllActionItems } from "./actionConfigs"; +import { getThemeList } from "@lowcoder-ee/redux/selectors/commonSettingSelectors"; +import { useSelector } from "react-redux"; +import { ActionOptions } from "comps/controls/actionSelector/actionSelectorControl"; +import { eventToShortcut, readableShortcut } from "util/keyUtils"; export function ActionInputSection() { const [actionValue, setActionValue] = useState(""); @@ -38,8 +42,20 @@ export function ActionInputSection() { const [validationError, setValidationError] = useState(null); const [showDynamicLayoutDropdown, setShowDynamicLayoutDropdown] = useState(false); const [selectedDynamicLayoutIndex, setSelectedDynamicLayoutIndex] = useState(null); + const [showThemeDropdown, setShowThemeDropdown] = useState(false); + const [selectedTheme, setSelectedTheme] = useState(null); + const [showCustomShortcutsActionDropdown, setShowCustomShortcutsActionDropdown] = useState(false); + const [selectedCustomShortcutAction, setSelectedCustomShortcutAction] = useState(null); const inputRef = useRef(null); const editorState = useContext(EditorContext); + const themeList = useSelector(getThemeList) || []; + + const THEME_OPTIONS = useMemo(() => { + return themeList.map((theme) => ({ + label: theme.name, + value: theme.id + "", + })); + }, [themeList]); const categories = useMemo(() => { return getComponentCategories(); @@ -110,7 +126,11 @@ export function ActionInputSection() { setShowDynamicLayoutDropdown(false); setActionValue(""); setSelectedDynamicLayoutIndex(null); - + setShowThemeDropdown(false); + setSelectedTheme(null); + setShowCustomShortcutsActionDropdown(false); + setSelectedCustomShortcutAction(null); + if (action.requiresComponentSelection) { setShowComponentDropdown(true); setPlaceholderText("Select a component to add"); @@ -134,6 +154,12 @@ export function ActionInputSection() { if(action.dynamicLayout) { setShowDynamicLayoutDropdown(true); } + if(action.isTheme) { + setShowThemeDropdown(true); + } + if(action.isCustomShortcuts) { + setShowCustomShortcutsActionDropdown(true); + } }, []); const handleComponentSelection = useCallback((key: string) => { @@ -199,6 +225,16 @@ export function ActionInputSection() { return; } + if(currentAction.isTheme && !selectedTheme) { + message.error('Please select a theme'); + return; + } + + if(currentAction.isCustomShortcuts && !selectedCustomShortcutAction) { + message.error('Please select a custom shortcut action'); + return; + } + try { await currentAction.execute({ actionKey: selectedActionKey, @@ -207,6 +243,8 @@ export function ActionInputSection() { selectedEditorComponent, selectedNestComponent, selectedDynamicLayoutIndex, + selectedTheme, + selectedCustomShortcutAction, editorState }); @@ -223,7 +261,10 @@ export function ActionInputSection() { setSelectedNestComponent(null); setShowDynamicLayoutDropdown(false); setSelectedDynamicLayoutIndex(null); - + setShowThemeDropdown(false); + setSelectedTheme(null); + setShowCustomShortcutsActionDropdown(false); + setSelectedCustomShortcutAction(null); } catch (error) { console.error('Error executing action:', error); message.error('Failed to execute action. Please try again.'); @@ -235,6 +276,8 @@ export function ActionInputSection() { selectedEditorComponent, selectedNestComponent, selectedDynamicLayoutIndex, + selectedTheme, + selectedCustomShortcutAction, editorState, currentAction, validateInput @@ -246,9 +289,21 @@ export function ActionInputSection() { if (currentAction.requiresComponentSelection && !selectedComponent) return true; if (currentAction.requiresEditorComponentSelection && !selectedEditorComponent) return true; if (currentAction.requiresInput && !actionValue.trim()) return true; + if (currentAction.requiresStyle && !selectedEditorComponent) return true; + if (currentAction.isTheme && !selectedTheme) return true; + if (currentAction.isCustomShortcuts && !selectedCustomShortcutAction) return true; return false; - }, [selectedActionKey, currentAction, selectedComponent, selectedEditorComponent, actionValue]); + }, [ + selectedActionKey, + currentAction, + selectedComponent, + selectedEditorComponent, + actionValue, + selectedCustomShortcutAction, + selectedTheme, + selectedNestComponent + ]); const shouldShowInput = useMemo(() => { if (!currentAction) return false; @@ -390,24 +445,70 @@ export function ActionInputSection() { )} + {showThemeDropdown && ( + { + setSelectedTheme(value); + }} + > + + + )} + + {showCustomShortcutsActionDropdown && ( + { + setSelectedCustomShortcutAction(value); + }} + > + + + )} + {shouldShowInput && ( - showStylingInput ? ( - - ) : ( + currentAction?.isCustomShortcuts ? ( { + setActionValue(eventToShortcut(e)); + e.preventDefault(); + e.stopPropagation(); + }} + onChange={() => {}} + readOnly /> + ) : ( + showStylingInput ? ( + + ) : ( + + ) ) )} diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts index 537118452..a4ad48ecd 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -6,6 +6,7 @@ import { getPromiseAfterDispatch } from "util/promiseUtils"; import { runScript } from "../utils"; import { updateAppPermissionInfo } from "redux/reduxActions/applicationActions"; import { reduxStore } from "redux/store/store"; +import { readableShortcut } from "util/keyUtils"; export const configureAppMetaAction: ActionConfig = { key: 'configure-app-meta', @@ -338,4 +339,199 @@ export const applyGlobalCSSAction: ActionConfig = { message.error('Failed to apply global CSS. Check console for details.'); } } +}; + +export const applyThemeAction: ActionConfig = { + key: 'apply-theme', + label: 'Apply theme', + category: 'app-configuration', + isTheme: true, + execute: async (params: ActionExecuteParams) => { + const { editorState, selectedTheme } = params; + + try { + if (!selectedTheme) { + message.error('No theme selected'); + return; + } + + const appSettingsComp = editorState.getAppSettingsComp(); + if (!appSettingsComp) { + message.error('App settings component not found'); + return; + } + + const themeIdComp = appSettingsComp.children.themeId; + if (!themeIdComp) { + message.error('Theme ID component not found'); + return; + } + + const DEFAULT_THEMEID = "default"; + const themeToApply = selectedTheme === DEFAULT_THEMEID ? DEFAULT_THEMEID : selectedTheme; + + themeIdComp.dispatchChangeValueAction(themeToApply); + + message.success(`Theme applied successfully: ${selectedTheme}`); + + } catch (error) { + console.error('Error applying theme:', error); + message.error('Failed to apply theme. Check console for details.'); + } + } +}; + +export const setCanvasSettingsAction: ActionConfig = { + key: 'set-canvas-settings', + label: 'Set canvas settings', + category: 'app-configuration', + requiresInput: false, + execute: async (params: ActionExecuteParams) => { + const { editorState } = params; + + // Default canvas settings + // TODO: Get canvas settings from the user + const defaultCanvasSettings = { + maxWidth: "450", + gridColumns: 12, + gridRowHeight: 8, + gridRowCount: Infinity, + gridPaddingX: 20, + gridPaddingY: 20, + gridBg: "", + gridBgImage: "", + gridBgImageRepeat: "no-repeat", + gridBgImageSize: "cover", + gridBgImagePosition: "center", + gridBgImageOrigin: "no-padding" + }; + + try { + const appSettingsComp = editorState.getAppSettingsComp(); + if (!appSettingsComp) { + message.error('App settings component not found'); + return; + } + + const { + maxWidth, + gridColumns, + gridRowHeight, + gridRowCount, + gridPaddingX, + gridPaddingY, + gridBg, + gridBgImage, + gridBgImageRepeat, + gridBgImageSize, + gridBgImagePosition, + gridBgImageOrigin, + } = appSettingsComp.children; + + if (maxWidth && defaultCanvasSettings.maxWidth) { + maxWidth.dispatchChangeValueAction(defaultCanvasSettings.maxWidth); + } + + if (gridColumns && defaultCanvasSettings.gridColumns) { + gridColumns.dispatchChangeValueAction(defaultCanvasSettings.gridColumns); + } + + if (gridRowHeight && defaultCanvasSettings.gridRowHeight) { + gridRowHeight.dispatchChangeValueAction(defaultCanvasSettings.gridRowHeight); + } + + if (gridRowCount && defaultCanvasSettings.gridRowCount) { + gridRowCount.dispatchChangeValueAction(defaultCanvasSettings.gridRowCount); + } + + if (gridPaddingX && defaultCanvasSettings.gridPaddingX) { + gridPaddingX.dispatchChangeValueAction(defaultCanvasSettings.gridPaddingX); + } + + if (gridPaddingY && defaultCanvasSettings.gridPaddingY) { + gridPaddingY.dispatchChangeValueAction(defaultCanvasSettings.gridPaddingY); + } + + if (gridBg && defaultCanvasSettings.gridBg) { + gridBg.dispatchChangeValueAction(defaultCanvasSettings.gridBg); + } + + if (gridBgImage && defaultCanvasSettings.gridBgImage) { + gridBgImage.dispatchChangeValueAction(defaultCanvasSettings.gridBgImage); + } + + if (gridBgImageRepeat && defaultCanvasSettings.gridBgImageRepeat) { + gridBgImageRepeat.dispatchChangeValueAction(defaultCanvasSettings.gridBgImageRepeat); + } + + if (gridBgImageSize && defaultCanvasSettings.gridBgImageSize) { + gridBgImageSize.dispatchChangeValueAction(defaultCanvasSettings.gridBgImageSize); + } + + if (gridBgImagePosition && defaultCanvasSettings.gridBgImagePosition) { + gridBgImagePosition.dispatchChangeValueAction(defaultCanvasSettings.gridBgImagePosition); + } + + if (gridBgImageOrigin && defaultCanvasSettings.gridBgImageOrigin) { + gridBgImageOrigin.dispatchChangeValueAction(defaultCanvasSettings.gridBgImageOrigin); + } + + message.success('Canvas settings applied successfully!'); + + } catch (error) { + console.error('Error applying canvas settings:', error); + message.error('Failed to apply canvas settings. Check console for details.'); + } + } +}; + +export const setCustomShortcutsAction: ActionConfig = { + key: 'set-custom-shortcuts', + label: 'Set custom shortcuts', + category: 'app-configuration', + isCustomShortcuts: true, + requiresInput: true, + inputPlaceholder: 'Press keys for shortcut', + inputType: 'text', + validation: (value: string) => { + if (!value.trim()) { + return 'Shortcut is required'; + } + return null; + }, + execute: async (params: ActionExecuteParams) => { + const { editorState, actionValue, selectedCustomShortcutAction } = params; + try { + if (!selectedCustomShortcutAction) { + message.error('No custom shortcut action selected'); + return; + } + + const appSettingsComp = editorState.getAppSettingsComp(); + if (!appSettingsComp) { + message.error('App settings component not found'); + return; + } + const customShortcutsComp = appSettingsComp.children.customShortcuts; + if (!customShortcutsComp) { + message.error('Custom shortcuts component not found'); + return; + } + + const newShortcutItem = { + shortcut: actionValue.trim(), + action: { + compType: selectedCustomShortcutAction, + comp: {} + } + }; + + customShortcutsComp.dispatch(customShortcutsComp.pushAction(newShortcutItem)); + const readableShortcutText = readableShortcut(actionValue.trim()); + message.success(`Custom shortcut added successfully: ${readableShortcutText} -> ${selectedCustomShortcutAction}`); + } catch (error) { + console.error('Error setting custom shortcut:', error); + message.error('Failed to set custom shortcut. Check console for details.'); + } + } }; \ 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 index b0f995be9..4b5422fcb 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentLayout.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentLayout.ts @@ -3,20 +3,143 @@ import { ActionConfig, ActionExecuteParams } from "../types"; import { getEditorComponentInfo } from "../utils"; import { changeValueAction, multiChangeAction, wrapActionExtraInfo } from "lowcoder-core"; -export const changeLayoutAction: ActionConfig = { - key: 'change-layout', - label: 'Change layout', +export const alignComponentAction: ActionConfig = { + key: 'align-component', + label: 'Align component', category: 'layout', requiresInput: true, - inputPlaceholder: 'Enter layout type (grid, flex, absolute)', + requiresEditorComponentSelection: true, + inputPlaceholder: 'Enter alignment type (left, center, right)', inputType: 'text', + validation: (value: string) => { + const alignment = value.toLowerCase().trim(); + if (!['left', 'center', 'right'].includes(alignment)) { + return 'Alignment must be one of: left, center, right'; + } + return null; + }, execute: async (params: ActionExecuteParams) => { - const { actionValue } = params; - - console.log('Changing layout to:', actionValue); - message.info(`Layout changed to: ${actionValue}`); + const { actionValue, editorState, selectedEditorComponent } = params; - // TODO: Implement actual layout change logic + if (!selectedEditorComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + const alignment = actionValue.toLowerCase().trim(); + if (!['left', 'center', 'right'].includes(alignment)) { + message.error('Invalid alignment. Must be: left, center, or right'); + return; + } + + try { + const componentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + if(!componentInfo) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey, currentLayout, simpleContainer, items } = componentInfo; + if(!componentKey) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const layout = currentLayout[componentKey]; + if(!layout) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const appSettingsComp = editorState.getAppSettingsComp(); + if (!appSettingsComp) { + message.error('App settings component not found'); + return; + } + + const gridColumns = appSettingsComp.children.gridColumns?.getView() || 24; + + const currentX = layout.x || 0; + const currentY = layout.y || 0; + const currentWidth = layout.w || 1; + const currentHeight = layout.h || 1; + + let newX = currentX; + + switch (alignment) { + case 'left': + newX = 0; + break; + case 'center': + newX = Math.max(0, Math.floor((gridColumns - currentWidth) / 2)); + break; + case 'right': + newX = Math.max(0, gridColumns - currentWidth); + break; + } + + newX = Math.max(0, Math.min(newX, gridColumns - currentWidth)); + + const newLayout = { + ...currentLayout, + [componentKey]: { + ...layout, + x: newX, + y: currentY, + w: currentWidth, + h: currentHeight, + } + }; + + Object.entries(currentLayout).forEach(([key, item]) => { + if (key !== componentKey) { + const otherItem = item as any; + const otherX = otherItem.x || 0; + const otherY = otherItem.y || 0; + const otherWidth = otherItem.w || 1; + const otherHeight = otherItem.h || 1; + + const collision = !( + newX + currentWidth <= otherX || + otherX + otherWidth <= newX || + currentY + currentHeight <= otherY || + otherY + otherHeight <= currentY + ); + + if (collision) { + newLayout[key] = { + ...otherItem, + y: Math.max(otherY, currentY + currentHeight) + }; + } else { + newLayout[key] = otherItem; + } + } + }); + + 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]), "alignComp"); + + message.success(`Component "${selectedEditorComponent}" aligned to ${alignment}`); + + } catch (error) { + console.error('Error aligning component:', error); + message.error('Failed to align component. Please try again.'); + } } }; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts index c9c6efa43..608a15156 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/index.ts @@ -5,7 +5,7 @@ export * from './componentManagement'; export * from './appConfiguration'; // Layout Actions -export { changeLayoutAction, updateDynamicLayoutAction } from './componentLayout'; +export { alignComponentAction, updateDynamicLayoutAction } from './componentLayout'; // Event Actions export { addEventHandlerAction } from './componentEvents'; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts index 51e34e9df..5c54124e2 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts @@ -34,6 +34,8 @@ export interface ActionConfig { requiresEditorComponentSelection?: boolean; requiresInput?: boolean; requiresStyle?: boolean; + isTheme?: boolean; + isCustomShortcuts?: boolean; isNested?: boolean; dynamicLayout?: boolean; inputPlaceholder?: string; @@ -50,6 +52,8 @@ export interface ActionExecuteParams { selectedEditorComponent: string | null; selectedNestComponent: string | null; selectedDynamicLayoutIndex: string | null; + selectedTheme: string | null; + selectedCustomShortcutAction: string | null; editorState: any; } From 1dddc59c9e81f35b00d05b46a7f2ca6511969964 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Mon, 14 Jul 2025 16:09:48 +0500 Subject: [PATCH 8/9] Updated Global CSS --- .../comps/comps/preLoadComp/actionConfigs.ts | 4 +-- .../comps/preLoadComp/actionInputSection.tsx | 3 --- .../preLoadComp/actions/appConfiguration.ts | 26 +++++++++---------- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index f917a1311..96d68fcbe 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -14,7 +14,7 @@ import { shareAppAction, testAllDatasourcesAction, applyGlobalJSAction, - applyGlobalCSSAction, + applyCSSAction, applyThemeAction, setCanvasSettingsAction, setCustomShortcutsAction, @@ -43,7 +43,7 @@ export const actionCategories: ActionCategory[] = [ shareAppAction, testAllDatasourcesAction, applyGlobalJSAction, - applyGlobalCSSAction, + applyCSSAction, applyThemeAction, setCanvasSettingsAction, setCustomShortcutsAction diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx index 559a1f6e0..6be8b85b6 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -289,9 +289,6 @@ export function ActionInputSection() { if (currentAction.requiresComponentSelection && !selectedComponent) return true; if (currentAction.requiresEditorComponentSelection && !selectedEditorComponent) return true; if (currentAction.requiresInput && !actionValue.trim()) return true; - if (currentAction.requiresStyle && !selectedEditorComponent) return true; - if (currentAction.isTheme && !selectedTheme) return true; - if (currentAction.isCustomShortcuts && !selectedCustomShortcutAction) return true; return false; }, [ diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts index a4ad48ecd..87a46c668 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -277,7 +277,7 @@ export const applyGlobalJSAction: ActionConfig = { scriptComp.dispatchChangeValueAction(jsCode); runScript(jsCode, false); - message.success('Global JavaScript applied successfully!'); + message.success('JavaScript applied successfully!'); } catch (error) { console.error('Error applying global JavaScript:', error); @@ -286,13 +286,13 @@ export const applyGlobalJSAction: ActionConfig = { } }; -export const applyGlobalCSSAction: ActionConfig = { - key: 'apply-global-css', - label: 'Apply global CSS', +export const applyCSSAction: ActionConfig = { + key: 'apply-css', + label: 'Apply CSS', category: 'app-configuration', requiresInput: true, requiresStyle: true, - inputPlaceholder: 'Enter CSS code to apply globally...', + inputPlaceholder: 'Enter CSS code to apply...', inputType: 'textarea', validation: (value: string) => { if (!value.trim()) { @@ -322,21 +322,21 @@ export const applyGlobalCSSAction: ActionConfig = { return; } - const globalCSSComp = preloadComp.children.globalCSS; - if (!globalCSSComp) { - message.error('Global CSS component not found'); + const cssComp = preloadComp.children.css; + if (!cssComp) { + message.error('CSS component not found'); return; } - globalCSSComp.dispatchChangeValueAction(cssCode); + cssComp.dispatchChangeValueAction(cssCode); - await globalCSSComp.run('global-css', cssCode); + await cssComp.run('css', cssCode); - message.success('Global CSS applied successfully!'); + message.success('CSS applied successfully!'); } catch (error) { - console.error('Error applying global CSS:', error); - message.error('Failed to apply global CSS. Check console for details.'); + console.error('Error applying CSS:', error); + message.error('Failed to apply CSS. Check console for details.'); } } }; From a380978f76efe16e8603b926bb4c38391f493c88 Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Thu, 10 Jul 2025 14:21:02 +0500 Subject: [PATCH 9/9] Update App meta data --- .../src/comps/comps/preLoadComp/actions/appConfiguration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts index 87a46c668..227879550 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/appConfiguration.ts @@ -534,4 +534,4 @@ export const setCustomShortcutsAction: ActionConfig = { message.error('Failed to set custom shortcut. Check console for details.'); } } -}; \ No newline at end of file +};