From 18045b9067e0a52a97bca4dbc3858624e38c227a Mon Sep 17 00:00:00 2001 From: Kamal Qureshi Date: Mon, 7 Jul 2025 15:45:16 +0500 Subject: [PATCH] Added Styling actions (Fixed Constant) Added Nesting components --- .../comps/comps/preLoadComp/actionConfigs.ts | 6 +- .../comps/preLoadComp/actionInputSection.tsx | 80 +++++++++--- .../actions/componentManagement.ts | 119 ++++++++++++++++++ .../preLoadComp/actions/componentStyling.ts | 107 +++++++++++++--- .../src/comps/comps/preLoadComp/styled.tsx | 3 + .../src/comps/comps/preLoadComp/types.ts | 3 + .../src/comps/comps/preLoadComp/utils.ts | 2 +- client/packages/lowcoder/src/comps/index.tsx | 13 ++ .../lowcoder/src/comps/uiCompRegistry.ts | 1 + 9 files changed, 297 insertions(+), 37 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts index 2eae082a16..02f0bbe736 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionConfigs.ts @@ -8,7 +8,8 @@ import { configureComponentAction, changeLayoutAction, addEventHandlerAction, - applyStyleAction + applyStyleAction, + nestComponentAction } from "./actions"; export const actionCategories: ActionCategory[] = [ @@ -20,7 +21,8 @@ export const actionCategories: ActionCategory[] = [ moveComponentAction, deleteComponentAction, resizeComponentAction, - renameComponentAction + renameComponentAction, + nestComponentAction ] }, { diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx index 148dac1674..a78ab6fb31 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actionInputSection.tsx @@ -25,7 +25,10 @@ export function ActionInputSection() { const [placeholderText, setPlaceholderText] = useState(""); const [selectedComponent, setSelectedComponent] = useState(null); const [showComponentDropdown, setShowComponentDropdown] = useState(false); + const [isNestedComponent, setIsNestedComponent] = useState(false); + const [selectedNestComponent, setSelectedNestComponent] = useState(null); const [showEditorComponentsDropdown, setShowEditorComponentsDropdown] = useState(false); + const [showStylingInput, setShowStylingInput] = useState(false); const [selectedEditorComponent, setSelectedEditorComponent] = useState(null); const [validationError, setValidationError] = useState(null); const inputRef = useRef(null); @@ -73,44 +76,55 @@ export function ActionInputSection() { setShowComponentDropdown(false); setShowEditorComponentsDropdown(false); + setShowStylingInput(false); setSelectedComponent(null); setSelectedEditorComponent(null); + setIsNestedComponent(false); + setSelectedNestComponent(null); setActionValue(""); if (action.requiresComponentSelection) { setShowComponentDropdown(true); setPlaceholderText("Select a component to add"); - } else if (action.requiresEditorComponentSelection) { + } + if (action.requiresEditorComponentSelection) { setShowEditorComponentsDropdown(true); setPlaceholderText(`Select a component to ${action.label.toLowerCase()}`); - } else if (action.requiresInput) { + } + if (action.requiresInput) { setPlaceholderText(action.inputPlaceholder || `Enter ${action.label.toLowerCase()} value`); } else { setPlaceholderText(`Execute ${action.label.toLowerCase()}`); } + if (action.requiresStyle) { + setShowStylingInput(true); + setPlaceholderText(`Select a component to style`); + } + if (action.isNested) { + setIsNestedComponent(true); + } }, []); const handleComponentSelection = useCallback((key: string) => { if (key.startsWith('comp-')) { const compName = key.replace('comp-', ''); - setSelectedComponent(compName); + isNestedComponent ? setSelectedNestComponent(compName) : setSelectedComponent(compName); setPlaceholderText(`Configure ${compName} component`); } - }, []); + }, [isNestedComponent]); const handleEditorComponentSelection = useCallback((key: string) => { setSelectedEditorComponent(key); - if (currentAction) { - setPlaceholderText(`${currentAction.label}`); - } + 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 handleInputChange = useCallback((e: React.ChangeEvent) => { const value = e.target.value; setActionValue(value); @@ -149,12 +163,18 @@ export function ActionInputSection() { return; } + if(currentAction.isNested && !selectedNestComponent) { + message.error('Please select a component to nest'); + return; + } + try { await currentAction.execute({ actionKey: selectedActionKey, actionValue, selectedComponent, selectedEditorComponent, + selectedNestComponent, editorState }); @@ -167,6 +187,8 @@ export function ActionInputSection() { setSelectedEditorComponent(null); setPlaceholderText(""); setValidationError(null); + setIsNestedComponent(false); + setSelectedNestComponent(null); } catch (error) { console.error('Error executing action:', error); @@ -177,6 +199,7 @@ export function ActionInputSection() { actionValue, selectedComponent, selectedEditorComponent, + selectedNestComponent, editorState, currentAction, validateInput @@ -235,7 +258,7 @@ export function ActionInputSection() { - {showComponentDropdown && ( + {(showComponentDropdown || isNestedComponent) && ( @@ -278,23 +307,34 @@ export function ActionInputSection() { > )} - + {shouldShowInput && ( - + showStylingInput ? ( + + ) : ( + + ) )} - + {validationError && (
{validationError} 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 f86358b164..669c77524b 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentManagement.ts @@ -108,6 +108,125 @@ export const addComponentAction: ActionConfig = { } }; +export const nestComponentAction: ActionConfig = { + key: 'nest-components', + label: 'Nest a component', + category: 'component-management', + requiresEditorComponentSelection: true, + requiresInput: false, + isNested: true, + execute: async (params: ActionExecuteParams) => { + const { selectedEditorComponent, selectedNestComponent, editorState } = params; + + if (!selectedEditorComponent || !selectedNestComponent || !editorState) { + message.error('Parent component, child component, and editor state are required'); + return; + } + + const parentComponentInfo = getEditorComponentInfo(editorState, selectedEditorComponent); + + if (!parentComponentInfo) { + message.error(`Parent component "${selectedEditorComponent}" not found`); + return; + } + + const { componentKey: parentKey, items } = parentComponentInfo; + + if (!parentKey) { + message.error(`Parent component "${selectedEditorComponent}" not found in layout`); + return; + } + + const parentItem = items[parentKey]; + if (!parentItem) { + message.error(`Parent component "${selectedEditorComponent}" not found in items`); + return; + } + + // Check if parent is a container + const parentCompType = parentItem.children.compType.getView(); + const parentManifest = uiCompRegistry[parentCompType]; + + if (!parentManifest?.isContainer) { + message.error(`Component "${selectedEditorComponent}" is not a container and cannot nest components`); + return; + } + + try { + + const nameGenerator = editorState.getNameGenerator(); + const compInfo = parseCompType(selectedNestComponent); + const compName = nameGenerator.genItemName(compInfo.compName); + const key = genRandomKey(); + + const manifest = uiCompRegistry[selectedNestComponent]; + 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: selectedNestComponent, + name: compName, + comp: defaultDataFn ? defaultDataFn(compName, nameGenerator, editorState) : undefined, + }; + + const parentContainer = parentItem.children.comp; + + const realContainer = parentContainer.realSimpleContainer(); + if (!realContainer) { + message.error(`Container "${selectedEditorComponent}" cannot accept nested components`); + return; + } + + const currentLayout = realContainer.children.layout.getView(); + const layoutInfo = manifest?.layoutInfo || defaultLayout(selectedNestComponent as UICompType); + + let itemPos = 0; + if (Object.keys(currentLayout).length > 0) { + itemPos = Math.max(...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, + }; + + realContainer.dispatch( + wrapActionExtraInfo( + multiChangeAction({ + layout: changeValueAction({ + ...currentLayout, + [key]: layoutItem, + }, true), + items: addMapChildAction(key, widgetValue), + }), + { compInfos: [{ compName: compName, compType: selectedNestComponent, type: "add" }] } + ) + ); + + editorState.setSelectedCompNames(new Set([compName]), "nestComp"); + + message.success(`Component "${manifest?.name || selectedNestComponent}" nested in "${selectedEditorComponent}" successfully!`); + } catch (error) { + console.error('Error nesting component:', error); + message.error('Failed to nest component. Please try again.'); + } + } +} + export const deleteComponentAction: ActionConfig = { key: 'delete-components', label: 'Delete a component', diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts index 2a152713ea..dbe6297a0b 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/actions/componentStyling.ts @@ -1,34 +1,113 @@ import { message } from "antd"; import { ActionConfig, ActionExecuteParams } from "../types"; +import { getEditorComponentInfo } from "../utils"; + +// Fallback constant style object to apply +// This wil be replaced by a JSON object returned by the AI model. +const FALLBACK_STYLE_OBJECT = { + fontSize: "10px", + fontWeight: "500", + color: "#333333", + backgroundColor: "#ffffff", + padding: "8px", + borderRadius: "4px", + border: "1px solid #ddd" +}; export const applyStyleAction: ActionConfig = { key: 'apply-style', label: 'Apply style to component', category: 'styling', requiresEditorComponentSelection: true, + requiresStyle: true, requiresInput: true, inputPlaceholder: 'Enter CSS styles (JSON format)', - inputType: 'json', + inputType: 'textarea', validation: (value: string) => { - if (!value.trim()) return 'Styles are required'; - try { - JSON.parse(value); - return null; - } catch { - return 'Invalid JSON format'; - } + if (!value.trim()) return 'Styles are required' + else return null; }, execute: async (params: ActionExecuteParams) => { - const { selectedEditorComponent, actionValue } = params; + const { selectedEditorComponent, actionValue, editorState } = params; + if (!selectedEditorComponent || !editorState) { + message.error('Component and editor state are required'); + return; + } + + // A fallback constant is currently used to style the component. + // This is a temporary solution and will be removed once we integrate the AI model with the component styling. try { - const styles = JSON.parse(actionValue); - console.log('Applying styles to component:', selectedEditorComponent, 'with styles:', styles); - message.info(`Styles applied to component "${selectedEditorComponent}"`); + let styleObject: Record = {}; + let usingFallback = false; + + try { + if (typeof actionValue === 'string') { + styleObject = JSON.parse(actionValue); + } else { + styleObject = actionValue; + } + } catch (e) { + styleObject = FALLBACK_STYLE_OBJECT; + usingFallback = true; + } + + const comp = editorState.getUICompByName(selectedEditorComponent); + + if (!comp) { + message.error(`Component "${selectedEditorComponent}" not found`); + return; + } + + const appliedStyles: string[] = []; + + for (const [styleKey, styleValue] of Object.entries(styleObject)) { + try { + const { children } = comp.children.comp; + const compType = comp.children.compType.getView(); + + // This method is used in LeftLayersContent.tsx to style the component. + if (!children.style) { + if (children[compType]?.children?.style?.children?.[styleKey]) { + children[compType].children.style.children[styleKey].dispatchChangeValueAction(styleValue); + appliedStyles.push(styleKey); + } else if (children[compType]?.children?.[styleKey]) { + children[compType].children[styleKey].dispatchChangeValueAction(styleValue); + appliedStyles.push(styleKey); + } else { + console.warn(`Style property ${styleKey} not found in component ${selectedEditorComponent}`); + } + } else { + if (children.style.children?.[styleKey]) { + children.style.children[styleKey].dispatchChangeValueAction(styleValue); + appliedStyles.push(styleKey); + } else if (children.style[styleKey]) { + children.style[styleKey].dispatchChangeValueAction(styleValue); + appliedStyles.push(styleKey); + } else { + console.warn(`Style property ${styleKey} not found in style object`); + } + } + } catch (error) { + console.error(`Error applying style ${styleKey}:`, error); + } + } + + if (appliedStyles.length > 0) { + editorState.setSelectedCompNames(new Set([selectedEditorComponent]), "applyStyle"); + + if (usingFallback) { + message.success(`Applied ${appliedStyles.length} fallback style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); + } else { + message.success(`Applied ${appliedStyles.length} style(s) to component "${selectedEditorComponent}": ${appliedStyles.join(', ')}`); + } + } else { + message.warning('No styles were applied. Check if the component supports styling.'); + } - // TODO: Implement actual style application logic } catch (error) { - message.error('Invalid style format'); + console.error('Error applying styles:', error); + message.error('Failed to apply styles. Please try again.'); } } }; \ 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 index 211115e996..ae13c2c3f8 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/styled.tsx +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/styled.tsx @@ -6,6 +6,9 @@ export const CustomDropdown = styled(Dropdown)` width: 14px !important; height: 14px !important; max-width: 14px !important; + overflow: hidden !important; + white-space: nowrap; + text-overflow: hidden !important; } `; diff --git a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts index 7e84ab1da2..a33f0f8f16 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/types.ts @@ -33,6 +33,8 @@ export interface ActionConfig { requiresComponentSelection?: boolean; requiresEditorComponentSelection?: boolean; requiresInput?: boolean; + requiresStyle?: boolean; + isNested?: boolean; inputPlaceholder?: string; inputType?: 'text' | 'number' | 'textarea' | 'json'; validation?: (value: string) => string | null; @@ -44,6 +46,7 @@ export interface ActionExecuteParams { actionValue: string; selectedComponent: string | null; selectedEditorComponent: string | null; + selectedNestComponent: 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 3f3ae1b739..b30f02fb2b 100644 --- a/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts +++ b/client/packages/lowcoder/src/comps/comps/preLoadComp/utils.ts @@ -22,7 +22,7 @@ export function generateComponentActionItems(categories: Record { - if (components.length > 0) { + if (components.length) { componentItems.push({ label: uiCompCategoryNames[categoryKey as UICompCategory], key: `category-${categoryKey}`, diff --git a/client/packages/lowcoder/src/comps/index.tsx b/client/packages/lowcoder/src/comps/index.tsx index 2395f4f290..81a9c634c1 100644 --- a/client/packages/lowcoder/src/comps/index.tsx +++ b/client/packages/lowcoder/src/comps/index.tsx @@ -544,6 +544,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: ResponsiveLayoutCompIcon, keywords: trans("uiComp.responsiveLayoutCompKeywords"), + isContainer: true, comp: ResponsiveLayoutComp, withoutLoading: true, layoutInfo: { @@ -559,6 +560,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: PageLayoutCompIcon, keywords: trans("uiComp.pageLayoutCompKeywords"), + isContainer: true, comp: PageLayoutComp, withoutLoading: true, layoutInfo: { @@ -576,6 +578,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: ColumnLayoutCompIcon, keywords: trans("uiComp.responsiveLayoutCompKeywords"), + isContainer: true, comp: ColumnLayoutComp, withoutLoading: true, layoutInfo: { @@ -591,6 +594,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: SplitLayoutCompIcon, keywords: trans("uiComp.splitLayoutCompKeywords"), + isContainer: true, comp: SplitLayoutComp, withoutLoading: true, layoutInfo: { @@ -606,6 +610,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: FloatingTextCompIcon, keywords: trans("uiComp.floatTextContainerCompKeywords"), + isContainer: true, comp: FloatTextContainerComp, withoutLoading: true, layoutInfo: { @@ -636,6 +641,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: TabbedContainerCompIcon, keywords: trans("uiComp.tabbedContainerCompKeywords"), + isContainer: true, comp: TabbedContainerComp, withoutLoading: true, layoutInfo: { @@ -652,6 +658,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: CollapsibleContainerCompIcon, keywords: trans("uiComp.collapsibleContainerCompKeywords"), + isContainer: true, comp: ContainerComp, withoutLoading: true, layoutInfo: { @@ -669,6 +676,7 @@ export var uiCompMap: Registry = { categories: ["layout"], icon: ContainerCompIcon, keywords: trans("uiComp.containerCompKeywords"), + isContainer: true, comp: ContainerComp, withoutLoading: true, layoutInfo: { @@ -686,6 +694,7 @@ export var uiCompMap: Registry = { description: trans("uiComp.listViewCompDesc"), categories: ["layout"], keywords: trans("uiComp.listViewCompKeywords"), + isContainer: true, comp: ListViewComp, layoutInfo: { w: 12, @@ -701,6 +710,7 @@ export var uiCompMap: Registry = { description: trans("uiComp.gridCompDesc"), categories: ["layout"], keywords: trans("uiComp.gridCompKeywords"), + isContainer: true, comp: GridComp, layoutInfo: { w: 12, @@ -718,6 +728,7 @@ export var uiCompMap: Registry = { keywords: trans("uiComp.modalCompKeywords"), comp: ModalComp, withoutLoading: true, + isContainer: true, }, drawer: { name: trans("uiComp.drawerCompName"), @@ -728,6 +739,7 @@ export var uiCompMap: Registry = { keywords: trans("uiComp.drawerCompKeywords"), comp: DrawerComp, withoutLoading: true, + isContainer: true, }, divider: { name: trans("uiComp.dividerCompName"), @@ -941,6 +953,7 @@ export var uiCompMap: Registry = { categories: ["forms"], icon: FormCompIcon, keywords: trans("uiComp.formCompKeywords"), + isContainer: true, comp: FormComp, withoutLoading: true, layoutInfo: { diff --git a/client/packages/lowcoder/src/comps/uiCompRegistry.ts b/client/packages/lowcoder/src/comps/uiCompRegistry.ts index 4c320de479..c260bad386 100644 --- a/client/packages/lowcoder/src/comps/uiCompRegistry.ts +++ b/client/packages/lowcoder/src/comps/uiCompRegistry.ts @@ -51,6 +51,7 @@ export interface UICompManifest { lazyLoad?: boolean; compName?: string; compPath?: string; + isContainer?: boolean; defaultDataFn?: CompDefaultDataFunction; defaultDataFnName?: string; defaultDataFnPath?: string;