diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx index bb4d4fc69b..4c558f3cc2 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle.jsx @@ -3,9 +3,13 @@ import { shallow } from 'zustand/shallow'; import './configHandle.scss'; import useStore from '@/AppBuilder/_stores/store'; import { findHighestLevelofSelection } from '../Grid/gridUtils'; +import SolidIcon from '@/_ui/Icon/solidIcons/index'; + +const CONFIG_HANDLE_HEIGHT = 20; +const BUFFER_HEIGHT = 1; + export const ConfigHandle = ({ id, - position, widgetTop, widgetHeight, setSelectedComponentAsModal = () => null, //! Only Modal widget passes this uses props down. All other widgets use selecto lib @@ -27,6 +31,7 @@ export const ConfigHandle = ({ (state) => componentType === 'Tabs' && state.getExposedValueOfComponent(id)?.currentTab, shallow ); + const position = widgetTop < 15 ? 'bottom' : 'top'; const setComponentToInspect = useStore((state) => state.setComponentToInspect); const isModal = componentType === 'Modal' || componentType === 'ModalV2'; @@ -36,9 +41,7 @@ export const ConfigHandle = ({ // If one component is hovered and one is selected, show the handle for the hovered component return ( isWidgetHovered || - (showHandle && - (!isMultipleComponentsSelected || (isModal && isModalOpen)) && - !anyComponentHovered) + (showHandle && (!isMultipleComponentsSelected || (isModal && isModalOpen)) && !anyComponentHovered) ); }, shallow); let height = visibility === false ? 10 : widgetHeight; @@ -48,7 +51,12 @@ export const ConfigHandle = ({ className={`config-handle ${customClassName}`} widget-id={id} style={{ - top: position === 'top' ? '-20px' : widgetTop + height - (widgetTop < 10 ? 15 : 10), + top: + componentType === 'Modal' && isModalOpen + ? '0px' + : position === 'top' + ? '-20px' + : `${height - (CONFIG_HANDLE_HEIGHT + BUFFER_HEIGHT)}px`, visibility: _showHandle ? 'visible' : 'hidden', left: '-1px', }} @@ -63,7 +71,10 @@ export const ConfigHandle = ({ > @@ -77,17 +88,30 @@ export const ConfigHandle = ({ data-cy={`${componentName?.toLowerCase()}-config-handle`} className="text-truncate" > - + {/* Settings Icon */} + + + {componentName} + {/* Divider */} +
+ {/* Delete Button */} {!isMultipleComponentsSelected && !shouldFreeze && ( -
+
- { deleteComponents([id]); }} data-cy={`${componentName.toLowerCase()}-delete-button`} - className="delete-icon" - /> + > + +
)} diff --git a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss index e7322959e5..5cb1b94268 100644 --- a/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss +++ b/frontend/src/AppBuilder/AppCanvas/ConfigHandle/configHandle.scss @@ -31,22 +31,7 @@ .badge { font-size: 9px; border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - - .delete-part { - margin-left: 10px; - float: right; - } - - .delete-part::before { - height: 12px; - display: inline-block; - width: 2px; - background-color: rgba(255, 255, 255, 0.8); - opacity: 0.5; - content: ""; - vertical-align: middle; - } + border-bottom-right-radius: 0 } } diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index fc163939f6..e622e1a2cd 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -6,7 +6,15 @@ import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { useDrop } from 'react-dnd'; import { addChildrenWidgetsToParent, addNewWidgetToTheEditor, computeViewerBackgroundColor } from './appCanvasUtils'; -import { CANVAS_WIDTHS, NO_OF_GRIDS, WIDGETS_WITH_DEFAULT_CHILDREN } from './appCanvasConstants'; +import { + CANVAS_WIDTHS, + NO_OF_GRIDS, + WIDGETS_WITH_DEFAULT_CHILDREN, + GRID_HEIGHT, + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, + BOX_PADDING, +} from './appCanvasConstants'; import { useGridStore } from '@/_stores/gridStore'; import NoComponentCanvasContainer from './NoComponentCanvasContainer'; import { RIGHT_SIDE_BAR_TAB } from '../RightSideBar/rightSidebarConstants'; @@ -35,10 +43,10 @@ export const Container = React.memo( canvasMaxWidth, isViewerSidebarPinned, pageSidebarStyle, + componentType, }) => { const realCanvasRef = useRef(null); const components = useStore((state) => state.getContainerChildrenMapping(id), shallow); - const componentType = useStore((state) => state.getComponentTypeFromId(id), shallow); const addComponentToCurrentPage = useStore((state) => state.addComponentToCurrentPage, shallow); const setActiveRightSideBarTab = useStore((state) => state.setActiveRightSideBarTab, shallow); const setLastCanvasClickPosition = useStore((state) => state.setLastCanvasClickPosition, shallow); @@ -95,12 +103,16 @@ export const Container = React.memo( if (canvasWidth !== undefined) { if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2; if (id === 'canvas') return canvasWidth; - return canvasWidth - 2; + if (componentType === 'Container' || componentType === 'Form') { + return ( + canvasWidth - (2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING) + ); + } + return canvasWidth - 2; // Need to update this 2 to correct value for other subcontainers } return realCanvasRef?.current?.offsetWidth; } const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; - useEffect(() => { useGridStore.getState().actions.setSubContainerWidths(id, getContainerCanvasWidth() / NO_OF_GRIDS); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -143,7 +155,7 @@ export const Container = React.memo( }} style={{ height: id === 'canvas' ? `${canvasHeight}` : '100%', - backgroundSize: `${gridWidth}px ${10}px`, + backgroundSize: `${gridWidth}px ${GRID_HEIGHT}px`, backgroundColor: currentMode === 'view' ? computeViewerBackgroundColor(darkMode, canvasBgColor) diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css index 5abcc430d4..e1c6bb3baa 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.css @@ -1,17 +1,6 @@ .target, .nested-target { position: absolute; - /* width: 100px; - height: 100px; */ - /* top: 150px; - left: 100px; */ - /* line-height: 100px; */ - /* text-align: center; */ - /* background: #ee8; */ - /* color: #333; */ - /* font-weight: bold; */ box-sizing: border-box; - /* transition: transform 0.1s; */ - /* z-index: 3001; */ } .target.hovered{ @@ -76,43 +65,6 @@ background: #8DA4EF !important; } - - -/* Hides all the control lines*/ -/* .moveable-line { - color: transparent !important; - --moveable-color: transparent !important; -} - -.moveable-control { - visibility: hidden; -} - -.target { - outline: 1px solid #4af; -} */ - - -.main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { - outline: 1px solid #4af; - z-index: 4 !important; -} - -.main-editor-canvas .widget-target:has(.nested-target:hover):hover { - outline: 0px solid #4af; -} - - -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { - outline: 1px solid #4af; - z-index: 4 !important; -} - -.active-target, .resizing-target { - outline: 1px solid #4af !important; - /* z-index: 1000000 !important; */ -} - .moveable-control-box:not([data-able-groupable]) .moveable-control-box:not(:hover) { opacity: 0; } @@ -141,10 +93,6 @@ height: 0px !important; } -.resizing-target * { - opacity: 0; -} - .moveable-control { width: 8px !important; @@ -210,4 +158,19 @@ .moveable-guideline-group { z-index: 9999; -} \ No newline at end of file +} + +.dragging-component-canvas { + outline: 1px solid var(--border-accent-strong) !important; + outline-offset: 0px; /* Creates space between element and outline */ + z-index: 999 !important; +} + +.non-dragging-component { + outline: 1px dotted var(--border-accent-weak) !important; + outline-offset: 0px; /* Creates space between element and outline */ + z-index: 999 !important; +} + + + diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 6821f43ce2..44f148a000 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -19,6 +19,9 @@ import { adjustWidth, hideGridLines, showGridLines, + handleActivateTargets, + handleDeactivateTargets, + handleActivateNonDraggingComponents, } from './gridUtils'; import { useAppVersionStore } from '@/_stores/appVersionStore'; import { resolveWidgetFieldValue } from '@/_helpers/utils'; @@ -56,7 +59,7 @@ export default function Grid({ gridWidth, currentLayout }) { const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); - const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow); + const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); const [dragParentId, setDragParentId] = useState(null); const [elementGuidelines, setElementGuidelines] = useState([]); @@ -580,7 +583,7 @@ export default function Grid({ gridWidth, currentLayout }) { } else { document.getElementById('real-canvas').classList.add('show-grid'); } - + handleActivateTargets(currentWidget.component?.parent); const currentWidth = currentWidget.width * _gridWidth; const diffWidth = e.width - currentWidth; const diffHeight = e.height - currentWidget.height; @@ -632,6 +635,7 @@ export default function Grid({ gridWidth, currentLayout }) { if (!isComponentVisible(e.target.id)) { return false; } + handleActivateNonDraggingComponents(); useGridStore.getState().actions.setResizingComponentId(e.target.id); e.setMin([gridWidth, GRID_HEIGHT]); }} @@ -641,8 +645,7 @@ export default function Grid({ gridWidth, currentLayout }) { const currentWidget = boxList.find(({ id }) => { return id === e.target.id; }); - document.getElementById('real-canvas')?.classList.remove('show-grid'); - document.getElementById('canvas-' + currentWidget.component?.parent)?.classList.remove('show-grid'); + hideGridLines(); let _gridWidth = useGridStore.getState().subContainerWidths[currentWidget.component?.parent] || gridWidth; let width = Math.round(e?.lastEvent?.width / _gridWidth) * _gridWidth; const height = Math.round(e?.lastEvent?.height / GRID_HEIGHT) * GRID_HEIGHT; @@ -696,17 +699,19 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('ResizeEnd error ->', error); } + handleDeactivateTargets(); setDragParentId(null); toggleCanvasUpdater(); }} onResizeGroupStart={({ events }) => { showGridLines(); + handleActivateNonDraggingComponents(); }} onResizeGroup={({ events }) => { const parentElm = events[0].target.closest('.real-canvas'); const parentWidth = parentElm?.clientWidth; const parentHeight = parentElm?.clientHeight; - + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); const { posRight, posLeft, posTop, posBottom } = getPositionForGroupDrag(events, parentWidth, parentHeight); events.forEach((ev) => { ev.target.style.width = `${ev.width}px`; @@ -775,6 +780,7 @@ export default function Grid({ gridWidth, currentLayout }) { } catch (error) { console.error('Error resizing group', error); } + handleDeactivateTargets(); toggleCanvasUpdater(); }} checkInput @@ -785,6 +791,7 @@ export default function Grid({ gridWidth, currentLayout }) { } newDragParentId.current = boxList.find((box) => box.id === e.target.id)?.parent; e?.moveable?.controlBox?.removeAttribute('data-off-screen'); + const box = boxList.find((box) => box.id === e.target.id); // Prevent drag if shift is pressed for SUBCONTAINER_WIDGETS if (SUBCONTAINER_WIDGETS.includes(box?.component?.component) && e.inputEvent.shiftKey) { @@ -818,7 +825,6 @@ export default function Grid({ gridWidth, currentLayout }) { container.contains(e.inputEvent.target) ); } - if (['RangeSlider', 'BoundedBox'].includes(box?.component?.component) || isDragOnInnerElement) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); const isHandle = targetElems.find((ele) => ele.classList.contains('handle-content')); @@ -826,11 +832,13 @@ export default function Grid({ gridWidth, currentLayout }) { return false; } } + handleActivateNonDraggingComponents(); }} onDragEnd={(e) => { + handleDeactivateTargets(); try { if (isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(null); + useStore.getState().setDraggingComponentId(null); isDraggingRef.current = false; } prevDragParentId.current = null; @@ -884,7 +892,7 @@ export default function Grid({ gridWidth, currentLayout }) { onDrag={(e) => { // Since onDrag is called multiple times when dragging, hence we are using isDraggingRef to prevent setting state again and again if (!isDraggingRef.current) { - useGridStore.getState().actions.setDraggingComponentId(e.target.id); + useStore.getState().setDraggingComponentId(e.target.id); showGridLines(); isDraggingRef.current = true; } @@ -962,6 +970,7 @@ export default function Grid({ gridWidth, currentLayout }) { setDragParentId(newParentId === 'canvas' ? null : newParentId); newDragParentId.current = newParentId === 'canvas' ? null : newParentId; prevDragParentId.current = newParentId; + handleActivateTargets(newParentId); } } // Postion ghost element exactly as same at dragged element @@ -988,14 +997,17 @@ export default function Grid({ gridWidth, currentLayout }) { ev.target.style.transform = `translate(${left}px, ${top}px)`; }); + handleActivateTargets(parentElm?.id?.replace('canvas-', '')); updateNewPosition(events); }} onDragGroupStart={({ events }) => { showGridLines(); setIsGroupDragging(true); + handleActivateNonDraggingComponents(); }} onDragGroupEnd={(e) => { handleDragGroupEnd(e); + handleDeactivateTargets(); toggleCanvasUpdater(); }} onClickGroup={(e) => { diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index 2889fc06db..da179bc11d 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -1,7 +1,7 @@ import { useGridStore } from '@/_stores/gridStore'; import { isEmpty } from 'lodash'; import useStore from '@/AppBuilder/_stores/store'; - +import { getTabId, getSubContainerIdWithSlots } from '../appCanvasUtils'; export function correctBounds(layout, bounds) { layout = scaleLayouts(layout); const collidesWith = []; @@ -414,3 +414,77 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.remove('show-grid'); document.getElementById('real-canvas')?.classList.add('hide-grid'); } + +// Track previously active elements for efficient cleanup +let previousActiveWidgets = null; +let previousActiveCanvas = null; + +export const handleActivateNonDraggingComponents = () => { + // Only add non-dragging class to visible components in viewport + document.querySelectorAll('.moveable-box:not(.active-target)').forEach((component) => { + // Check if element is visible in viewport + const rect = component.getBoundingClientRect(); + const isVisible = + rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; + + if (isVisible) { + component.classList.add('non-dragging-component'); + } + }); +}; + +export const handleActivateTargets = (parentId) => { + const WIDGETS_WITH_CANVAS_OUTLINE = ['Container', 'Modal', 'Form', 'Listview', 'Kanban']; + + const newParentType = document.getElementById('canvas-' + parentId)?.getAttribute('component-type'); + let _parentId = parentId; + if (newParentType === 'Tabs') { + _parentId = getTabId(parentId); + } else if (WIDGETS_WITH_CANVAS_OUTLINE.includes(newParentType)) { + _parentId = getSubContainerIdWithSlots(parentId); + } + + // Clean up previous active elements + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + const parentComponent = document.getElementById(_parentId); + if (!parentComponent) return; + + if (WIDGETS_WITH_CANVAS_OUTLINE?.includes(newParentType)) { + // If it's multiple canvas in single widget, highlight the specific canvas + const canvasElm = document.getElementById('canvas-' + parentId); + if (canvasElm) { + canvasElm.classList.add('dragging-component-canvas'); + previousActiveCanvas = canvasElm; + } + } else { + // Otherwise highlight the component box + parentComponent.classList.remove('non-dragging-component'); + parentComponent.classList.add('dragging-component-canvas'); + previousActiveWidgets = parentComponent; + } +}; + +export const handleDeactivateTargets = () => { + if (previousActiveWidgets) { + previousActiveWidgets.classList.remove('dragging-component-canvas'); + previousActiveWidgets = null; + } + + if (previousActiveCanvas) { + previousActiveCanvas.classList.remove('dragging-component-canvas'); + previousActiveCanvas = null; + } + + document.querySelectorAll('.non-dragging-component').forEach((component) => { + component.classList.remove('non-dragging-component'); + }); +}; diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 427cced97b..b26586dc08 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -6,7 +6,7 @@ import { OverlayTrigger } from 'react-bootstrap'; import { renderTooltip } from '@/_helpers/appUtils'; import { useTranslation } from 'react-i18next'; import ErrorBoundary from '@/_ui/ErrorBoundary'; - +import { BOX_PADDING } from './appCanvasConstants'; const shouldAddBoxShadowAndVisibility = [ 'Table', 'TextInput', @@ -164,7 +164,7 @@ const RenderWidget = ({
state.getComponentDefinition(id)?.layouts?.[currentLayout], shallow); const isWidgetActive = useStore((state) => state.selectedComponents.find((sc) => sc === id) && !readOnly, shallow); - const isDragging = useGridStore((state) => state.draggingComponentId === id); + const isDragging = useStore((state) => state.draggingComponentId === id); const isResizing = useGridStore((state) => state.resizingComponentId === id); const componentType = useStore((state) => state.getComponentDefinition(id)?.component?.component, shallow); const setHoveredComponentForGrid = useStore((state) => state.setHoveredComponentForGrid, shallow); @@ -52,7 +52,9 @@ const WidgetWrapper = memo( height: visibility === false ? '10px' : `${height}px`, transform: `translate(${layoutData.left * gridWidth}px, ${layoutData.top}px)`, WebkitFontSmoothing: 'antialiased', + border: visibility === false ? `1px solid var(--border-default)` : 'none', }; + if (!componentType) return null; return ( <> @@ -67,8 +69,8 @@ const WidgetWrapper = memo( data-id={`${id}`} id={id} widgetid={id} + component-type={componentType} style={{ - // transform: `translate(332px, -134px)`, // zIndex: mode === 'view' && widget.component.component == 'Datepicker' ? 2 : null, ...styles, }} @@ -84,7 +86,6 @@ const WidgetWrapper = memo( {mode == 'edit' && ( { allComponents[parentId]?.component?.component === 'Calendar' || allComponents[parentId]?.component?.component === 'Kanban' || allComponents[parentId]?.component?.component === 'Container' || + allComponents[parentId]?.component?.component === 'Form' || allComponents[parentId]?.component?.component === 'ModalV2'; if (componentParentId && isParentTabORCalendar) { @@ -327,6 +328,7 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar' || parentComponent.component.component === 'Container' || + parentComponent.component.component === 'Form' || parentComponent.component.component === 'ModalV2' ); } @@ -665,11 +667,14 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => { return canvasBgColor; }; -export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => { +export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => { const { tab } = child; if (parentComponent === 'Tabs') return `${parentId}-${tab}`; - else if (parentComponent === 'Container' || parentComponent === 'ModalV2') { + else if ( + slotName && + (parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2') + ) { return `${parentId}-${slotName}`; } return parentId; @@ -685,3 +690,19 @@ export const getParentWidgetFromId = (parentType, parentId) => { } return parentType; }; + +export const getTabId = (parentId) => { + return parentId.split('-').slice(0, -1).join('-'); +}; + +export const getSubContainerIdWithSlots = (parentId) => { + let cleanParentId = parentId; + if (parentId) { + if (parentId.includes('header')) { + cleanParentId = parentId.replace('-header', ''); + } else if (parentId.includes('footer')) { + cleanParentId = parentId.replace('-footer', ''); + } + } + return cleanParentId; +}; diff --git a/frontend/src/AppBuilder/AppCanvas/selecto.scss b/frontend/src/AppBuilder/AppCanvas/selecto.scss index 9ca8a37f41..5602b35d5a 100644 --- a/frontend/src/AppBuilder/AppCanvas/selecto.scss +++ b/frontend/src/AppBuilder/AppCanvas/selecto.scss @@ -3,15 +3,18 @@ } .main-editor-canvas .widget-target:not(:has(.widget-target:hover)):hover { - z-index: 4 !important; -} - -.main-editor-canvas .widget-target:has(.nested-target:hover):hover { - outline: 0px solid #4af; -} - -.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { outline: 1px solid #4af; z-index: 4 !important; } +.main-editor-canvas .nested-target:not(:has(.nested-target:hover)):hover { + // outline: 1px solid #4af; + z-index: 4 !important; +} + +// .main-editor-canvas .widget-target:hover { +// outline: 1px solid #4af; +// } + + + diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index d4676ad4b6..b39924854e 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -19,16 +19,34 @@ export const Form = ({ allComponents, pages, }) => { - const properties = Object.keys(componentMeta.properties); + const tempComponentMeta = deepClone(componentMeta); + + let properties = []; + let additionalActions = []; + let dataProperties = []; + const events = Object.keys(componentMeta.events); const validations = Object.keys(componentMeta.validation || {}); - const tempComponentMeta = deepClone(componentMeta); + + for (const [key] of Object.entries(componentMeta?.properties)) { + if (componentMeta?.properties[key]?.section === 'additionalActions') { + additionalActions.push(key); + } else if (componentMeta?.properties[key]?.accordian === 'Data') { + dataProperties.push(key); + } else { + properties.push(key); + } + } const { id } = component; const newOptions = [{ name: 'None', value: 'none' }]; - Object.entries(allComponents).forEach(([componentId, component]) => { - if (component.component.parent === id && component?.component?.component === 'Button') { - newOptions.push({ name: component.component.name, value: componentId }); + Object.entries(allComponents).forEach(([componentId, _component]) => { + const validParent = + _component.component.parent === id || + _component.component.parent === `${id}-footer` || + _component.component.parent === `${id}-header`; + if (validParent && _component?.component?.component === 'Button') { + newOptions.push({ name: _component.component.name, value: componentId }); } }); @@ -48,7 +66,8 @@ export const Form = ({ allComponents, validations, darkMode, - pages + pages, + additionalActions ); return ; @@ -68,7 +87,8 @@ export const baseComponentProperties = ( allComponents, validations, darkMode, - pages + pages, + additionalActions ) => { let items = []; if (properties.length > 0) { @@ -90,6 +110,24 @@ export const baseComponentProperties = ( }); } + items.push({ + title: 'Additional actions', + isOpen: true, + children: additionalActions?.map((property) => + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + property, + 'properties', + currentState, + allComponents, + darkMode + ) + ), + }); + if (events.length > 0) { items.push({ title: `${i18next.t('widget.common.events', 'Events')}`, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index 0e9f5f4ce3..2d8eb7f0a8 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -4,9 +4,40 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 330, + height: 480, }, defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 10, + left: 1, + height: 40, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Form title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 12, + left: 32, + height: 36, + }, + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, { componentName: 'Text', layout: { @@ -225,6 +256,7 @@ export const formConfig = { loadingState: { type: 'toggle', displayName: 'Loading state', + section: 'additionalActions', validation: { schema: { type: 'boolean' }, defaultValue: false, @@ -242,12 +274,64 @@ export const formConfig = { value: true, }, }, + showHeader: { type: 'toggle', displayName: 'Header' }, + showFooter: { type: 'toggle', displayName: 'Footer' }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, }, events: { onSubmit: { displayName: 'On submit' }, onInvalid: { displayName: 'On invalid' }, }, styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + headerHeight: { + type: 'code', + displayName: 'Header height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, + footerHeight: { + type: 'code', + displayName: 'Footer height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, backgroundColor: { type: 'color', displayName: 'Background color', @@ -274,26 +358,13 @@ export const formConfig = { defaultValue: '#fff', }, }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, - }, - disabledState: { - type: 'toggle', - displayName: 'Disable', - validation: { - schema: { type: 'boolean' }, - defaultValue: false, - }, - }, }, exposedVariables: { data: {}, isValid: true, + isVisible: true, + isDisabled: false, + isLoading: false, }, actions: [ { @@ -304,6 +375,21 @@ export const formConfig = { handle: 'resetForm', displayName: 'Reset Form', }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisable', + displayName: 'Set Disable', + params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set Loading', + params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }], + }, ], definition: { others: { @@ -317,15 +403,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, - buttonToSubmit: { value: '{{"none"}}' }, + showHeader: { value: '{{false}}' }, + showFooter: { value: '{{false}}' }, + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - visibility: { value: '{{true}}' }, - disabledState: { value: '{{false}}' }, + headerHeight: { value: '60px' }, + footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/AppBuilder/Widgets/Container.jsx b/frontend/src/AppBuilder/Widgets/Container.jsx deleted file mode 100644 index 1334098423..0000000000 --- a/frontend/src/AppBuilder/Widgets/Container.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useMemo } from 'react'; -import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container'; -import Spinner from '@/_ui/Spinner'; -import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; - -export const Container = ({ - id, - properties, - styles, - darkMode, - height, - width, - setExposedVariables, - setExposedVariable, -}) => { - const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; - - const { isDisabled, isVisible, isLoading } = useExposeState( - properties.loadingState, - properties.visibility, - properties.disabledState, - setExposedVariables, - setExposedVariable - ); - - const contentBgColor = useMemo(() => { - return { - backgroundColor: - ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor, - }; - }, [styles.backgroundColor, darkMode]); - - const headerBgColor = useMemo(() => { - return { - backgroundColor: - ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode - ? '#232E3C' - : styles.headerBackgroundColor, - }; - }, [styles.headerBackgroundColor, darkMode]); - - const computedStyles = { - backgroundColor: contentBgColor.backgroundColor, - borderRadius: borderRadius ? parseFloat(borderRadius) : 0, - border: `1px solid ${borderColor}`, - height, - display: isVisible ? 'flex' : 'none', - flexDirection: 'column', - position: 'relative', - boxShadow, - }; - - const computedHeaderStyles = { - ...headerBgColor, - height: `${headerHeight}px`, - flexShrink: 0, - flexGrow: 0, - borderBottom: `1px solid var(--border-weak)`, - }; - - const computedContentStyles = { - ...contentBgColor, - flex: 1, - overflow: 'auto', - }; - - return ( -
- {properties.showHeader && ( - - )} - {isLoading ? ( -
- -
- ) : ( - - )} -
- ); -}; diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx new file mode 100644 index 0000000000..4978427370 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -0,0 +1,119 @@ +import React, { useMemo } from 'react'; +import { Container as ContainerComponent } from '@/AppBuilder/AppCanvas/Container'; +import Spinner from '@/_ui/Spinner'; +import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; +import { shallow } from 'zustand/shallow'; +import { + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, +} from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import useStore from '@/AppBuilder/_stores/store'; +import './container.scss'; + +export const Container = ({ + id, + properties, + styles, + darkMode, + height, + width, + setExposedVariables, + setExposedVariable, +}) => { + const { isDisabled, isVisible, isLoading } = useExposeState( + properties.loadingState, + properties.visibility, + properties.disabledState, + setExposedVariables, + setExposedVariable + ); + + const isWidgetInContainerDragging = useStore( + (state) => state.containerChildrenMapping?.[id]?.includes(state?.draggingComponentId), + shallow + ); + + const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; + const contentBgColor = useMemo(() => { + return { + backgroundColor: + ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor, + }; + }, [styles.backgroundColor, darkMode]); + + const headerBgColor = useMemo(() => { + return { + backgroundColor: + ['#fff', '#ffffffff'].includes(styles.headerBackgroundColor) && darkMode + ? '#232E3C' + : styles.headerBackgroundColor, + }; + }, [styles.headerBackgroundColor, darkMode]); + + const computedStyles = { + backgroundColor: contentBgColor.backgroundColor, + borderRadius: borderRadius ? parseFloat(borderRadius) : 0, + border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`, + height, + display: isVisible ? 'flex' : 'none', + flexDirection: 'column', + position: 'relative', + boxShadow, + }; + + const containerHeaderStyles = { + flexShrink: 0, + padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`, + ...headerBgColor, + }; + + const containerContentStyles = { + overflow: 'hidden auto', + display: 'flex', + height: '100%', + padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, + }; + + return ( +
+ {isLoading ? ( + + ) : ( + <> + {properties.showHeader && ( +
+ +
+ )} +
+ +
+ + )} +
+ ); +}; diff --git a/frontend/src/AppBuilder/Widgets/Container/container.scss b/frontend/src/AppBuilder/Widgets/Container/container.scss new file mode 100644 index 0000000000..323f5e8c9a --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Container/container.scss @@ -0,0 +1,13 @@ +.wj-container-header { + position: relative; + &::after { + content: ''; + position: absolute; + bottom: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } + } + \ No newline at end of file diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 674d707f03..afeb4cf844 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -1,17 +1,25 @@ -import React, { useRef, useState, useEffect, useMemo } from 'react'; +import React, { useRef, useState, useEffect } from 'react'; import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; // eslint-disable-next-line import/no-unresolved -import { diff } from 'deep-object-diff'; import _, { debounce, omit } from 'lodash'; -import { Box } from '@/Editor/Box'; import { generateUIComponents } from './FormUtils'; import { useMounted } from '@/_hooks/use-mount'; import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils'; -import { useAppInfo } from '@/_stores/appDataStore'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; import RenderSchema from './RenderSchema'; import useStore from '@/AppBuilder/_stores/store'; +import { useExposeState } from '@/AppBuilder/_hooks/useExposeVariables'; import { shallow } from 'zustand/shallow'; +import { + CONTAINER_FORM_CANVAS_PADDING, + SUBCONTAINER_CANVAS_BORDER_WIDTH, +} from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import './form.scss'; + +const getCanvasHeight = (height) => { + const parsedHeight = height.includes('px') ? parseInt(height, 10) : height; + return Math.ceil(parsedHeight); +}; export const Form = function Form(props) { const { @@ -29,27 +37,71 @@ export const Form = function Form(props) { dataCy, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); - const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles; - const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties; + const { + borderRadius, + borderColor, + boxShadow, + headerHeight, + footerHeight, + footerBackgroundColor, + headerBackgroundColor, + } = styles; + const { + buttonToSubmit, + loadingState, + advanced, + JSONSchema, + showHeader = false, + showFooter = false, + visibility, + disabledState, + } = properties; + const { isDisabled, isVisible, isLoading } = useExposeState( + properties.loadingState, + properties.visibility, + properties.disabledState, + setExposedVariables, + setExposedVariable + ); const backgroundColor = ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor; const computedStyles = { backgroundColor, borderRadius: borderRadius ? parseFloat(borderRadius) : 0, - border: `1px solid ${borderColor}`, + border: `${SUBCONTAINER_CANVAS_BORDER_WIDTH}px solid ${borderColor}`, height, - display: visibility ? 'flex' : 'none', + display: isVisible ? 'flex' : 'none', position: 'relative', - overflow: 'hidden auto', boxShadow, + flexDirection: 'column', }; - const childIdNameMap = useMemo(() => { - return Object.keys(childComponents).reduce((acc, id) => { - const component = childComponents[id]?.component?.component; - return { ...acc, [id]: component?.name }; - }, {}); - }, [childComponents]); + const formHeader = { + flexShrink: 0, + paddingBottom: '3px', + paddingTop: '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, + }; + + const formContent = { + overflow: 'hidden auto', + display: 'flex', + height: '100%', + paddingTop: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingBottom: showFooter ? '3px' : '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + }; + + const formFooter = { + flexShrink: 0, + padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, + }; const parentRef = useRef(null); const childDataRef = useRef({}); @@ -58,6 +110,8 @@ export const Form = function Form(props) { const [isValid, setValidation] = useState(true); const [uiComponents, setUIComponents] = useState([]); const mounted = useMounted(); + const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10; + const canvasFooterHeight = getCanvasHeight(footerHeight) / 10; useEffect(() => { const exposedVariables = { @@ -155,7 +209,7 @@ export const Form = function Form(props) { }; setExposedVariables(exposedVariables); setValidation(childValidation); - }, [childrenData, advanced, JSON.stringify(childIdNameMap)]); + }, [childrenData, advanced]); useEffect(() => { document.addEventListener('submitForm', handleFormSubmission); @@ -245,105 +299,113 @@ export const Form = function Form(props) { if (e.target.className === 'real-canvas') onComponentClick(id, component); }} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked > - {loadingState ? ( -
-
-
-
-
- ) : ( -
- {!advanced && ( -
- - {/* - */} -
+ {showHeader && ( +
+ + {isDisabled && ( +
{}} + onDrop={(e) => e.stopPropagation()} + /> )} - {advanced && - uiComponents?.map((item, index) => { - return ( -
-
- +
+ )} +
+ {isLoading ? ( +
+
+
+ ) : ( +
+ {!advanced && ( +
+ +
+ )} + {advanced && + uiComponents?.map((item, index) => { + return ( +
+
+ +
- {/* */} -
- ); - })} -
+ ); + })} + + )} +
+ {showFooter && ( +
+ + {isDisabled && ( + )} ); diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss new file mode 100644 index 0000000000..530e837eb2 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -0,0 +1,40 @@ +.wj-form-header { + position: relative; + &::after { + content: ""; + position: absolute; + bottom: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } +} + +.wj-form-footer { + position: relative; + &::after { + content: ""; + position: absolute; + top: 0; + left: -7px; + right: -7px; + height: 1px; + background-color: var(--border-weak); + } +} + +.tj-form-disabled-overlay { + /* TODO: Make slot overlays common */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.8); + z-index: 1; + margin: 0; + + box-sizing: content-box; + padding: 4px 0; +} diff --git a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx index 2621708532..8aa7b578ac 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/KanbanBoard.jsx @@ -412,6 +412,7 @@ export function KanbanBoard({ widgetHeight, kanbanProps, parentRef, id }) { width: `${(Number(cardWidth) || 300) + 48}px`, }} kanbanProps={kanbanProps} + componentType="Kanban" > {items[columnId] && ( diff --git a/frontend/src/AppBuilder/Widgets/Listview.jsx b/frontend/src/AppBuilder/Widgets/Listview.jsx index 5ec5abbb17..285f9e5bf9 100644 --- a/frontend/src/AppBuilder/Widgets/Listview.jsx +++ b/frontend/src/AppBuilder/Widgets/Listview.jsx @@ -12,11 +12,8 @@ import { shallow } from 'zustand/shallow'; export const Listview = function Listview({ id, - component, width, height, - containerProps, - removeComponent, properties, styles, fireEvent, @@ -270,38 +267,8 @@ export const Listview = function Listview({ columns={positiveColumns} listViewMode={mode} darkMode={darkMode} + componentType="Listview" /> - {/* { - const changedData = { [component.name]: { [optionName]: value } }; - const existingDataAtIndex = prevData[index] ?? {}; - const newDataAtIndex = { - ...prevData[index], - [component.name]: { - ...existingDataAtIndex[component.name], - ...changedData[component.name], - id: componentId, - }, - }; - const newChildrenData = { ...prevData, [index]: newDataAtIndex }; - return { ...prevData, ...newChildrenData }; - }); - }} - /> */}
))}
diff --git a/frontend/src/AppBuilder/Widgets/Tabs.jsx b/frontend/src/AppBuilder/Widgets/Tabs.jsx index 3a93fa698b..7f4fb527e4 100644 --- a/frontend/src/AppBuilder/Widgets/Tabs.jsx +++ b/frontend/src/AppBuilder/Widgets/Tabs.jsx @@ -126,6 +126,7 @@ export const Tabs = function Tabs({ allowContainerSelect={true} styles={{ backgroundColor: bgColor }} darkMode={darkMode} + componentType="Tabs" /> ); diff --git a/frontend/src/AppBuilder/_helpers/editorHelpers.js b/frontend/src/AppBuilder/_helpers/editorHelpers.js index 38e9abd955..5e817c7f7e 100644 --- a/frontend/src/AppBuilder/_helpers/editorHelpers.js +++ b/frontend/src/AppBuilder/_helpers/editorHelpers.js @@ -59,7 +59,7 @@ import { BoundedBox } from '@/Editor/Components/BoundedBox/BoundedBox'; import { isPDFSupported } from '@/_helpers/appUtils'; import { resolveWidgetFieldValue } from '@/_helpers/utils'; import { useEditorStore } from '@/_stores/editorStore'; -import { Container } from '@/AppBuilder/Widgets/Container'; +import { Container } from '@/AppBuilder/Widgets/Container/Container'; import { Listview } from '@/AppBuilder/Widgets/Listview'; import { Tabs } from '@/AppBuilder/Widgets/Tabs'; import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban'; diff --git a/frontend/src/AppBuilder/_stores/slices/gridSlice.js b/frontend/src/AppBuilder/_stores/slices/gridSlice.js index 1958b8bece..642266a32b 100644 --- a/frontend/src/AppBuilder/_stores/slices/gridSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/gridSlice.js @@ -7,6 +7,7 @@ const initialState = { triggerCanvasUpdater: false, lastCanvasIdClick: '', lastCanvasClickPosition: null, + draggingComponentId: null, }; export const createGridSlice = (set, get) => ({ @@ -21,6 +22,7 @@ export const createGridSlice = (set, get) => ({ debouncedToggleCanvasUpdater: debounce(() => { get().toggleCanvasUpdater(); }, 200), + setDraggingComponentId: (id) => set(() => ({ draggingComponentId: id })), moveComponentPosition: (direction) => { const { setComponentLayout, currentLayout, getSelectedComponentsDefinition, debouncedToggleCanvasUpdater } = get(); let layouts = {}; diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx index ae2740bc84..3cdad272b7 100644 --- a/frontend/src/Editor/DraggableBox.jsx +++ b/frontend/src/Editor/DraggableBox.jsx @@ -10,7 +10,7 @@ import { resolveWidgetFieldValue } from '@/_helpers/utils'; import ErrorBoundary from './ErrorBoundary'; import { useEditorStore } from '@/_stores/editorStore'; import { shallow } from 'zustand/shallow'; -import { useNoOfGrid, useGridStore } from '@/_stores/gridStore'; +import { useGridStore } from '@/_stores/gridStore'; import WidgetBox from './WidgetBox'; import * as Sentry from '@sentry/react'; import { findHighestLevelofSelection } from './DragContainer'; @@ -61,7 +61,7 @@ const DraggableBox = React.memo( }) => { const isResizing = useGridStore((state) => state.resizingComponentId === id); const [canDrag, setCanDrag] = useState(true); - const noOfGrid = useNoOfGrid(); + const noOfGrid = 43; const { currentLayout, setHoveredComponent, diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx index 01335af55a..fc78875819 100644 --- a/frontend/src/Editor/SubContainer.jsx +++ b/frontend/src/Editor/SubContainer.jsx @@ -23,7 +23,7 @@ import { useEditorStore } from '@/_stores/editorStore'; // eslint-disable-next-line import/no-unresolved import { diff } from 'deep-object-diff'; -import { useGridStore, useResizingComponentId } from '@/_stores/gridStore'; +import { useGridStore } from '@/_stores/gridStore'; import GhostWidget from './GhostWidget'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; @@ -68,7 +68,7 @@ export const SubContainer = ({ shallow ); - const resizingComponentId = useResizingComponentId(); + const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); const noOfGrids = 43; const { isGridActive } = useGridStore((state) => ({ isGridActive: state.activeGrid === parent }), shallow); diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index ac82fcb171..2d8eb7f0a8 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -4,9 +4,40 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 330, + height: 480, }, defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 10, + left: 1, + height: 40, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Form title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 12, + left: 32, + height: 36, + }, + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, { componentName: 'Text', layout: { @@ -225,6 +256,7 @@ export const formConfig = { loadingState: { type: 'toggle', displayName: 'Loading state', + section: 'additionalActions', validation: { schema: { type: 'boolean' }, defaultValue: false, @@ -242,12 +274,64 @@ export const formConfig = { value: true, }, }, + showHeader: { type: 'toggle', displayName: 'Header' }, + showFooter: { type: 'toggle', displayName: 'Footer' }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, }, events: { onSubmit: { displayName: 'On submit' }, onInvalid: { displayName: 'On invalid' }, }, styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + headerHeight: { + type: 'code', + displayName: 'Header height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, + footerHeight: { + type: 'code', + displayName: 'Footer height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, backgroundColor: { type: 'color', displayName: 'Background color', @@ -274,26 +358,13 @@ export const formConfig = { defaultValue: '#fff', }, }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, - }, - disabledState: { - type: 'toggle', - displayName: 'Disable', - validation: { - schema: { type: 'boolean' }, - defaultValue: false, - }, - }, }, exposedVariables: { data: {}, isValid: true, + isVisible: true, + isDisabled: false, + isLoading: false, }, actions: [ { @@ -304,6 +375,21 @@ export const formConfig = { handle: 'resetForm', displayName: 'Reset Form', }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisable', + displayName: 'Set Disable', + params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set Loading', + params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }], + }, ], definition: { others: { @@ -317,14 +403,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, + showHeader: { value: '{{false}}' }, + showFooter: { value: '{{false}}' }, + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - visibility: { value: '{{true}}' }, - disabledState: { value: '{{false}}' }, + headerHeight: { value: '60px' }, + footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/_stores/gridStore.js b/frontend/src/_stores/gridStore.js index aa070e4a89..213f07ac16 100644 --- a/frontend/src/_stores/gridStore.js +++ b/frontend/src/_stores/gridStore.js @@ -7,7 +7,6 @@ const initialState = { noOfGrid: 43, draggedSubContainer: false, resizingComponentId: null, - draggingComponentId: null, dragTarget: null, isGroupHandleHoverd: false, idGroupDragged: false, @@ -20,11 +19,7 @@ export const useGridStore = create( (set) => ({ ...initialState, actions: { - setActiveGrid: (gridId) => set({ activeGrid: gridId }), - setNoOfGrid: (noOfGrid) => set({ noOfGrid }), - setDraggedSubContainer: (draggedSubContainer) => set({ draggedSubContainer }), setResizingComponentId: (id) => set({ resizingComponentId: id }), - setDraggingComponentId: (id) => set({ draggingComponentId: id }), setDragTarget: (dragTarget) => set({ dragTarget }), setIsGroupHandleHoverd: (isGroupHandleHoverd) => set({ isGroupHandleHoverd }), setIdGroupDragged: (idGroupDragged) => set({ idGroupDragged }), @@ -46,21 +41,5 @@ useGridStore.subscribe(({ draggingComponentId }) => { } }); -// useEditorStore.subscribe(({ hoveredComponent }) => { -// console.log('hoveredComponent--', hoveredComponent); -// if (hoveredComponent) { -// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control'); -// document.querySelector(`[target-id='${hoveredComponent}']`)?.setAttribute('data-hovered-control', true); -// } else if (document.querySelector(`[data-hovered-control]`)) { -// document.querySelector(`[data-hovered-control]`)?.removeAttribute('data-hovered-control'); -// } -// }); - -export const useActiveGrid = () => useGridStore((state) => state.activeGrid, shallow); -export const useNoOfGrid = () => useGridStore((state) => state.noOfGrid, shallow); -export const useDraggedSubContainer = () => useGridStore((state) => state.draggedSubContainer, shallow); -export const useGridStoreActions = () => useGridStore((state) => state.actions, shallow); -export const useDragTarget = () => useGridStore((state) => state.dragTarget, shallow); -export const useResizingComponentId = () => useGridStore((state) => state.resizingComponentId, shallow); export const useIsGroupHandleHoverd = () => useGridStore((state) => state.isGroupHandleHoverd, shallow); export const useOpenModalWidgetId = () => useGridStore((state) => state.openModalWidgetId, shallow); diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js index ac82fcb171..2d8eb7f0a8 100644 --- a/server/src/modules/apps/services/widget-config/form.js +++ b/server/src/modules/apps/services/widget-config/form.js @@ -4,9 +4,40 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 330, + height: 480, }, defaultChildren: [ + { + componentName: 'Text', + slotName: 'header', + layout: { + top: 10, + left: 1, + height: 40, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor'], + defaultValue: { + text: 'Form title', + textSize: 20, + textColor: '#000', + }, + }, + { + componentName: 'Button', + slotName: 'footer', + layout: { + top: 12, + left: 32, + height: 36, + }, + properties: ['text'], + defaultValue: { + text: 'Button2', + padding: 'none', + }, + }, { componentName: 'Text', layout: { @@ -225,6 +256,7 @@ export const formConfig = { loadingState: { type: 'toggle', displayName: 'Loading state', + section: 'additionalActions', validation: { schema: { type: 'boolean' }, defaultValue: false, @@ -242,12 +274,64 @@ export const formConfig = { value: true, }, }, + showHeader: { type: 'toggle', displayName: 'Header' }, + showFooter: { type: 'toggle', displayName: 'Footer' }, + visibility: { + type: 'toggle', + displayName: 'Visibility', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: true, + }, + }, + disabledState: { + type: 'toggle', + displayName: 'Disable', + section: 'additionalActions', + validation: { + schema: { type: 'boolean' }, + defaultValue: false, + }, + }, }, events: { onSubmit: { displayName: 'On submit' }, onInvalid: { displayName: 'On invalid' }, }, styles: { + headerBackgroundColor: { + type: 'color', + displayName: 'Header background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + footerBackgroundColor: { + type: 'color', + displayName: 'Footer background color', + validation: { + schema: { type: 'string' }, + defaultValue: '#ffffffff', + }, + }, + headerHeight: { + type: 'code', + displayName: 'Header height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, + footerHeight: { + type: 'code', + displayName: 'Footer height', + validation: { + schema: { type: 'string' }, + defaultValue: '80px', + }, + }, backgroundColor: { type: 'color', displayName: 'Background color', @@ -274,26 +358,13 @@ export const formConfig = { defaultValue: '#fff', }, }, - visibility: { - type: 'toggle', - displayName: 'Visibility', - validation: { - schema: { type: 'boolean' }, - defaultValue: true, - }, - }, - disabledState: { - type: 'toggle', - displayName: 'Disable', - validation: { - schema: { type: 'boolean' }, - defaultValue: false, - }, - }, }, exposedVariables: { data: {}, isValid: true, + isVisible: true, + isDisabled: false, + isLoading: false, }, actions: [ { @@ -304,6 +375,21 @@ export const formConfig = { handle: 'resetForm', displayName: 'Reset Form', }, + { + handle: 'setVisibility', + displayName: 'Set visibility', + params: [{ handle: 'setVisibility', displayName: 'Set Visibility', defaultValue: '{{true}}', type: 'toggle' }], + }, + { + handle: 'setDisable', + displayName: 'Set Disable', + params: [{ handle: 'setDisable', displayName: 'Set Disable', defaultValue: '{{false}}', type: 'toggle' }], + }, + { + handle: 'setLoading', + displayName: 'Set Loading', + params: [{ handle: 'setLoading', displayName: 'Set Loading', defaultValue: '{{false}}', type: 'toggle' }], + }, ], definition: { others: { @@ -317,14 +403,18 @@ export const formConfig = { value: "{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}", }, + showHeader: { value: '{{false}}' }, + showFooter: { value: '{{false}}' }, + visibility: { value: '{{true}}' }, + disabledState: { value: '{{false}}' }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - visibility: { value: '{{true}}' }, - disabledState: { value: '{{false}}' }, + headerHeight: { value: '60px' }, + footerHeight: { value: '60px' }, }, }, };