From d5b3708de00c299de8690ad6e4de93f62116a459 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 20 Mar 2025 21:38:15 +0530 Subject: [PATCH 01/43] feat: Makes header and footer resizable --- .../AppBuilder/AppCanvas/Grid/gridUtils.js | 21 +++ .../Inspector/Components/Form.jsx | 19 --- .../AppBuilder/WidgetManager/widgets/form.js | 7 + .../Form/Components/HorizontalSlot.jsx | 78 ++++++++++ frontend/src/AppBuilder/Widgets/Form/Form.jsx | 137 ++++++++---------- .../src/AppBuilder/Widgets/Form/form.scss | 45 ++++++ .../src/AppBuilder/_hooks/useActiveSlot.js | 46 ++++++ frontend/src/AppBuilder/_hooks/useMoveable.js | 135 +++++++++++++++++ .../src/Editor/WidgetManager/configs/form.js | 7 + .../apps/services/widget-config/form.js | 7 + 10 files changed, 408 insertions(+), 94 deletions(-) create mode 100644 frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx create mode 100644 frontend/src/AppBuilder/_hooks/useActiveSlot.js create mode 100644 frontend/src/AppBuilder/_hooks/useMoveable.js diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index da179bc11d..b18dd9d311 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -415,6 +415,27 @@ export function hideGridLines() { document.getElementById('real-canvas')?.classList.add('hide-grid'); } +export function showGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + canvasElm.classList.remove('hide-grid'); + canvasElm.classList.add('show-grid'); + + document.getElementById('real-canvas')?.classList.add('hide-grid'); + document.getElementById('real-canvas')?.classList.remove('show-grid'); +} + +export function hideGridLinesOnSlot(slotId) { + var canvasElm = document.getElementById(`canvas-${slotId}`); + + + canvasElm.classList.remove('show-grid'); + canvasElm.classList.add('hide-grid'); + + document.getElementById('real-canvas')?.classList.remove('hide-grid'); + document.getElementById('real-canvas')?.classList.add('show-grid'); +} + // Track previously active elements for efficient cleanup let previousActiveWidgets = null; let previousActiveCanvas = null; diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index b39924854e..e0178ddbeb 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -168,25 +168,6 @@ export const baseComponentProperties = ( }); } - items.push({ - title: `${i18next.t('widget.common.general', 'General')}`, - isOpen: true, - children: ( - <> - {renderElement( - component, - componentMeta, - layoutPropertyChanged, - dataQueries, - 'tooltip', - 'general', - currentState, - allComponents - )} - - ), - }); - items.push({ title: `${i18next.t('widget.common.devices', 'Devices')}`, isOpen: true, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index 2d8eb7f0a8..c5194822b6 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -294,6 +294,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx new file mode 100644 index 0000000000..0e15e4a058 --- /dev/null +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -0,0 +1,78 @@ +import React, { useEffect } from 'react'; +import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; +import { showGridLinesOnSlot, hideGridLinesOnSlot } from '@/AppBuilder/AppCanvas/Grid/gridUtils'; +import { useResizable } from '@/AppBuilder/_hooks/useMoveable'; + +export const HorizontalSlot = React.memo( + ({ + id, + height = 0, + width, + darkMode, + isDisabled, + isActive, + slotName = 'header', // 'header' or 'footer' + slotStyle = {}, + onResize + }) => { + const parsedHeight = parseInt(height, 10); + + const { getRootProps, getHandleProps, getResizeState } = useResizable({ + initialHeight: parsedHeight, + initialWidth: '100%', // Now respects parent's width + minHeight: 40, + maxHeight: 400, + maxWidth: '100%', + stepHeight: 10, // Height will change in steps of 10px + onResize: () => {}, + onDragEnd: (values) => { + onResize(values); + }, + isReverseVerticalDrag: slotName === 'footer', // Reverse dragging for Footer + }); + + const { height: resizedHeight, isDragging } = getResizeState(); + + + + useEffect(() => { + if (isDragging) { + showGridLinesOnSlot(id); + } else { + hideGridLinesOnSlot(id); + } + }, [isDragging, id]); + + const canvasHeight = parseInt(resizedHeight, 10) / 10; + + return ( +
+
+ +
+
+ + {isDisabled && ( +
{}} + onDrop={(e) => e.stopPropagation()} + /> + )} +
+ ); + } +); diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index afeb4cf844..ffa2373a96 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -14,6 +14,9 @@ import { CONTAINER_FORM_CANVAS_PADDING, SUBCONTAINER_CANVAS_BORDER_WIDTH, } from '@/AppBuilder/AppCanvas/appCanvasConstants'; +import { HorizontalSlot } from './Components/HorizontalSlot'; +import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot'; + import './form.scss'; const getCanvasHeight = (height) => { @@ -35,6 +38,7 @@ export const Form = function Form(props) { properties, resetComponent = () => {}, dataCy, + onComponentClick, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); const { @@ -46,16 +50,7 @@ export const Form = function Form(props) { footerBackgroundColor, headerBackgroundColor, } = styles; - const { - buttonToSubmit, - loadingState, - advanced, - JSONSchema, - showHeader = false, - showFooter = false, - visibility, - disabledState, - } = properties; + const { buttonToSubmit, advanced, JSONSchema, showHeader = false, showFooter = false } = properties; const { isDisabled, isVisible, isLoading } = useExposeState( properties.loadingState, properties.visibility, @@ -76,16 +71,6 @@ export const Form = function Form(props) { flexDirection: 'column', }; - 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', @@ -96,13 +81,6 @@ export const Form = function Form(props) { 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({}); @@ -110,7 +88,6 @@ 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(() => { @@ -287,6 +264,38 @@ export const Form = function Form(props) { setChildrenData(childDataRef.current); }; + const activeSlot = useActiveSlot(id); // Track the active slot for this widget + const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); + const updateHeaderSizeInStore = ({ newHeight }) => { + const heightInPx = `${parseInt(newHeight, 10)}px`; + console.log('newHeight', newHeight); + setComponentProperty(id, `headerHeight`, heightInPx, 'properties', 'value', false); + }; + + const updateFooterSizeInStore = ({ newHeight }) => { + const heightInPx = `${parseInt(newHeight, 10)}px`; + console.log('newHeight', newHeight); + setComponentProperty(id, `footerHeight`, heightInPx, 'properties', 'value', false); + }; + const formFooter = { + flexShrink: 0, + paddingTop: '3px', + paddingBottom: '7px', + paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, + paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + backgroundColor: + ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, + }; + 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, + }; + return (
{showHeader && ( -
- - {isDisabled && ( -
{}} - onDrop={(e) => e.stopPropagation()} - /> - )} -
+ )} +
{isLoading ? (
@@ -382,30 +381,18 @@ export const Form = function Form(props) { )}
{showFooter && ( -
- - {isDisabled && ( - + )} ); diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss index 530e837eb2..1758e1d0d1 100644 --- a/frontend/src/AppBuilder/Widgets/Form/form.scss +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -1,3 +1,7 @@ +.jet-form-body { + background-color: inherit; +} + .wj-form-header { position: relative; &::after { @@ -38,3 +42,44 @@ box-sizing: content-box; padding: 4px 0; } + +.resizable-slot { + position: relative; + height: auto; + box-shadow: 0 0 0 1px transparent; /* Acts as a border */ + transition: box-shadow 0.15s ease-in-out; + + &:hover { + box-shadow: 0 0 0 1px var(--border-weak); + } + + &.active { + box-shadow: 0 0 0 1px var(--border-accent-strong); + } + + .resize-handle { + position: absolute; + bottom: -4px; + left: 50%; /* Center horizontally */ + transform: translateX(-50%); /* Ensure proper centering */ + width: 24px; + height: 8px; + border-radius: 4px; + background-color: var(--background-accent-strong); + cursor: ns-resize; + z-index: 1; + visibility: hidden; + transition: visibility 0.15s ease-in-out; + } + + &.active .resize-handle { + visibility: visible; + } +} +.only-bottom { +} + +.jet-form-footer .resize-handle { + top: -4px; + bottom: unset; +} diff --git a/frontend/src/AppBuilder/_hooks/useActiveSlot.js b/frontend/src/AppBuilder/_hooks/useActiveSlot.js new file mode 100644 index 0000000000..bc3269a7ca --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useActiveSlot.js @@ -0,0 +1,46 @@ +import { useState, useEffect } from 'react'; +import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; + +const useIsWidgetSelected = (id) => { + // Get selected components from store using shallow comparison + const selectedComponents = useStore((state) => state.selectedComponents, shallow); + + // Check if the only selected component is the provided `id` + return selectedComponents.length === 1 && selectedComponents[0] === id; +}; + + +export const useActiveSlot = (widgetId) => { + const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID + const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected + useEffect(() => { + const handleClick = (event) => { + let target = event.target; + + // Traverse up to find a slot with an id + while (target && target !== document.body) { + if (target.id && target.id.startsWith('canvas-')) { + const slotId = target.id.replace(/^canvas-/, ''); // ✅ Strip "canvas-" + setActiveSlot(slotId); + return; + } + target = target.parentElement; + } + + // If no slot is found, reset to widget ID + setActiveSlot(widgetId); + }; + + // Attach single click if the widget is selected, otherwise listen for double-click + const eventType = isSelected ? 'click' : 'dblclick'; + + document.addEventListener(eventType, handleClick); + + return () => { + document.removeEventListener(eventType, handleClick); + }; + }, [widgetId, isSelected]); // Re-run when widgetId or selection state changes + + return activeSlot; +}; diff --git a/frontend/src/AppBuilder/_hooks/useMoveable.js b/frontend/src/AppBuilder/_hooks/useMoveable.js new file mode 100644 index 0000000000..ea2687e473 --- /dev/null +++ b/frontend/src/AppBuilder/_hooks/useMoveable.js @@ -0,0 +1,135 @@ +import { useRef, useState } from 'react'; + +const defaultProps = { + minHeight: 50, + maxHeight: 600, + minWidth: 50, + maxWidth: 600, + lockHorizontal: false, + lockVertical: false, + stepHeight: 10, // Default step size for height + stepWidth: 10, // Default step size for width + onResize: null, + onDragStart: null, + onDragEnd: null, + isReverseVerticalDrag: false, +}; + +export const useResizable = (options = {}) => { + const props = { ...defaultProps, ...options }; + const parentRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); // ✅ Track dragging state + + const [height, setHeight] = useState( + typeof props.initialHeight === 'string' ? props.initialHeight : `${props.initialHeight || 200}px` + ); + const [width, setWidth] = useState( + typeof props.initialWidth === 'string' ? props.initialWidth : `${props.initialWidth || 200}px` + ); + + const getRootProps = () => ({ + ref: parentRef, + style: { height, width }, + }); + + const getResizeState = () => ({ + height, + width, + isDragging, + }); + + const getHandleProps = () => { + const handleMouseDown = (e) => { + // Prevent right-click drag activation (button === 2) + if (e.button === 2) return; + + if (!parentRef.current) return; + e.stopPropagation(); + e.preventDefault(); + const startHeight = parseInt(parentRef.current.clientHeight); + const startWidth = parseInt(parentRef.current.clientWidth); + const parentWidth = parentRef.current.parentElement ? parentRef.current.parentElement.clientWidth : startWidth; + const startY = e.clientY; + const startX = e.clientX; + const isPercentage = typeof props.initialWidth === 'string' && props.initialWidth.includes('%'); + + setIsDragging(true); // ✅ Set dragging state to true + + if (props.onDragStart) { + props.onDragStart({ newHeight: startHeight, newWidth: startWidth }); + } + + const handleMouseMove = (moveEvent) => { + moveEvent.stopPropagation(); + moveEvent.preventDefault(); + let newHeight = startHeight; + let newWidth = startWidth; + + if (!props.lockVertical) { + const deltaY = props.isReverseVerticalDrag ? startY - moveEvent.clientY : moveEvent.clientY - startY; + newHeight = startHeight + deltaY; + newHeight = Math.max(props.minHeight, Math.min(props.maxHeight, newHeight)); + newHeight = Math.round(newHeight / props.stepHeight) * props.stepHeight; // Snap to stepHeight + } + + if (!props.lockHorizontal) { + newWidth = startWidth + (moveEvent.clientX - startX); + newWidth = Math.max(props.minWidth, Math.min(props.maxWidth, newWidth)); + newWidth = Math.round(newWidth / props.stepWidth) * props.stepWidth; // Snap to stepWidth + + if (isPercentage) { + newWidth = (newWidth / parentWidth) * 100; // Convert to percentage + newWidth = `${newWidth.toFixed(2)}%`; + } else { + newWidth = `${newWidth}px`; + } + } + + setHeight(`${newHeight}px`); + setWidth(newWidth); + + if (parentRef.current) { + parentRef.current.style.height = `${newHeight}px`; + parentRef.current.style.width = newWidth; + } + + if (props.onResize) { + props.onResize({ + newHeight, + newWidth, + heightDiff: newHeight - startHeight, + widthDiff: isPercentage + ? parseInt(newWidth) - (startWidth / parentWidth) * 100 + : parseInt(newWidth) - startWidth, + }); + } + }; + + const handleMouseUp = () => { + setIsDragging(false); // ✅ Set dragging state to false + + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + + if (props.onDragEnd) { + // Get the updated height and width from the DOM instead of relying on state + const finalHeight = parentRef.current ? parseInt(parentRef.current.clientHeight) : parseInt(height); + const finalWidth = parentRef.current ? parseInt(parentRef.current.clientWidth) : parseInt(width); + + props.onDragEnd({ newHeight: finalHeight, newWidth: finalWidth }); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + }; + + return { + onMouseDown: handleMouseDown, + }; + }; + + return { rootRef: parentRef, getRootProps, getHandleProps, getResizeState }; +}; + +export default useResizable; diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index 2d8eb7f0a8..c5194822b6 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -294,6 +294,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js index 2d8eb7f0a8..c5194822b6 100644 --- a/server/src/modules/apps/services/widget-config/form.js +++ b/server/src/modules/apps/services/widget-config/form.js @@ -294,6 +294,13 @@ export const formConfig = { defaultValue: false, }, }, + tooltip: { + type: 'code', + displayName: 'Tooltip', + validation: { schema: { type: 'string' } }, + section: 'additionalActions', + placeholder: 'Enter tooltip text', + }, }, events: { onSubmit: { displayName: 'On submit' }, From 7a50c41103128ef312406d5b78e510528acada0d Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:52:37 +0530 Subject: [PATCH 02/43] Fixes interaction bugs with slot resizing --- .../Form/Components/HorizontalSlot.jsx | 15 ++++--- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 10 +++-- .../src/AppBuilder/Widgets/Form/form.scss | 25 ++++++++++- .../src/AppBuilder/_hooks/useActiveSlot.js | 41 ++++++++++++++++--- 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx index 0e15e4a058..e892dc4f9f 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -13,15 +13,16 @@ export const HorizontalSlot = React.memo( isActive, slotName = 'header', // 'header' or 'footer' slotStyle = {}, - onResize + onResize, + maxHeight, }) => { const parsedHeight = parseInt(height, 10); const { getRootProps, getHandleProps, getResizeState } = useResizable({ initialHeight: parsedHeight, initialWidth: '100%', // Now respects parent's width - minHeight: 40, - maxHeight: 400, + minHeight: 10, + maxHeight: maxHeight || 400, maxWidth: '100%', stepHeight: 10, // Height will change in steps of 10px onResize: () => {}, @@ -33,8 +34,6 @@ export const HorizontalSlot = React.memo( const { height: resizedHeight, isDragging } = getResizeState(); - - useEffect(() => { if (isDragging) { showGridLinesOnSlot(id); @@ -47,7 +46,10 @@ export const HorizontalSlot = React.memo( return (
-
+
diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index ffa2373a96..1328fc195d 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -268,21 +268,24 @@ export const Form = function Form(props) { const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); const updateHeaderSizeInStore = ({ newHeight }) => { const heightInPx = `${parseInt(newHeight, 10)}px`; - console.log('newHeight', newHeight); setComponentProperty(id, `headerHeight`, heightInPx, 'properties', 'value', false); }; const updateFooterSizeInStore = ({ newHeight }) => { const heightInPx = `${parseInt(newHeight, 10)}px`; - console.log('newHeight', newHeight); setComponentProperty(id, `footerHeight`, heightInPx, 'properties', 'value', false); }; + + // debugger; + const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10; + const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10; const formFooter = { flexShrink: 0, paddingTop: '3px', paddingBottom: '7px', paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + maxHeight: `${footerMaxHeight}px`, backgroundColor: ['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor, }; @@ -292,13 +295,14 @@ export const Form = function Form(props) { paddingTop: '7px', paddingLeft: `${CONTAINER_FORM_CANVAS_PADDING}px`, paddingRight: `${CONTAINER_FORM_CANVAS_PADDING}px`, + maxHeight: `${headerMaxHeight}px`, backgroundColor: ['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor, }; return (
{ export const useActiveSlot = (widgetId) => { const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected + useEffect(() => { - const handleClick = (event) => { + if (!isSelected) { + setActiveSlot(''); + } + }, [isSelected]); + + useEffect(() => { + const handleDoubleClick = (event) => { let target = event.target; // Traverse up to find a slot with an id @@ -31,16 +38,40 @@ export const useActiveSlot = (widgetId) => { // If no slot is found, reset to widget ID setActiveSlot(widgetId); }; + const handleSingleClick = (event) => { + let target = event.target; + + // Traverse up to find a valid main slot (not header/footer) + while (target && target !== document.body) { + if ( + target.id && + target.id.startsWith('canvas-') && + !target.id.endsWith('-header') && + !target.id.endsWith('-footer') + ) { + const slotId = target.id.replace(/^canvas-/, ''); // Strip "canvas-" + setActiveSlot(slotId); + return; + } + target = target.parentElement; + } + + // If no main slot is found, fallback to widget ID + setActiveSlot(widgetId); + }; // Attach single click if the widget is selected, otherwise listen for double-click - const eventType = isSelected ? 'click' : 'dblclick'; + // const eventType = isSelected ? 'click' : 'dblclick'; + const eventType = 'dblclick'; - document.addEventListener(eventType, handleClick); + document.addEventListener(eventType, handleDoubleClick); + document.addEventListener('click', handleSingleClick); return () => { - document.removeEventListener(eventType, handleClick); + document.removeEventListener(eventType, handleDoubleClick); + document.removeEventListener('click', handleSingleClick); }; - }, [widgetId, isSelected]); // Re-run when widgetId or selection state changes + }, [widgetId]); // Re-run when widgetId or selection state changes return activeSlot; }; From 86355f6f9bb3a921f35b6470c8356fd309a41849 Mon Sep 17 00:00:00 2001 From: Johnson Cherian Date: Fri, 28 Mar 2025 16:45:54 +0530 Subject: [PATCH 03/43] chore: Adds new design for form, container default children (#12239) Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> --- .../WidgetManager/widgets/container.js | 18 +- .../AppBuilder/WidgetManager/widgets/form.js | 266 +++++------------- .../Widgets/Container/Container.jsx | 3 +- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 21 +- .../src/AppBuilder/Widgets/Form/form.scss | 8 +- .../Editor/WidgetManager/configs/container.js | 18 +- .../src/Editor/WidgetManager/configs/form.js | 266 +++++------------- .../apps/services/widget-config/container.js | 20 +- .../apps/services/widget-config/form.js | 266 +++++------------- 9 files changed, 252 insertions(+), 634 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js index 37a895f553..d1670b8a93 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -47,10 +47,16 @@ export const containerConfig = { defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -97,15 +103,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -157,6 +154,7 @@ export const containerConfig = { loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index c5194822b6..f28044d52c 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,7 +19,7 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', + text: 'Form', textSize: 20, textColor: '#000', }, @@ -34,203 +34,83 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', + }, + }, + { + componentName: 'TextInput', + layout: { + top: 200, + left: 5, + height: 40, + width: 31, + }, + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto'], + defaultValue: { + label: 'Favorite color?', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +156,16 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -323,22 +213,6 @@ export const formConfig = { 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', @@ -410,18 +284,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}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx index 4978427370..a706d29069 100644 --- a/frontend/src/AppBuilder/Widgets/Container/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -33,7 +33,8 @@ export const Container = ({ shallow ); - const { borderRadius, borderColor, boxShadow, headerHeight = 80 } = styles; + const { borderRadius, borderColor, boxShadow } = styles; + const { headerHeight = 80 } = properties; const contentBgColor = useMemo(() => { return { backgroundColor: diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 1328fc195d..d918a1a2b5 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -41,16 +41,16 @@ export const Form = function Form(props) { onComponentClick, } = props; const childComponents = useStore((state) => state.getChildComponents(id), shallow); + const { borderRadius, borderColor, boxShadow, footerBackgroundColor, headerBackgroundColor } = styles; const { - borderRadius, - borderColor, - boxShadow, - headerHeight, - footerHeight, - footerBackgroundColor, - headerBackgroundColor, - } = styles; - const { buttonToSubmit, advanced, JSONSchema, showHeader = false, showFooter = false } = properties; + buttonToSubmit, + advanced, + JSONSchema, + showHeader = false, + showFooter = false, + headerHeight = 80, + footerHeight = 80, + } = properties; const { isDisabled, isVisible, isLoading } = useExposeState( properties.loadingState, properties.visibility, @@ -88,7 +88,8 @@ export const Form = function Form(props) { const [isValid, setValidation] = useState(true); const [uiComponents, setUIComponents] = useState([]); const mounted = useMounted(); - const canvasFooterHeight = getCanvasHeight(footerHeight) / 10; + const canvasHeaderHeight = headerHeight / 10; + const canvasFooterHeight = footerHeight / 10; useEffect(() => { const exposedVariables = { diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss index e1e694d7c0..766b309a7f 100644 --- a/frontend/src/AppBuilder/Widgets/Form/form.scss +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -10,8 +10,8 @@ content: ""; position: absolute; bottom: 0; - left: -7px; - right: -7px; + left: -2px; + right: -2px; height: 1px; background-color: var(--border-weak); } @@ -23,8 +23,8 @@ content: ""; position: absolute; top: 0; - left: -7px; - right: -7px; + left: -2px; + right: -2px; height: 1px; background-color: var(--border-weak); } diff --git a/frontend/src/Editor/WidgetManager/configs/container.js b/frontend/src/Editor/WidgetManager/configs/container.js index 37a895f553..d1670b8a93 100644 --- a/frontend/src/Editor/WidgetManager/configs/container.js +++ b/frontend/src/Editor/WidgetManager/configs/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -47,10 +47,16 @@ export const containerConfig = { defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -97,15 +103,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -157,6 +154,7 @@ export const containerConfig = { loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index c5194822b6..f28044d52c 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,7 +19,7 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', + text: 'Form', textSize: 20, textColor: '#000', }, @@ -34,203 +34,83 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', + }, + }, + { + componentName: 'TextInput', + layout: { + top: 200, + left: 5, + height: 40, + width: 31, + }, + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto'], + defaultValue: { + label: 'Favorite color?', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +156,16 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -323,22 +213,6 @@ export const formConfig = { 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', @@ -410,18 +284,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}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; diff --git a/server/src/modules/apps/services/widget-config/container.js b/server/src/modules/apps/services/widget-config/container.js index ec1d5174b0..d1670b8a93 100644 --- a/server/src/modules/apps/services/widget-config/container.js +++ b/server/src/modules/apps/services/widget-config/container.js @@ -3,7 +3,7 @@ export const containerConfig = { displayName: 'Container', description: 'Group components', defaultSize: { - width: 5, + width: 10, height: 200, }, component: 'Container', @@ -47,10 +47,16 @@ export const containerConfig = { defaultValue: true, }, }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, }, defaultChildren: [ { componentName: 'Text', + slotName: 'header', layout: { top: 20, left: 1, @@ -97,15 +103,6 @@ export const containerConfig = { }, accordian: 'container', }, - headerHeight: { - type: 'numberInput', - displayName: 'Height', - validation: { - schema: { type: 'number' }, - defaultValue: 80, - }, - accordian: 'header', - }, borderRadius: { type: 'numberInput', displayName: 'Border', @@ -153,10 +150,11 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: {value: `{{true}}`}, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: `{{80}}` }, }, events: [], styles: { diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js index c5194822b6..f28044d52c 100644 --- a/server/src/modules/apps/services/widget-config/form.js +++ b/server/src/modules/apps/services/widget-config/form.js @@ -4,7 +4,7 @@ export const formConfig = { description: 'Wrapper for multiple components', defaultSize: { width: 13, - height: 480, + height: 450, }, defaultChildren: [ { @@ -19,7 +19,7 @@ export const formConfig = { accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { - text: 'Form title', + text: 'Form', textSize: 20, textColor: '#000', }, @@ -34,203 +34,83 @@ export const formConfig = { }, properties: ['text'], defaultValue: { - text: 'Button2', + text: 'Submit', padding: 'none', }, }, - { - componentName: 'Text', - layout: { - top: 40, - left: 10, - height: 30, - width: 17, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'User Details', - fontWeight: 'bold', - textSize: 18, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 90, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Name', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, - { - componentName: 'Text', - layout: { - top: 160, - left: 10, - height: 30, - }, - properties: ['text'], - styles: [ - 'textSize', - 'fontWeight', - 'fontStyle', - 'textColor', - 'isScrollRequired', - 'lineHeight', - 'textIndent', - 'textAlign', - 'verticalAlignment', - 'decoration', - 'transformation', - 'letterSpacing', - 'wordSpacing', - 'fontVariant', - 'backgroundColor', - 'borderColor', - 'borderRadius', - 'boxShadow', - 'padding', - ], - defaultValue: { - text: 'Age', - fontWeight: 'normal', - textSize: 14, - textColor: '#000', - backgroundColor: '#fff00000', - textAlign: 'left', - decoration: 'none', - transformation: 'none', - fontStyle: 'normal', - lineHeight: 1.5, - textIndent: '0', - letterSpacing: '0', - wordSpacing: '0', - fontVariant: 'normal', - verticalAlignment: 'top', - padding: 'default', - boxShadow: '0px 0px 0px 0px #00000090', - borderRadius: '0', - isScrollRequired: 'enabled', - }, - }, { componentName: 'TextInput', layout: { - top: 120, - left: 10, - height: 30, - width: 25, + top: 20, + left: 5, + height: 40, + width: 31, }, properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { placeholder: 'Enter your name', - label: '', + label: 'Name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { componentName: 'NumberInput', layout: { - top: 190, - left: 10, - height: 30, - width: 25, + top: 80, + left: 5, + height: 40, + width: 31, }, - properties: ['value', 'label'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - value: 24, - label: '', + placeholder: 'Age', + label: 'Age', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, { - componentName: 'Button', + componentName: 'TextInput', layout: { - top: 240, - left: 10, - height: 30, - width: 10, + top: 140, + left: 5, + height: 40, + width: 31, }, - properties: ['text'], + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto', 'padding'], defaultValue: { - text: 'Submit', + placeholder: 'Tomy', + label: 'Pet name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', + }, + }, + { + componentName: 'TextInput', + layout: { + top: 200, + left: 5, + height: 40, + width: 31, + }, + properties: ['placeholder', 'label'], + styles: ['alignment', 'width', 'auto'], + defaultValue: { + label: 'Favorite color?', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', }, }, ], @@ -276,6 +156,16 @@ export const formConfig = { }, showHeader: { type: 'toggle', displayName: 'Header' }, showFooter: { type: 'toggle', displayName: 'Footer' }, + headerHeight: { + type: 'numberInput', + displayName: 'Header height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, + footerHeight: { + type: 'numberInput', + displayName: 'Footer height', + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, visibility: { type: 'toggle', displayName: 'Visibility', @@ -323,22 +213,6 @@ export const formConfig = { 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', @@ -410,18 +284,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}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, + headerHeight: { value: 60 }, + footerHeight: { value: 60 }, }, events: [], styles: { backgroundColor: { value: '#fff' }, borderRadius: { value: '0' }, borderColor: { value: '#fff' }, - headerHeight: { value: '60px' }, - footerHeight: { value: '60px' }, }, }, }; From 7384a96c6e0155ddba47d957ff458de3fe07fee0 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:55:15 +0530 Subject: [PATCH 04/43] Adds default height for body --- .../AppBuilder/WidgetManager/widgets/form.js | 27 +++------- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 17 +++--- .../src/AppBuilder/Widgets/Form/FormUtils.js | 20 +++++++ .../src/Editor/WidgetManager/configs/form.js | 27 +++------- .../apps/services/widget-config/form.js | 52 +++++++++++++++++-- 5 files changed, 88 insertions(+), 55 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index f28044d52c..e5996062fb 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -47,11 +47,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', label: 'Name', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -66,11 +67,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Age', label: 'Age', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -85,30 +87,13 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Tomy', label: 'Pet name', width: '{{60}}', alignment: 'side', - auto: '{{false}}', - padding: 'default', - }, - }, - { - componentName: 'TextInput', - layout: { - top: 200, - left: 5, - height: 40, - width: 31, - }, - properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto'], - defaultValue: { - label: 'Favorite color?', - width: '{{60}}', - alignment: 'side', + direction: 'left', auto: '{{false}}', padding: 'default', }, diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index d918a1a2b5..eb09a1ad4d 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -2,7 +2,7 @@ import React, { useRef, useState, useEffect } from 'react'; import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container'; // eslint-disable-next-line import/no-unresolved import _, { debounce, omit } from 'lodash'; -import { generateUIComponents } from './FormUtils'; +import { generateUIComponents, getBodyHeight } from './FormUtils'; import { useMounted } from '@/_hooks/use-mount'; import { onComponentClick, removeFunctionObjects } from '@/_helpers/appUtils'; import { deepClone } from '@/_helpers/utilities/utils.helpers'; @@ -19,11 +19,6 @@ import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot'; 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 { id, @@ -60,6 +55,9 @@ export const Form = function Form(props) { ); const backgroundColor = ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor; + + const computedFormBodyHeight = getBodyHeight(height, showHeader, showFooter, headerHeight, footerHeight); + const computedStyles = { backgroundColor, borderRadius: borderRadius ? parseFloat(borderRadius) : 0, @@ -336,10 +334,13 @@ export const Form = function Form(props) { ) : (
{!advanced && ( -
+
{ if (/^(true|false)$/i.test(input) == true) return JSON.parse(input); return true; }; + +export const getBodyHeight = (height, showHeader, showFooter, headerHeight = 60, footerHeight = 60) => { + let modalHeight = height ? parseInt(height, 10) : 0; + let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0; + let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0; + + if (showHeader) { + // 10 is header padding + modalHeight = modalHeight - parsedHeaderHeight - 10; + } + if (showFooter) { + // 14 is footer padding + modalHeight = modalHeight - parsedFooterHeight - 14; + } + + const rounded = Math.ceil(modalHeight / 10) * 10; + + console.log('rounded', rounded) + return `${Math.max(rounded - 20, 40)}px`; +}; diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index f28044d52c..e5996062fb 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -47,11 +47,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', label: 'Name', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -66,11 +67,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Age', label: 'Age', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -85,30 +87,13 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Tomy', label: 'Pet name', width: '{{60}}', alignment: 'side', - auto: '{{false}}', - padding: 'default', - }, - }, - { - componentName: 'TextInput', - layout: { - top: 200, - left: 5, - height: 40, - width: 31, - }, - properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto'], - defaultValue: { - label: 'Favorite color?', - width: '{{60}}', - alignment: 'side', + direction: 'left', auto: '{{false}}', padding: 'default', }, diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js index f28044d52c..d46c637849 100644 --- a/server/src/modules/apps/services/widget-config/form.js +++ b/server/src/modules/apps/services/widget-config/form.js @@ -47,11 +47,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Enter your name', label: 'Name', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -66,11 +67,12 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Age', label: 'Age', width: '{{60}}', + direction: 'left', alignment: 'side', auto: '{{false}}', padding: 'default', @@ -85,32 +87,72 @@ export const formConfig = { width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'padding'], + styles: ['alignment', 'width', 'auto', 'padding', 'direction'], defaultValue: { placeholder: 'Tomy', label: 'Pet name', width: '{{60}}', alignment: 'side', + direction: 'left', auto: '{{false}}', padding: 'default', }, }, { - componentName: 'TextInput', + componentName: 'Text', layout: { top: 200, left: 5, + height: 30, + width: 10, + }, + properties: ['text'], + accessorKey: 'text', + styles: ['fontWeight', 'textSize', 'textColor', 'direction'], + defaultValue: { + text: 'Who are you', + textSize: 12, + direction: 'left', + textColor: '#000', + }, + }, + { + componentName: 'TextArea', + layout: { + top: 200, + left: 14, + height: 80, + width: 22, + }, + properties: ['placeholder', 'value'], + styles: ['alignment', 'width', 'auto', 'padding', 'visibility'], + defaultValue: { + placeholder: 'Tomy', + value: 'Pet name', + width: '{{60}}', + alignment: 'side', + auto: '{{false}}', + padding: 'default', + visibility: '{{true}}', + }, + }, + { + componentName: 'MultiselectV2', + layout: { + top: 400, + left: 5, height: 40, width: 31, }, properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto'], + styles: ['alignment', 'width', 'auto', 'direction'], defaultValue: { label: 'Favorite color?', width: '{{60}}', alignment: 'side', auto: '{{false}}', padding: 'default', + direction: 'left', }, }, ], From a3db2ab3d5e54f14d2f9db7a0d6ebd91a70b135a Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:39:26 +0530 Subject: [PATCH 05/43] Hides header footer height --- .../src/AppBuilder/WidgetManager/widgets/form.js | 4 +++- .../Widgets/Form/Components/HorizontalSlot.jsx | 5 ++++- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 15 ++++++++------- frontend/src/AppBuilder/Widgets/Form/form.scss | 6 +++--- frontend/src/Editor/WidgetManager/configs/form.js | 4 +++- .../modules/apps/services/widget-config/form.js | 4 +++- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index e5996062fb..0ef410b908 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -20,7 +20,7 @@ export const formConfig = { styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { text: 'Form', - textSize: 20, + textSize: 16, textColor: '#000', }, }, @@ -144,11 +144,13 @@ export const formConfig = { headerHeight: { type: 'numberInput', displayName: 'Header height', + isHidden: true, validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, }, footerHeight: { type: 'numberInput', displayName: 'Footer height', + isHidden: true, validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, }, visibility: { diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx index e892dc4f9f..ad58c835ca 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -14,6 +14,7 @@ export const HorizontalSlot = React.memo( slotName = 'header', // 'header' or 'footer' slotStyle = {}, onResize, + isEditing, maxHeight, }) => { const parsedHeight = parseInt(height, 10); @@ -47,7 +48,9 @@ export const HorizontalSlot = React.memo( return (
state.setComponentProperty, shallow); const updateHeaderSizeInStore = ({ newHeight }) => { - const heightInPx = `${parseInt(newHeight, 10)}px`; - setComponentProperty(id, `headerHeight`, heightInPx, 'properties', 'value', false); + const _height = parseInt(newHeight, 10); + setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false); }; const updateFooterSizeInStore = ({ newHeight }) => { - const heightInPx = `${parseInt(newHeight, 10)}px`; - setComponentProperty(id, `footerHeight`, heightInPx, 'properties', 'value', false); + const _height = parseInt(newHeight, 10); + setComponentProperty(id, `footerHeight`, _height, 'properties', 'value', false); }; - // debugger; + const mode = useStore((state) => state.currentMode, shallow); + const isEditing = mode === 'edit'; const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10; const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10; const formFooter = { @@ -315,7 +316,7 @@ export const Form = function Form(props) { Date: Thu, 3 Apr 2025 12:20:12 +0530 Subject: [PATCH 06/43] Fixes border color --- frontend/src/AppBuilder/Widgets/Form/form.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/form.scss b/frontend/src/AppBuilder/Widgets/Form/form.scss index 6756ec51ed..e012f0a075 100644 --- a/frontend/src/AppBuilder/Widgets/Form/form.scss +++ b/frontend/src/AppBuilder/Widgets/Form/form.scss @@ -56,7 +56,7 @@ box-shadow: 0 0 0 1px var(--border-weak); } - &is-editing.active { + &.is-editing.active { box-shadow: 0 0 0 1px var(--border-accent-weak); } From 86625c01a06b853288641dc2bd5c18224b0c8885 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 3 Apr 2025 12:41:31 +0530 Subject: [PATCH 07/43] Fixes border radius --- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 5370bc6a55..8c015fefc1 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -57,6 +57,7 @@ export const Form = function Form(props) { ['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor; const computedFormBodyHeight = getBodyHeight(height, showHeader, showFooter, headerHeight, footerHeight); + const computedBorderRadius = `${borderRadius ? parseFloat(borderRadius) : 0}px`; const computedStyles = { backgroundColor, @@ -67,6 +68,7 @@ export const Form = function Form(props) { position: 'relative', boxShadow, flexDirection: 'column', + clipPath: `inset(0 round ${computedBorderRadius})`, }; const formContent = { From f9f3f841111a61edb9a2a2211229237df7bf5d5d Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:07:19 +0530 Subject: [PATCH 08/43] Changes resize handle size --- .../AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx | 6 +++++- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 8 +++----- frontend/src/AppBuilder/Widgets/Form/form.scss | 4 +++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx index ad58c835ca..86a5c58b14 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -45,6 +45,10 @@ export const HorizontalSlot = React.memo( const canvasHeight = parseInt(resizedHeight, 10) / 10; + const resizeStyle = { + backgroundColor: darkMode ? '#1F2837' : '#fff', + }; + return (
-
+
{isDisabled && ( diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 8c015fefc1..7e536f5e1f 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -88,8 +88,6 @@ export const Form = function Form(props) { const [isValid, setValidation] = useState(true); const [uiComponents, setUIComponents] = useState([]); const mounted = useMounted(); - const canvasHeaderHeight = headerHeight / 10; - const canvasFooterHeight = footerHeight / 10; useEffect(() => { const exposedVariables = { @@ -318,7 +316,7 @@ export const Form = function Form(props) { )} -
+
{isLoading ? (
@@ -393,7 +391,7 @@ export const Form = function Form(props) { Date: Tue, 8 Apr 2025 13:53:31 +0530 Subject: [PATCH 09/43] fix: Update container header for old data to false --- .../Inspector/Components/Form.jsx | 36 ++++++------- .../WidgetManager/widgets/container.js | 4 +- .../Editor/WidgetManager/configs/container.js | 4 +- ...097765065-UpdateContainerHeaderProperty.ts | 50 +++++++++++++++++++ .../apps/services/widget-config/container.js | 4 +- 5 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 server/migrations/1744097765065-UpdateContainerHeaderProperty.ts diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx index b39924854e..b6d7033b49 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Form.jsx @@ -110,24 +110,6 @@ 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')}`, @@ -149,6 +131,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 (validations.length > 0) { items.push({ title: `${i18next.t('widget.common.validation', 'Validation')}`, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js index 424b9a801d..04eb035abf 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js @@ -44,7 +44,7 @@ export const containerConfig = { displayName: 'Show header', validation: { schema: { type: 'boolean' }, - defaultValue: false, + defaultValue: true, }, }, }, @@ -154,7 +154,7 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: { value: `{{false}}` }, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/WidgetManager/configs/container.js b/frontend/src/Editor/WidgetManager/configs/container.js index 424b9a801d..04eb035abf 100644 --- a/frontend/src/Editor/WidgetManager/configs/container.js +++ b/frontend/src/Editor/WidgetManager/configs/container.js @@ -44,7 +44,7 @@ export const containerConfig = { displayName: 'Show header', validation: { schema: { type: 'boolean' }, - defaultValue: false, + defaultValue: true, }, }, }, @@ -154,7 +154,7 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: { value: `{{false}}` }, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, diff --git a/server/migrations/1744097765065-UpdateContainerHeaderProperty.ts b/server/migrations/1744097765065-UpdateContainerHeaderProperty.ts new file mode 100644 index 0000000000..1e7dd95b9e --- /dev/null +++ b/server/migrations/1744097765065-UpdateContainerHeaderProperty.ts @@ -0,0 +1,50 @@ +import { Component } from 'src/entities/component.entity'; + +import { processDataInBatches } from '@helpers/migration.helper'; +import { EntityManager, MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateContainerHeaderProperty1744097765065 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + const componentTypes = ['Container']; + const batchSize = 100; + const entityManager = queryRunner.manager; + + for (const componentType of componentTypes) { + await processDataInBatches( + entityManager, + async (entityManager: EntityManager) => { + return await entityManager.find(Component, { + where: { type: componentType }, + order: { createdAt: 'ASC' }, + }); + }, + async (entityManager: EntityManager, components: Component[]) => { + await this.processUpdates(entityManager, components); + }, + batchSize + ); + } + } + + private async processUpdates(entityManager: EntityManager, components: Component[]) { + for (const component of components) { + const properties = component.properties; + const styles = component.styles; + const general = component.general; + + // Update showHeader property to false for old instances + if (!properties.showHeader) { + properties.showHeader = { value: '{{false}}' }; + } + + // Update the modal component with the modified properties + await entityManager.update(Component, component.id, { + properties, + styles, + general, + }); + } + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/server/src/modules/apps/services/widget-config/container.js b/server/src/modules/apps/services/widget-config/container.js index 424b9a801d..04eb035abf 100644 --- a/server/src/modules/apps/services/widget-config/container.js +++ b/server/src/modules/apps/services/widget-config/container.js @@ -44,7 +44,7 @@ export const containerConfig = { displayName: 'Show header', validation: { schema: { type: 'boolean' }, - defaultValue: false, + defaultValue: true, }, }, }, @@ -154,7 +154,7 @@ export const containerConfig = { showOnMobile: { value: '{{false}}' }, }, properties: { - showHeader: { value: `{{false}}` }, + showHeader: { value: `{{true}}` }, loadingState: { value: `{{false}}` }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, From 9c820813a32978cf691e58d3615bd012880b2332 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Mon, 14 Apr 2025 12:23:30 +0530 Subject: [PATCH 10/43] Created page_permissions and page_users table. --- .../1744610362161-CreatePagePermissions.ts | 51 +++++++++++++ .../1744611380594-CreatePageUsers.ts | 76 +++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 server/migrations/1744610362161-CreatePagePermissions.ts create mode 100644 server/migrations/1744611380594-CreatePageUsers.ts diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts new file mode 100644 index 0000000000..ebf622da8b --- /dev/null +++ b/server/migrations/1744610362161-CreatePagePermissions.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreatePagePermissions1744610362161 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'page_permissions', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'page_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'type', + type: 'enum', + enum: ['SINGLE', 'GROUP'], + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'page_permissions', + new TableForeignKey({ + columnNames: ['page_id'], + referencedColumnNames: ['id'], + referencedTableName: 'pages', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('page_permissions'); + } +} diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts new file mode 100644 index 0000000000..f1c6c89beb --- /dev/null +++ b/server/migrations/1744611380594-CreatePageUsers.ts @@ -0,0 +1,76 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class CreatePageUsers1744611380594 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'page_users', + columns: [ + { + name: 'id', + type: 'uuid', + isGenerated: true, + default: 'gen_random_uuid()', + isPrimary: true, + }, + { + name: 'page_permissions_id', + type: 'uuid', + isNullable: false, + }, + { + name: 'user_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'permission_groups_id', + type: 'uuid', + isNullable: true, + }, + { + name: 'created_at', + type: 'timestamp', + isNullable: false, + default: 'now()', + }, + ], + }), + true + ); + + await queryRunner.createForeignKey( + 'page_users', + new TableForeignKey({ + columnNames: ['page_permissions_id'], + referencedColumnNames: ['id'], + referencedTableName: 'page_permissions', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'page_users', + new TableForeignKey({ + columnNames: ['user_id'], + referencedColumnNames: ['id'], + referencedTableName: 'users', + onDelete: 'CASCADE', + }) + ); + + await queryRunner.createForeignKey( + 'page_users', + new TableForeignKey({ + columnNames: ['permission_groups_id'], + referencedColumnNames: ['id'], + referencedTableName: 'permission_groups', + onDelete: 'CASCADE', + }) + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('page_users'); + } +} From 4025d2305044ef1fa4f1910aad6ce14f99bd7d3c Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Mon, 14 Apr 2025 12:30:23 +0530 Subject: [PATCH 11/43] Added edition check in migrations. --- server/migrations/1744610362161-CreatePagePermissions.ts | 6 ++++++ server/migrations/1744611380594-CreatePageUsers.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts index ebf622da8b..ca4afbac66 100644 --- a/server/migrations/1744610362161-CreatePagePermissions.ts +++ b/server/migrations/1744610362161-CreatePagePermissions.ts @@ -1,7 +1,13 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; +import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePagePermissions1744610362161 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { + if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { + return; + } + await queryRunner.createTable( new Table({ name: 'page_permissions', diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts index f1c6c89beb..5fe4d126c7 100644 --- a/server/migrations/1744611380594-CreatePageUsers.ts +++ b/server/migrations/1744611380594-CreatePageUsers.ts @@ -1,7 +1,13 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; +import { TOOLJET_EDITIONS } from '@modules/app/constants'; +import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePageUsers1744611380594 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { + if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { + return; + } + await queryRunner.createTable( new Table({ name: 'page_users', From 882c80a765051787a6650b07ad68f3a5b229411e Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:15:13 +0530 Subject: [PATCH 12/43] Disables resize handle on view mode --- .../Widgets/Form/Components/HorizontalSlot.jsx | 2 +- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 2 +- frontend/src/AppBuilder/_hooks/useActiveSlot.js | 17 ++++++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx index 86a5c58b14..888b91d3b7 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Components/HorizontalSlot.jsx @@ -70,7 +70,7 @@ export const HorizontalSlot = React.memo( }} componentType="Form" /> -
+ {isEditing &&
}
{isDisabled && ( diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 7e536f5e1f..c674d4f84a 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -263,7 +263,7 @@ export const Form = function Form(props) { setChildrenData(childDataRef.current); }; - const activeSlot = useActiveSlot(id); // Track the active slot for this widget + const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); const updateHeaderSizeInStore = ({ newHeight }) => { const _height = parseInt(newHeight, 10); diff --git a/frontend/src/AppBuilder/_hooks/useActiveSlot.js b/frontend/src/AppBuilder/_hooks/useActiveSlot.js index 9eb69c05b6..14f80a366f 100644 --- a/frontend/src/AppBuilder/_hooks/useActiveSlot.js +++ b/frontend/src/AppBuilder/_hooks/useActiveSlot.js @@ -10,7 +10,6 @@ const useIsWidgetSelected = (id) => { return selectedComponents.length === 1 && selectedComponents[0] === id; }; - export const useActiveSlot = (widgetId) => { const [activeSlot, setActiveSlot] = useState(''); // Default to widget ID const isSelected = useIsWidgetSelected(widgetId); // Check if widget is selected @@ -25,6 +24,11 @@ export const useActiveSlot = (widgetId) => { const handleDoubleClick = (event) => { let target = event.target; + if (!widgetId) { + setActiveSlot(null); + return; + } + // Traverse up to find a slot with an id while (target && target !== document.body) { if (target.id && target.id.startsWith('canvas-')) { @@ -41,6 +45,11 @@ export const useActiveSlot = (widgetId) => { const handleSingleClick = (event) => { let target = event.target; + if (!widgetId) { + setActiveSlot(null); + return; + } + // Traverse up to find a valid main slot (not header/footer) while (target && target !== document.body) { if ( @@ -61,14 +70,12 @@ export const useActiveSlot = (widgetId) => { }; // Attach single click if the widget is selected, otherwise listen for double-click - // const eventType = isSelected ? 'click' : 'dblclick'; - const eventType = 'dblclick'; - document.addEventListener(eventType, handleDoubleClick); + document.addEventListener('dblclick', handleDoubleClick); document.addEventListener('click', handleSingleClick); return () => { - document.removeEventListener(eventType, handleDoubleClick); + document.removeEventListener('dblclick', handleDoubleClick); document.removeEventListener('click', handleSingleClick); }; }, [widgetId]); // Re-run when widgetId or selection state changes From 45d38b3f6ec0a7238663d46edeaf064c10a4150c Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Tue, 15 Apr 2025 18:06:27 +0530 Subject: [PATCH 13/43] Created the base for app-permissions module. --- .../modules/app-permissions/ability/guard.ts | 26 +++++++++++ .../modules/app-permissions/ability/index.ts | 43 +++++++++++++++++++ .../app-permissions/constants/features.ts | 9 ++++ .../app-permissions/constants/index.ts | 3 ++ .../src/modules/app-permissions/controller.ts | 27 ++++++++++++ .../app-permissions/interfaces/IController.ts | 6 +++ .../app-permissions/interfaces/IService.ts | 3 ++ server/src/modules/app-permissions/module.ts | 19 ++++++++ server/src/modules/app-permissions/service.ts | 11 +++++ .../modules/app-permissions/types/index.ts | 11 +++++ .../src/modules/app/constants/module-info.ts | 2 + server/src/modules/app/constants/modules.ts | 1 + server/src/modules/app/module.ts | 2 + 13 files changed, 163 insertions(+) create mode 100644 server/src/modules/app-permissions/ability/guard.ts create mode 100644 server/src/modules/app-permissions/ability/index.ts create mode 100644 server/src/modules/app-permissions/constants/features.ts create mode 100644 server/src/modules/app-permissions/constants/index.ts create mode 100644 server/src/modules/app-permissions/controller.ts create mode 100644 server/src/modules/app-permissions/interfaces/IController.ts create mode 100644 server/src/modules/app-permissions/interfaces/IService.ts create mode 100644 server/src/modules/app-permissions/module.ts create mode 100644 server/src/modules/app-permissions/service.ts create mode 100644 server/src/modules/app-permissions/types/index.ts diff --git a/server/src/modules/app-permissions/ability/guard.ts b/server/src/modules/app-permissions/ability/guard.ts new file mode 100644 index 0000000000..1011d7985b --- /dev/null +++ b/server/src/modules/app-permissions/ability/guard.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { FeatureAbilityFactory } from '.'; +import { AbilityGuard } from '@modules/app/guards/ability.guard'; +import { App } from '@entities/app.entity'; +import { ResourceDetails } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; + +@Injectable() +export class FeatureAbilityGuard extends AbilityGuard { + protected getResource(): ResourceDetails { + return { + resourceType: MODULES.APP_PERMISSIONS, + }; + } + protected getAbilityFactory() { + return FeatureAbilityFactory; + } + + protected getSubjectType() { + return App; + } + + protected forwardAbility(): boolean { + return true; + } +} diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts new file mode 100644 index 0000000000..c473903711 --- /dev/null +++ b/server/src/modules/app-permissions/ability/index.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { Ability, AbilityBuilder, InferSubjects } from '@casl/ability'; +import { AbilityFactory } from '@modules/app/ability-factory'; +import { UserAllPermissions } from '@modules/app/types'; +import { FEATURE_KEY } from '../constants'; +import { App } from '@entities/app.entity'; +import { MODULES } from '@modules/app/constants/modules'; + +type Subjects = InferSubjects | 'all'; +export type FeatureAbility = Ability<[FEATURE_KEY, Subjects]>; + +@Injectable() +export class FeatureAbilityFactory extends AbilityFactory { + protected getSubjectType() { + return App; + } + + protected defineAbilityFor( + can: AbilityBuilder['can'], + UserAllPermissions: UserAllPermissions, + extractedMetadata: { moduleName: string; features: string[] }, + request?: any + ): void { + const appId = request?.tj_resource_id; + const { superAdmin, isAdmin, userPermission } = UserAllPermissions; + + const userAppPermissions = userPermission?.[MODULES.APP]; + const isAllAppsEditable = !!userAppPermissions?.isAllEditable; + + if (isAdmin || superAdmin) { + // Admin or super admin and do all operations + can([FEATURE_KEY.FETCH_USERS], App); + return; + } + + if ( + isAllAppsEditable || + (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) + ) { + can([FEATURE_KEY.FETCH_USERS], App); + } + } +} diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts new file mode 100644 index 0000000000..8f53ed8a49 --- /dev/null +++ b/server/src/modules/app-permissions/constants/features.ts @@ -0,0 +1,9 @@ +import { FEATURE_KEY } from './index'; +import { MODULES } from '@modules/app/constants/modules'; +import { FeaturesConfig } from '../types'; + +export const FEATURES: FeaturesConfig = { + [MODULES.APP_PERMISSIONS]: { + [FEATURE_KEY.FETCH_USERS]: {}, + }, +}; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts new file mode 100644 index 0000000000..67397f38a4 --- /dev/null +++ b/server/src/modules/app-permissions/constants/index.ts @@ -0,0 +1,3 @@ +export enum FEATURE_KEY { + FETCH_USERS = 'fetch_users', +} diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts new file mode 100644 index 0000000000..f0a402cad3 --- /dev/null +++ b/server/src/modules/app-permissions/controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, NotFoundException, Param, Res, UseGuards } from '@nestjs/common'; +import { Response } from 'express'; +import { User } from '@modules/app/decorators/user.decorator'; +import { IAppPermissionsController } from './interfaces/IController'; +import { FeatureAbilityGuard } from './ability/guard'; +import { InitModule } from '@modules/app/decorators/init-module'; +import { MODULES } from '@modules/app/constants/modules'; +import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; +import { FEATURE_KEY } from './constants'; +import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; + +@InitModule(MODULES.APP_PERMISSIONS) +@UseGuards(JwtAuthGuard, FeatureAbilityGuard) +@Controller('app-permissions') +export class AppPermissionsController implements IAppPermissionsController { + constructor() {} + + @InitFeature(FEATURE_KEY.FETCH_USERS) + @Get(':appId/pages/users') + async fetchUsers( + @User() user, + @Param('appId') appId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } +} diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts new file mode 100644 index 0000000000..e758fcf1ab --- /dev/null +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -0,0 +1,6 @@ +import { User } from '@entities/user.entity'; +import { Response } from 'express'; + +export interface IAppPermissionsController { + fetchUsers(user: User, appId: string, response: Response): Promise; +} diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts new file mode 100644 index 0000000000..6f072faef9 --- /dev/null +++ b/server/src/modules/app-permissions/interfaces/IService.ts @@ -0,0 +1,3 @@ +export interface IAppPermissionsService { + fetchUsers(appId: string): Promise; +} diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts new file mode 100644 index 0000000000..40011b0292 --- /dev/null +++ b/server/src/modules/app-permissions/module.ts @@ -0,0 +1,19 @@ +import { getImportPath } from '@modules/app/constants'; +import { DynamicModule } from '@nestjs/common'; +import { FeatureAbilityFactory } from './ability'; + +export class AppPermissionsModule { + static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { + const importPath = await getImportPath(configs.IS_GET_CONTEXT); + const { AppPermissionsController } = await import(`${importPath}/app-permissions/controller`); + const { AppPermissionsService } = await import(`${importPath}/app-permissions/service`); + + return { + module: AppPermissionsModule, + imports: [], + controllers: [AppPermissionsController], + providers: [AppPermissionsService, FeatureAbilityFactory], + exports: [], + }; + } +} diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts new file mode 100644 index 0000000000..7744982418 --- /dev/null +++ b/server/src/modules/app-permissions/service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { IAppPermissionsService } from './interfaces/IService'; + +@Injectable() +export class AppPermissionsService implements IAppPermissionsService { + constructor() {} + + async fetchUsers(appId) { + throw new Error('Method not implemented.'); + } +} diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts new file mode 100644 index 0000000000..d1f1760948 --- /dev/null +++ b/server/src/modules/app-permissions/types/index.ts @@ -0,0 +1,11 @@ +import { FEATURE_KEY } from '../constants'; +import { FeatureConfig } from '@modules/app/types'; +import { MODULES } from '@modules/app/constants/modules'; + +interface Features { + [FEATURE_KEY.FETCH_USERS]: FeatureConfig; +} + +export interface FeaturesConfig { + [MODULES.APP_PERMISSIONS]: Features; +} diff --git a/server/src/modules/app/constants/module-info.ts b/server/src/modules/app/constants/module-info.ts index 27dceb7b3c..9131a8ba7a 100644 --- a/server/src/modules/app/constants/module-info.ts +++ b/server/src/modules/app/constants/module-info.ts @@ -34,6 +34,7 @@ import { FEATURES as AI_FEATURES } from '@modules/ai/constants/feature'; import { getTooljetEdition } from '@helpers/utils.helper'; import { TOOLJET_EDITIONS } from '.'; import { FEATURES as WHITE_LABELLING_FEATURES } from '@modules/white-labelling/constant/feature'; +import { FEATURES as APP_PERMISSIONS_FEATURES } from '@modules/app-permissions/constants/features'; const GROUP_PERMISSIONS_FEATURES = getTooljetEdition() === TOOLJET_EDITIONS.EE ? GROUP_PERMISSIONS_FEATURES_EE : GROUP_PERMISSIONS_FEATURES_CE; @@ -73,4 +74,5 @@ export const MODULE_INFO: { [key: string]: any } = { ...ORGANIZATION_CONSTANT, ...AI_FEATURES, ...WHITE_LABELLING_FEATURES, + ...APP_PERMISSIONS_FEATURES, }; diff --git a/server/src/modules/app/constants/modules.ts b/server/src/modules/app/constants/modules.ts index d3a04367ab..62af8b6ab0 100644 --- a/server/src/modules/app/constants/modules.ts +++ b/server/src/modules/app/constants/modules.ts @@ -36,4 +36,5 @@ export enum MODULES { IMPORT_EXPORT_RESOURCES = 'ImportExportResources', TEMPLATES = 'Templates', AI = 'ai', + APP_PERMISSIONS = 'AppPermissions', } diff --git a/server/src/modules/app/module.ts b/server/src/modules/app/module.ts index c0ca97e4be..00cf27f6a2 100644 --- a/server/src/modules/app/module.ts +++ b/server/src/modules/app/module.ts @@ -41,6 +41,7 @@ import { TooljetDbModule } from '@modules/tooljet-db/module'; import { WorkflowsModule } from '@modules/workflows/module'; import { AiModule } from '@modules/ai/module'; import { CustomStylesModule } from '@modules/custom-styles/module'; +import { AppPermissionsModule } from '@modules/app-permissions/module'; export class AppModule implements OnModuleInit { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -94,6 +95,7 @@ export class AppModule implements OnModuleInit { await WorkflowsModule.register(configs), await AiModule.register(configs), await CustomStylesModule.register(configs), + await AppPermissionsModule.register(configs), ]; return { From 4b4cae5b832a0b3f849c71b0c405ce6890691908 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 00:41:49 +0530 Subject: [PATCH 14/43] Added API to get users for an app with view access --- .../modules/app-permissions/interfaces/IService.ts | 4 +++- .../app-permissions/interfaces/IUtilService.ts | 5 +++++ server/src/modules/app-permissions/module.ts | 10 +++++++--- .../app-permissions/repositories/repository.ts | 0 server/src/modules/app-permissions/service.ts | 2 +- server/src/modules/app-permissions/util.service.ts | 12 ++++++++++++ 6 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 server/src/modules/app-permissions/interfaces/IUtilService.ts create mode 100644 server/src/modules/app-permissions/repositories/repository.ts create mode 100644 server/src/modules/app-permissions/util.service.ts diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts index 6f072faef9..750b33ea9f 100644 --- a/server/src/modules/app-permissions/interfaces/IService.ts +++ b/server/src/modules/app-permissions/interfaces/IService.ts @@ -1,3 +1,5 @@ +import { User } from '@entities/user.entity'; + export interface IAppPermissionsService { - fetchUsers(appId: string): Promise; + fetchUsers(appId: string, user: User): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts new file mode 100644 index 0000000000..12cacfe06e --- /dev/null +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -0,0 +1,5 @@ +import { User } from '@entities/user.entity'; + +export interface IUtilService { + getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise; +} diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index 40011b0292..aadb998dc6 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -1,19 +1,23 @@ import { getImportPath } from '@modules/app/constants'; import { DynamicModule } from '@nestjs/common'; import { FeatureAbilityFactory } from './ability'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@entities/user.entity'; +import { RolesRepository } from '@modules/roles/repository'; export class AppPermissionsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs.IS_GET_CONTEXT); const { AppPermissionsController } = await import(`${importPath}/app-permissions/controller`); const { AppPermissionsService } = await import(`${importPath}/app-permissions/service`); + const { AppPermissionsUtilService } = await import(`${importPath}/app-permissions/util.service`); return { module: AppPermissionsModule, - imports: [], + imports: [TypeOrmModule.forFeature([User])], controllers: [AppPermissionsController], - providers: [AppPermissionsService, FeatureAbilityFactory], - exports: [], + providers: [AppPermissionsService, AppPermissionsUtilService, RolesRepository, FeatureAbilityFactory], + exports: [AppPermissionsUtilService], }; } } diff --git a/server/src/modules/app-permissions/repositories/repository.ts b/server/src/modules/app-permissions/repositories/repository.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts index 7744982418..db7a61d98b 100644 --- a/server/src/modules/app-permissions/service.ts +++ b/server/src/modules/app-permissions/service.ts @@ -5,7 +5,7 @@ import { IAppPermissionsService } from './interfaces/IService'; export class AppPermissionsService implements IAppPermissionsService { constructor() {} - async fetchUsers(appId) { + async fetchUsers(appId, user) { throw new Error('Method not implemented.'); } } diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts new file mode 100644 index 0000000000..bb07699714 --- /dev/null +++ b/server/src/modules/app-permissions/util.service.ts @@ -0,0 +1,12 @@ +import { User } from '@entities/user.entity'; +import { IUtilService } from './interfaces/IUtilService'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppPermissionsUtilService implements IUtilService { + constructor() {} + + async getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise { + throw new Error('Method not implemented.'); + } +} From fc3669af741b62f1f56a6a3b0294e501cedec733 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 03:38:31 +0530 Subject: [PATCH 15/43] Added API to get user groups for an app with view access --- server/src/modules/app-permissions/ability/index.ts | 2 ++ .../src/modules/app-permissions/constants/features.ts | 1 + server/src/modules/app-permissions/constants/index.ts | 1 + server/src/modules/app-permissions/controller.ts | 10 ++++++++++ .../modules/app-permissions/interfaces/IController.ts | 2 ++ .../src/modules/app-permissions/interfaces/IService.ts | 2 ++ .../modules/app-permissions/interfaces/IUtilService.ts | 3 +++ server/src/modules/app-permissions/module.ts | 3 ++- server/src/modules/app-permissions/service.ts | 4 ++++ server/src/modules/app-permissions/types/index.ts | 1 + server/src/modules/app-permissions/util.service.ts | 5 +++++ 11 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts index c473903711..e9f5a0a270 100644 --- a/server/src/modules/app-permissions/ability/index.ts +++ b/server/src/modules/app-permissions/ability/index.ts @@ -30,6 +30,7 @@ export class FeatureAbilityFactory extends AbilityFactory if (isAdmin || superAdmin) { // Admin or super admin and do all operations can([FEATURE_KEY.FETCH_USERS], App); + can([FEATURE_KEY.FETCH_USER_GROUPS], App); return; } @@ -38,6 +39,7 @@ export class FeatureAbilityFactory extends AbilityFactory (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) ) { can([FEATURE_KEY.FETCH_USERS], App); + can([FEATURE_KEY.FETCH_USER_GROUPS], App); } } } diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts index 8f53ed8a49..a6ec529c26 100644 --- a/server/src/modules/app-permissions/constants/features.ts +++ b/server/src/modules/app-permissions/constants/features.ts @@ -5,5 +5,6 @@ import { FeaturesConfig } from '../types'; export const FEATURES: FeaturesConfig = { [MODULES.APP_PERMISSIONS]: { [FEATURE_KEY.FETCH_USERS]: {}, + [FEATURE_KEY.FETCH_USER_GROUPS]: {}, }, }; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index 67397f38a4..dc0862e88c 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -1,3 +1,4 @@ export enum FEATURE_KEY { FETCH_USERS = 'fetch_users', + FETCH_USER_GROUPS = 'fetch_user_groups', } diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts index f0a402cad3..3a301cf732 100644 --- a/server/src/modules/app-permissions/controller.ts +++ b/server/src/modules/app-permissions/controller.ts @@ -24,4 +24,14 @@ export class AppPermissionsController implements IAppPermissionsController { ): Promise { throw new NotFoundException(); } + + @InitFeature(FEATURE_KEY.FETCH_USER_GROUPS) + @Get(':appId/pages/user-groups') + async fetchUserGroups( + @User() user, + @Param('appId') appId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts index e758fcf1ab..615873e1ed 100644 --- a/server/src/modules/app-permissions/interfaces/IController.ts +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -3,4 +3,6 @@ import { Response } from 'express'; export interface IAppPermissionsController { fetchUsers(user: User, appId: string, response: Response): Promise; + + fetchUserGroups(user: User, appId: string, response: Response): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts index 750b33ea9f..8d6ee9eee5 100644 --- a/server/src/modules/app-permissions/interfaces/IService.ts +++ b/server/src/modules/app-permissions/interfaces/IService.ts @@ -2,4 +2,6 @@ import { User } from '@entities/user.entity'; export interface IAppPermissionsService { fetchUsers(appId: string, user: User): Promise; + + fetchUserGroups(appId: string, user: User): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index 12cacfe06e..b453efd251 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -1,5 +1,8 @@ import { User } from '@entities/user.entity'; +import { GroupPermissions } from '@entities/group_permissions.entity'; export interface IUtilService { getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise; + + getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise; } diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index aadb998dc6..b0cca61062 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -2,6 +2,7 @@ import { getImportPath } from '@modules/app/constants'; import { DynamicModule } from '@nestjs/common'; import { FeatureAbilityFactory } from './ability'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { GroupPermissions } from '@entities/group_permissions.entity'; import { User } from '@entities/user.entity'; import { RolesRepository } from '@modules/roles/repository'; @@ -14,7 +15,7 @@ export class AppPermissionsModule { return { module: AppPermissionsModule, - imports: [TypeOrmModule.forFeature([User])], + imports: [TypeOrmModule.forFeature([GroupPermissions, User])], controllers: [AppPermissionsController], providers: [AppPermissionsService, AppPermissionsUtilService, RolesRepository, FeatureAbilityFactory], exports: [AppPermissionsUtilService], diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts index db7a61d98b..e97a39fb84 100644 --- a/server/src/modules/app-permissions/service.ts +++ b/server/src/modules/app-permissions/service.ts @@ -8,4 +8,8 @@ export class AppPermissionsService implements IAppPermissionsService { async fetchUsers(appId, user) { throw new Error('Method not implemented.'); } + + async fetchUserGroups(appId, user) { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts index d1f1760948..ecced6b47c 100644 --- a/server/src/modules/app-permissions/types/index.ts +++ b/server/src/modules/app-permissions/types/index.ts @@ -4,6 +4,7 @@ import { MODULES } from '@modules/app/constants/modules'; interface Features { [FEATURE_KEY.FETCH_USERS]: FeatureConfig; + [FEATURE_KEY.FETCH_USER_GROUPS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index bb07699714..2705e60668 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -1,11 +1,16 @@ import { User } from '@entities/user.entity'; import { IUtilService } from './interfaces/IUtilService'; import { Injectable } from '@nestjs/common'; +import { GroupPermissions } from '@entities/group_permissions.entity'; @Injectable() export class AppPermissionsUtilService implements IUtilService { constructor() {} + async getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise { + throw new Error('Method not implemented.'); + } + async getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise { throw new Error('Method not implemented.'); } From 87fa9e71a1aab1cd5aa3393d99d3dfb9d444152b Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 11:11:39 +0530 Subject: [PATCH 16/43] Added entities for page_permissions and page_users tables --- .../src/entities/group_permissions.entity.ts | 4 +++ server/src/entities/page.entity.ts | 4 +++ .../src/entities/page_permissions.entity.ts | 33 ++++++++++++++++++ server/src/entities/page_users.entity.ts | 34 +++++++++++++++++++ server/src/entities/user.entity.ts | 4 +++ 5 files changed, 79 insertions(+) create mode 100644 server/src/entities/page_permissions.entity.ts create mode 100644 server/src/entities/page_users.entity.ts diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 089b7ff7d9..693f4f930c 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -13,6 +13,7 @@ import { Organization } from './organization.entity'; import { GroupUsers } from './group_users.entity'; import { GranularPermissions } from './granular_permissions.entity'; import { GROUP_PERMISSIONS_TYPE } from '@modules/group-permissions/constants'; +import { PageUser } from './page_users.entity'; @Entity({ name: 'permission_groups' }) export class GroupPermissions extends BaseEntity { @@ -62,5 +63,8 @@ export class GroupPermissions extends BaseEntity { @OneToMany(() => GranularPermissions, (granularPermissions) => granularPermissions.group, { onDelete: 'CASCADE' }) groupGranularPermissions: GranularPermissions[]; + @OneToMany(() => PageUser, (pageUser) => pageUser.permissionGroup) + pageUsers: PageUser[]; + disabled?: boolean; } diff --git a/server/src/entities/page.entity.ts b/server/src/entities/page.entity.ts index ca4e06333e..4b3dc5466e 100644 --- a/server/src/entities/page.entity.ts +++ b/server/src/entities/page.entity.ts @@ -10,6 +10,7 @@ import { } from 'typeorm'; import { AppVersion } from './app_version.entity'; import { Component } from './component.entity'; +import { PagePermission } from './page_permissions.entity'; @Entity({ name: 'pages' }) export class Page { @@ -61,4 +62,7 @@ export class Page { @OneToMany(() => Component, (component) => component.page) components: Component[]; + + @OneToMany(() => PagePermission, (permission) => permission.page) + permissions: PagePermission[]; } diff --git a/server/src/entities/page_permissions.entity.ts b/server/src/entities/page_permissions.entity.ts new file mode 100644 index 0000000000..877afc5e29 --- /dev/null +++ b/server/src/entities/page_permissions.entity.ts @@ -0,0 +1,33 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm'; +import { Page } from './page.entity'; +import { PageUser } from './page_users.entity'; + +export enum PermissionType { + SINGLE = 'SINGLE', + GROUP = 'GROUP', +} + +@Entity('page_permissions') +export class PagePermission { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'page_id', type: 'uuid', nullable: false }) + pageId: string; + + @Column({ + type: 'enum', + enum: PermissionType, + }) + type: PermissionType; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => Page, (page) => page.permissions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'page_id' }) + page: Page; + + @OneToMany(() => PageUser, (pageUser) => pageUser.pagePermission) + users: PageUser[]; +} diff --git a/server/src/entities/page_users.entity.ts b/server/src/entities/page_users.entity.ts new file mode 100644 index 0000000000..960be5b32f --- /dev/null +++ b/server/src/entities/page_users.entity.ts @@ -0,0 +1,34 @@ +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; +import { User } from './user.entity'; +import { PagePermission } from './page_permissions.entity'; +import { GroupPermissions } from './group_permissions.entity'; + +@Entity('page_users') +export class PageUser { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'page_permissions_id', type: 'uuid' }) + pagePermissionsId: string; + + @Column({ name: 'user_id', type: 'uuid', nullable: true }) + userId: string | null; + + @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) + permissionGroupsId: string | null; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @ManyToOne(() => PagePermission, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'page_permissions_id' }) + pagePermission: PagePermission; + + @ManyToOne(() => User, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + + @ManyToOne(() => GroupPermissions, { onDelete: 'CASCADE', nullable: true }) + @JoinColumn({ name: 'permission_groups_id' }) + permissionGroup: GroupPermissions; +} diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 5bdecb2b43..e052e11245 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -29,6 +29,7 @@ import { OnboardingStatus } from '@modules/onboarding/constants'; import { AiConversation } from './ai_conversation.entity'; import { AiResponseVote } from './ai_response_vote.entity'; import { USER_ROLE } from '@modules/group-permissions/constants'; +import { PageUser } from './page_users.entity'; @Entity({ name: 'users' }) export class User extends BaseEntity { @@ -184,6 +185,9 @@ export class User extends BaseEntity { @OneToMany(() => AiResponseVote, (aiResponseVote) => aiResponseVote.user, { onDelete: 'CASCADE' }) aiResponseVotes: AiResponseVote[]; + @OneToMany(() => PageUser, (pageUser) => pageUser.user) + pageUsers: PageUser[]; + organizationId: string; invitedOrganizationId: string; organizationIds?: Array; From 6923d7ef156ec3ba3a561dc5916b891eb8604262 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 16:53:41 +0530 Subject: [PATCH 17/43] Created base for APIs related to page permission endpoints and DTO --- .../src/entities/page_permissions.entity.ts | 10 ++-- .../app-permissions/constants/features.ts | 4 ++ .../app-permissions/constants/index.ts | 9 ++++ .../src/modules/app-permissions/controller.ts | 49 ++++++++++++++++++- .../src/modules/app-permissions/dto/index.ts | 26 ++++++++++ .../app-permissions/interfaces/IController.ts | 21 ++++++++ .../app-permissions/interfaces/IService.ts | 9 ++++ .../interfaces/IUtilService.ts | 5 ++ .../repositories/repository.ts | 0 server/src/modules/app-permissions/service.ts | 16 ++++++ .../modules/app-permissions/types/index.ts | 4 ++ .../modules/app-permissions/util.service.ts | 9 ++++ 12 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 server/src/modules/app-permissions/dto/index.ts delete mode 100644 server/src/modules/app-permissions/repositories/repository.ts diff --git a/server/src/entities/page_permissions.entity.ts b/server/src/entities/page_permissions.entity.ts index 877afc5e29..7d265b696b 100644 --- a/server/src/entities/page_permissions.entity.ts +++ b/server/src/entities/page_permissions.entity.ts @@ -1,11 +1,7 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, OneToMany } from 'typeorm'; import { Page } from './page.entity'; import { PageUser } from './page_users.entity'; - -export enum PermissionType { - SINGLE = 'SINGLE', - GROUP = 'GROUP', -} +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; @Entity('page_permissions') export class PagePermission { @@ -17,9 +13,9 @@ export class PagePermission { @Column({ type: 'enum', - enum: PermissionType, + enum: PAGE_PERMISSION_TYPE, }) - type: PermissionType; + type: PAGE_PERMISSION_TYPE; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; diff --git a/server/src/modules/app-permissions/constants/features.ts b/server/src/modules/app-permissions/constants/features.ts index a6ec529c26..6d77625ec5 100644 --- a/server/src/modules/app-permissions/constants/features.ts +++ b/server/src/modules/app-permissions/constants/features.ts @@ -6,5 +6,9 @@ export const FEATURES: FeaturesConfig = { [MODULES.APP_PERMISSIONS]: { [FEATURE_KEY.FETCH_USERS]: {}, [FEATURE_KEY.FETCH_USER_GROUPS]: {}, + [FEATURE_KEY.FETCH_PAGE_PERMISSIONS]: {}, + [FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: {}, + [FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: {}, + [FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: {}, }, }; diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index dc0862e88c..ef0b13f325 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -1,4 +1,13 @@ +export enum PAGE_PERMISSION_TYPE { + SINGLE = 'SINGLE', + GROUP = 'GROUP', +} + export enum FEATURE_KEY { FETCH_USERS = 'fetch_users', FETCH_USER_GROUPS = 'fetch_user_groups', + FETCH_PAGE_PERMISSIONS = 'fetch_page_permissions', + CREATE_PAGE_PERMISSIONS = 'create_page_permissions', + UPDATE_PAGE_PERMISSIONS = 'update_page_permissions', + DELETE_PAGE_PERMISSIONS = 'delete_page_permissions', } diff --git a/server/src/modules/app-permissions/controller.ts b/server/src/modules/app-permissions/controller.ts index 3a301cf732..2d0ccea9ce 100644 --- a/server/src/modules/app-permissions/controller.ts +++ b/server/src/modules/app-permissions/controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, NotFoundException, Param, Res, UseGuards } from '@nestjs/common'; +import { Body, Controller, Delete, Get, NotFoundException, Param, Post, Put, Res, UseGuards } from '@nestjs/common'; import { Response } from 'express'; import { User } from '@modules/app/decorators/user.decorator'; import { IAppPermissionsController } from './interfaces/IController'; @@ -8,6 +8,7 @@ import { MODULES } from '@modules/app/constants/modules'; import { InitFeature } from '@modules/app/decorators/init-feature.decorator'; import { FEATURE_KEY } from './constants'; import { JwtAuthGuard } from '@modules/session/guards/jwt-auth.guard'; +import { CreatePagePermissionDto } from './dto'; @InitModule(MODULES.APP_PERMISSIONS) @UseGuards(JwtAuthGuard, FeatureAbilityGuard) @@ -34,4 +35,50 @@ export class AppPermissionsController implements IAppPermissionsController { ): Promise { throw new NotFoundException(); } + + @InitFeature(FEATURE_KEY.FETCH_PAGE_PERMISSIONS) + @Get(':appId/pages/:pageId') + async fetchPagePermissions( + @User() user, + @Param('appId') appId: string, + @Param('pageId') pageId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.CREATE_PAGE_PERMISSIONS) + @Post(':appId/pages/:pageId') + async createPagePermissions( + @User() user, + @Param('appId') appId: string, + @Param('pageId') pageId: string, + @Body() body: CreatePagePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.UPDATE_PAGE_PERMISSIONS) + @Put(':appId/pages/:pageId') + async updatePagePermissions( + @User() user, + @Param('appId') appId: string, + @Param('pageId') pageId: string, + @Body() body: CreatePagePermissionDto, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } + + @InitFeature(FEATURE_KEY.DELETE_PAGE_PERMISSIONS) + @Delete(':appId/pages/:pageId') + async deletePagePermissions( + @User() user, + @Param('appId') appId: string, + @Param('pageId') pageId: string, + @Res({ passthrough: true }) response: Response + ): Promise { + throw new NotFoundException(); + } } diff --git a/server/src/modules/app-permissions/dto/index.ts b/server/src/modules/app-permissions/dto/index.ts new file mode 100644 index 0000000000..20a1bd98b8 --- /dev/null +++ b/server/src/modules/app-permissions/dto/index.ts @@ -0,0 +1,26 @@ +import { IsUUID, IsEnum, IsArray, IsString, IsOptional, ValidateIf } from 'class-validator'; +import { Type } from 'class-transformer'; +import { PAGE_PERMISSION_TYPE } from '../constants'; + +export class CreatePagePermissionDto { + @IsUUID(4) + @IsOptional() + pageId: string; + + @IsEnum(PAGE_PERMISSION_TYPE) + type: PAGE_PERMISSION_TYPE; + + @ValidateIf((o) => o.type === PAGE_PERMISSION_TYPE.SINGLE) + @IsArray() + @IsString({ each: true }) + @IsOptional() + @Type(() => String) + users?: string[]; + + @ValidateIf((o) => o.type === PAGE_PERMISSION_TYPE.GROUP) + @IsArray() + @IsString({ each: true }) + @IsOptional() + @Type(() => String) + groups?: string[]; +} diff --git a/server/src/modules/app-permissions/interfaces/IController.ts b/server/src/modules/app-permissions/interfaces/IController.ts index 615873e1ed..bfa35aa730 100644 --- a/server/src/modules/app-permissions/interfaces/IController.ts +++ b/server/src/modules/app-permissions/interfaces/IController.ts @@ -1,8 +1,29 @@ import { User } from '@entities/user.entity'; import { Response } from 'express'; +import { CreatePagePermissionDto } from '../dto'; export interface IAppPermissionsController { fetchUsers(user: User, appId: string, response: Response): Promise; fetchUserGroups(user: User, appId: string, response: Response): Promise; + + fetchPagePermissions(user: User, appId: string, pageId: string, response: Response): Promise; + + createPagePermissions( + user: User, + appId: string, + pageId: string, + body: CreatePagePermissionDto, + response: Response + ): Promise; + + updatePagePermissions( + user: User, + appId: string, + pageId: string, + body: CreatePagePermissionDto, + response: Response + ): Promise; + + deletePagePermissions(user: User, appId: string, pageId: string, response: Response): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IService.ts b/server/src/modules/app-permissions/interfaces/IService.ts index 8d6ee9eee5..cad5fef726 100644 --- a/server/src/modules/app-permissions/interfaces/IService.ts +++ b/server/src/modules/app-permissions/interfaces/IService.ts @@ -1,7 +1,16 @@ import { User } from '@entities/user.entity'; +import { CreatePagePermissionDto } from '../dto'; export interface IAppPermissionsService { fetchUsers(appId: string, user: User): Promise; fetchUserGroups(appId: string, user: User): Promise; + + fetchPagePermissions(pageId: string): Promise; + + createPagePermissions(pageId: string, body: CreatePagePermissionDto): Promise; + + updatePagePermissions(appId: string, pageId: string, body: CreatePagePermissionDto, user: User): Promise; + + deletePagePermissions(pageId: string): Promise; } diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index b453efd251..0d2be25315 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -1,8 +1,13 @@ import { User } from '@entities/user.entity'; import { GroupPermissions } from '@entities/group_permissions.entity'; +import { CreatePagePermissionDto } from '../dto'; export interface IUtilService { getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise; getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise; + + createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise; + + updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise; } diff --git a/server/src/modules/app-permissions/repositories/repository.ts b/server/src/modules/app-permissions/repositories/repository.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/src/modules/app-permissions/service.ts b/server/src/modules/app-permissions/service.ts index e97a39fb84..c6b5bc640d 100644 --- a/server/src/modules/app-permissions/service.ts +++ b/server/src/modules/app-permissions/service.ts @@ -12,4 +12,20 @@ export class AppPermissionsService implements IAppPermissionsService { async fetchUserGroups(appId, user) { throw new Error('Method not implemented.'); } + + async fetchPagePermissions(pageId) { + throw new Error('Method not implemented.'); + } + + async createPagePermissions(pageId, body) { + throw new Error('Method not implemented.'); + } + + async updatePagePermissions(appId, pageId, body, user) { + throw new Error('Method not implemented.'); + } + + async deletePagePermissions(pageId) { + throw new Error('Method not implemented.'); + } } diff --git a/server/src/modules/app-permissions/types/index.ts b/server/src/modules/app-permissions/types/index.ts index ecced6b47c..86a41afba1 100644 --- a/server/src/modules/app-permissions/types/index.ts +++ b/server/src/modules/app-permissions/types/index.ts @@ -5,6 +5,10 @@ import { MODULES } from '@modules/app/constants/modules'; interface Features { [FEATURE_KEY.FETCH_USERS]: FeatureConfig; [FEATURE_KEY.FETCH_USER_GROUPS]: FeatureConfig; + [FEATURE_KEY.FETCH_PAGE_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.CREATE_PAGE_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.UPDATE_PAGE_PERMISSIONS]: FeatureConfig; + [FEATURE_KEY.DELETE_PAGE_PERMISSIONS]: FeatureConfig; } export interface FeaturesConfig { diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index 2705e60668..377c693a26 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -2,6 +2,7 @@ import { User } from '@entities/user.entity'; import { IUtilService } from './interfaces/IUtilService'; import { Injectable } from '@nestjs/common'; import { GroupPermissions } from '@entities/group_permissions.entity'; +import { CreatePagePermissionDto } from './dto'; @Injectable() export class AppPermissionsUtilService implements IUtilService { @@ -14,4 +15,12 @@ export class AppPermissionsUtilService implements IUtilService { async getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise { throw new Error('Method not implemented.'); } + + async createPagePermission(pageId: string, body: CreatePagePermissionDto): Promise { + throw new Error('Method not implemented.'); + } + + async updatePagePermission(pageId: string, body: CreatePagePermissionDto): Promise { + throw new Error('Method not implemented.'); + } } From c3b71a32caadaf133406c8b0e0a2ffa4bb6ef67b Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 16:55:22 +0530 Subject: [PATCH 18/43] Created repositories to interact with page_permissions and page_users tables --- .../page-permissions.repository.ts | 42 +++++++++++++++++ .../repositories/page-users.repository.ts | 45 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 server/src/modules/app-permissions/repositories/page-permissions.repository.ts create mode 100644 server/src/modules/app-permissions/repositories/page-users.repository.ts diff --git a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts new file mode 100644 index 0000000000..89b5b190c1 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts @@ -0,0 +1,42 @@ +import { PagePermission } from '@entities/page_permissions.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { PageUsersRepository } from './page-users.repository'; +import { dbTransactionWrap } from '@helpers/database.helper'; +import { PAGE_PERMISSION_TYPE } from '../constants'; + +@Injectable() +export class PagePermissionsRepository extends Repository { + constructor(private dataSource: DataSource, private readonly pageUsersRepository: PageUsersRepository) { + super(PagePermission, dataSource.createEntityManager()); + } + + async getPagePermissions(pageId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + return manager.find(PagePermission, { + where: { pageId }, + relations: ['users', 'users.user'], + }); + }, manager || this.manager); + } + + async createPagePermissions( + pageId: string, + type: PAGE_PERMISSION_TYPE, + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const pagePermission = manager.create(PagePermission, { + pageId, + type, + }); + return manager.save(pagePermission); + }, manager || this.manager); + } + + async deletePagePermissions(pageId: string, manager?: EntityManager): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + await manager.delete(PagePermission, { pageId }); + }, manager || this.manager); + } +} diff --git a/server/src/modules/app-permissions/repositories/page-users.repository.ts b/server/src/modules/app-permissions/repositories/page-users.repository.ts new file mode 100644 index 0000000000..e70a9e1394 --- /dev/null +++ b/server/src/modules/app-permissions/repositories/page-users.repository.ts @@ -0,0 +1,45 @@ +import { PageUser } from '@entities/page_users.entity'; +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, Repository } from 'typeorm'; +import { dbTransactionWrap } from '@helpers/database.helper'; + +@Injectable() +export class PageUsersRepository extends Repository { + constructor(private dataSource: DataSource) { + super(PageUser, dataSource.createEntityManager()); + } + + async createPageUsersWithSingle( + pagePermissionsId: string, + users: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const pageUsers = users.map((userId) => { + return manager.create(PageUser, { + pagePermissionsId, + userId, + permissionGroupsId: null, + }); + }); + return manager.save(pageUsers); + }, manager || this.manager); + } + + async createPageUsersWithGroup( + pagePermissionsId: string, + groups: string[], + manager?: EntityManager + ): Promise { + return dbTransactionWrap(async (manager: EntityManager) => { + const pageUsers = groups.map((permissionGroupsId) => { + return manager.create(PageUser, { + pagePermissionsId, + permissionGroupsId, + userId: null, + }); + }); + return manager.save(pageUsers); + }, manager || this.manager); + } +} From dec1c1814c418c20317f0e909a224bb9431f09ed Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 16:56:07 +0530 Subject: [PATCH 19/43] Updated the module file with required repositories and entities --- server/src/modules/app-permissions/module.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server/src/modules/app-permissions/module.ts b/server/src/modules/app-permissions/module.ts index b0cca61062..e52082ed95 100644 --- a/server/src/modules/app-permissions/module.ts +++ b/server/src/modules/app-permissions/module.ts @@ -5,6 +5,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { GroupPermissions } from '@entities/group_permissions.entity'; import { User } from '@entities/user.entity'; import { RolesRepository } from '@modules/roles/repository'; +import { PageUsersRepository } from './repositories/page-users.repository'; +import { PagePermissionsRepository } from './repositories/page-permissions.repository'; +import { PageUser } from '@entities/page_users.entity'; +import { PagePermission } from '@entities/page_permissions.entity'; export class AppPermissionsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -15,9 +19,16 @@ export class AppPermissionsModule { return { module: AppPermissionsModule, - imports: [TypeOrmModule.forFeature([GroupPermissions, User])], + imports: [TypeOrmModule.forFeature([GroupPermissions, User, PageUser, PagePermission])], controllers: [AppPermissionsController], - providers: [AppPermissionsService, AppPermissionsUtilService, RolesRepository, FeatureAbilityFactory], + providers: [ + AppPermissionsService, + AppPermissionsUtilService, + RolesRepository, + PageUsersRepository, + PagePermissionsRepository, + FeatureAbilityFactory, + ], exports: [AppPermissionsUtilService], }; } From 54ffa99c3fe975d11dadbddbdd1a8daa505111c5 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 17 Apr 2025 16:57:18 +0530 Subject: [PATCH 20/43] Updated guard for accessing page permission features --- .../modules/app-permissions/ability/index.ts | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/server/src/modules/app-permissions/ability/index.ts b/server/src/modules/app-permissions/ability/index.ts index e9f5a0a270..d2e8c263b2 100644 --- a/server/src/modules/app-permissions/ability/index.ts +++ b/server/src/modules/app-permissions/ability/index.ts @@ -26,11 +26,21 @@ export class FeatureAbilityFactory extends AbilityFactory const userAppPermissions = userPermission?.[MODULES.APP]; const isAllAppsEditable = !!userAppPermissions?.isAllEditable; + const isAllAppsViewable = !!userAppPermissions?.isAllViewable; if (isAdmin || superAdmin) { // Admin or super admin and do all operations - can([FEATURE_KEY.FETCH_USERS], App); - can([FEATURE_KEY.FETCH_USER_GROUPS], App); + can( + [ + FEATURE_KEY.FETCH_USERS, + FEATURE_KEY.FETCH_USER_GROUPS, + FEATURE_KEY.FETCH_PAGE_PERMISSIONS, + FEATURE_KEY.CREATE_PAGE_PERMISSIONS, + FEATURE_KEY.UPDATE_PAGE_PERMISSIONS, + FEATURE_KEY.DELETE_PAGE_PERMISSIONS, + ], + App + ); return; } @@ -38,8 +48,25 @@ export class FeatureAbilityFactory extends AbilityFactory isAllAppsEditable || (userAppPermissions?.editableAppsId?.length && appId && userAppPermissions.editableAppsId.includes(appId)) ) { - can([FEATURE_KEY.FETCH_USERS], App); - can([FEATURE_KEY.FETCH_USER_GROUPS], App); + can( + [ + FEATURE_KEY.FETCH_USERS, + FEATURE_KEY.FETCH_USER_GROUPS, + FEATURE_KEY.FETCH_PAGE_PERMISSIONS, + FEATURE_KEY.CREATE_PAGE_PERMISSIONS, + FEATURE_KEY.UPDATE_PAGE_PERMISSIONS, + FEATURE_KEY.DELETE_PAGE_PERMISSIONS, + ], + App + ); + return; + } + + if ( + isAllAppsViewable || + (userAppPermissions?.viewableAppsId?.length && appId && userAppPermissions.viewableAppsId.includes(appId)) + ) { + can([FEATURE_KEY.FETCH_USERS, FEATURE_KEY.FETCH_USER_GROUPS, FEATURE_KEY.FETCH_PAGE_PERMISSIONS], App); } } } From 48ff73a912ecf949f11cdffa05728d3b02f3b555 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 17 Apr 2025 18:26:21 +0530 Subject: [PATCH 21/43] Fixes bug with slots not getting selected --- frontend/src/AppBuilder/Widgets/Form/Form.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index c674d4f84a..798ed56bcf 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -263,6 +263,9 @@ export const Form = function Form(props) { setChildrenData(childDataRef.current); }; + const mode = useStore((state) => state.currentMode, shallow); + const isEditing = mode === 'edit'; + const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); const updateHeaderSizeInStore = ({ newHeight }) => { @@ -275,8 +278,6 @@ export const Form = function Form(props) { setComponentProperty(id, `footerHeight`, _height, 'properties', 'value', false); }; - const mode = useStore((state) => state.currentMode, shallow); - const isEditing = mode === 'edit'; const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10; const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10; const formFooter = { From 1bb61854f08e90baaaedb302773b28cfd9d7201a Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:21:03 +0530 Subject: [PATCH 22/43] Fixes issues showing grid lines on canvas always --- frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js | 7 ------- frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx | 1 + 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index b18dd9d311..b6a801d863 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -420,20 +420,13 @@ export function showGridLinesOnSlot(slotId) { canvasElm.classList.remove('hide-grid'); canvasElm.classList.add('show-grid'); - - document.getElementById('real-canvas')?.classList.add('hide-grid'); - document.getElementById('real-canvas')?.classList.remove('show-grid'); } export function hideGridLinesOnSlot(slotId) { var canvasElm = document.getElementById(`canvas-${slotId}`); - canvasElm.classList.remove('show-grid'); canvasElm.classList.add('hide-grid'); - - document.getElementById('real-canvas')?.classList.remove('hide-grid'); - document.getElementById('real-canvas')?.classList.add('show-grid'); } // Track previously active elements for efficient cleanup diff --git a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx index 9507370e13..6610ae5fb4 100644 --- a/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx +++ b/frontend/src/AppBuilder/AppCanvas/RenderWidget.jsx @@ -33,6 +33,7 @@ const SHOULD_ADD_BOX_SHADOW_AND_VISIBILITY = [ 'Divider', 'VerticalDivider', 'Link', + 'Form', ]; const RenderWidget = ({ From 8c53ac4bab5a14bf26e3f3894e2e44d81b7c2eee Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Tue, 22 Apr 2025 09:27:02 +0530 Subject: [PATCH 23/43] feat: add crud operatios for page permissions --- .../editor/left-sidebar/authorization.svg | 3 + .../LeftSidebar/PageMenu/PageHandlerMenu.jsx | 12 + .../LeftSidebar/PageMenu/PageMenu.jsx | 2 + .../LeftSidebar/PageMenu/PagePermission.jsx | 377 +++++++++++++++ .../LeftSidebar/PageMenu/style.scss | 72 +++ frontend/src/AppBuilder/Viewer/PageGroup.jsx | 5 +- frontend/src/AppBuilder/_hooks/useAppData.js | 440 ++++++++++-------- .../AppBuilder/_stores/slices/eventsSlice.js | 4 + .../_stores/slices/pageMenuSlice.js | 27 +- frontend/src/_helpers/constants.js | 7 + frontend/src/_helpers/handleAppAccess.js | 6 +- .../src/_services/appPermission.service.js | 49 ++ frontend/src/_services/index.js | 1 + frontend/src/_ui/Modal/index.jsx | 4 +- server/ee | 2 +- server/src/modules/app-permissions/module.ts | 2 +- .../page-permissions.repository.ts | 2 +- .../repositories/page-users.repository.ts | 46 ++ server/src/modules/apps/module.ts | 2 + server/src/modules/workflows/module.ts | 2 + 20 files changed, 850 insertions(+), 215 deletions(-) create mode 100644 frontend/assets/images/icons/editor/left-sidebar/authorization.svg create mode 100644 frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx create mode 100644 frontend/src/_services/appPermission.service.js diff --git a/frontend/assets/images/icons/editor/left-sidebar/authorization.svg b/frontend/assets/images/icons/editor/left-sidebar/authorization.svg new file mode 100644 index 0000000000..609f7a5910 --- /dev/null +++ b/frontend/assets/images/icons/editor/left-sidebar/authorization.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index e2a395ecab..ec84149418 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -20,6 +20,8 @@ export const PageHandlerMenu = ({ darkMode }) => { const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal); const clonePage = useStore((state) => state.clonePage); const markAsHomePage = useStore((state) => state.markAsHomePage); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + // const popoverTargetRef = null; // console.log( // { @@ -164,6 +166,16 @@ export const PageHandlerMenu = ({ darkMode }) => { }} disabled={isHomePage} /> + { + togglePagePermissionModal(true); + }} + /> { const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); @@ -94,6 +95,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { >
+ diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx new file mode 100644 index 0000000000..ce7c78e6f3 --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -0,0 +1,377 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { components } from 'react-select'; +import ModalBase from '@/_ui/Modal'; +import Select from '@/_ui/Select'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; +import useStore from '@/AppBuilder/_stores/store'; +import { appPermissionService } from '@/_services'; +import { ConfirmDialog } from '@/_components'; +import toast from 'react-hot-toast'; + +const PERMISSION_TYPES = { + single: 'SINGLE', + group: 'GROUP', + all: 'ALL', +}; + +export default function PagePermission({ darkMode }) { + const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const editingPage = useStore((state) => state.editingPage); + const appId = useStore((state) => state.app.appId); + const selectedUserGroups = useStore((state) => state.selectedUserGroups); + const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); + const selectedUsers = useStore((state) => state.selectedUsers); + const setSelectedUsers = useStore((state) => state.setSelectedUsers); + const pagePermission = useStore((state) => state.pagePermission); + const setPagePermission = useStore((state) => state.setPagePermission); + + const [pagePermissionType, setPagePermissionType] = useState('all'); + const [showUserGroupSelect, toggleUserGroupSelect] = useState(false); + const [showUsersSelect, toggleUsersSelect] = useState(false); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + console.log({ editingPage, showUserGroupSelect }); + + useEffect(() => { + if (!editingPage?.id && !showPagePermissionModal) return; + const fetchPagePermission = () => { + appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { + if (data) { + if (data[0]) { + setPagePermissionType(data[0]?.type?.toLowerCase()); + setPagePermission(data); + toggleUserGroupSelect(true); + data?.length && + setSelectedUserGroups( + data[0]?.users?.map((user) => ({ + label: user?.permissionGroup?.name, + value: user?.permissionGroup?.id, + })) + ); + } + } + }); + }; + fetchPagePermission(); + }, [appId, editingPage, setPagePermission, setSelectedUserGroups, showPagePermissionModal]); + + const permissionTypeOptions = useMemo( + () => [ + { + label: 'All users with access to the app', + value: 'all', + icon: 'globe', + }, + { + label: 'Users', + value: 'single', + icon: 'user', + }, + { + label: 'User groups', + value: 'group', + icon: 'usergroup', + }, + ], + [] + ); + console.log({ pagePermission }); + const handlePermissionTypeChange = (value) => { + console.log({ value }); + switch (value) { + case 'group': { + toggleUserGroupSelect(true); + toggleUsersSelect(false); + setPagePermissionType('group'); + break; + } + case 'single': { + toggleUsersSelect(true); + toggleUserGroupSelect(false); + setPagePermissionType('single'); + break; + } + case 'all': { + toggleUsersSelect(false); + toggleUserGroupSelect(false); + setPagePermissionType('all'); + } + } + }; + + const handlePagePermissionModalClose = () => { + togglePagePermissionModal(false); + toggleUserGroupSelect(false); + toggleUsersSelect(false); + setPagePermissionType('all'); + setPagePermission(null); + }; + + const createPagePermission = () => { + const body = { + pageId: editingPage?.id, + type: PERMISSION_TYPES[pagePermissionType], + ...(pagePermissionType === 'group' + ? { groups: selectedUserGroups.map((group) => group?.value) } + : { users: selectedUsers.map((user) => user?.value) }), + }; + setIsLoading(true); + appPermissionService + .createPagePermission(appId, editingPage?.id, body) + .then((data) => { + console.log({ data }); + }) + .catch(() => { + toast.error('Permission could not be created. Please try again!'); + }) + .finally(() => { + setIsLoading(false); + handlePagePermissionModalClose(); + toast.success('Permission successfully created!'); + }); + }; + + const updatePagePermission = () => { + const body = { + pageId: editingPage?.id, + type: PERMISSION_TYPES[pagePermissionType], + ...(pagePermissionType === 'group' + ? { groups: selectedUserGroups.map((group) => group?.value) } + : { users: selectedUsers.map((user) => user?.value) }), + }; + setIsLoading(true); + appPermissionService + .updatePagePermission(appId, editingPage?.id, body) + .then((data) => { + console.log({ data }); + }) + .catch(() => { + toast.error('Permission could not be updated. Please try again!'); + }) + .finally(() => { + setIsLoading(false); + handlePagePermissionModalClose(); + toast.success('Permission successfully updated!'); + }); + }; + + const deletePagePermission = () => { + setIsLoading(true); + appPermissionService + .deletePagePermission(appId, editingPage?.id) + .then((data) => { + console.log({ data }); + }) + .catch(() => { + toast.error('Permission could not be deleted. Please try again!'); + }) + .finally(() => { + setIsLoading(false); + setShowConfirmDelete(false); + handlePagePermissionModalClose(); + toast.success('Permission successfully deleted!'); + }); + }; + + const renderPermissionTypeOptions = ({ label, icon }) => { + return ( +
+
+ +
+
+ {label} +
+
+ ); + }; + + return ( + <> + + Page permission +
+ } + handleConfirm={!pagePermission ? createPagePermission : updatePagePermission} + show={showPagePermissionModal} + isLoading={isLoading} + handleClose={handlePagePermissionModalClose} + confirmBtnProps={{ + title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission', + disabled: pagePermissionType == 'all' ? true : false, + tooltipMessage: '', + }} + darkMode={darkMode} + className="page-permissions-modal" + headerAction={() => + pagePermission && ( + { + togglePagePermissionModal(false); + setShowConfirmDelete(true); + }} + > + + + ) + } + > +
+
+
+ +
+
+
+

+ Only selected users will be allowed to access this page. Read docs to know more. +

+
+
+
+ + setSelectedUserGroups(groups)} + /> +
+ ); +}; + +const UserSelect = () => { + const appId = useStore((state) => state.app.appId); + const editingPage = useStore((state) => state.editingPage); + const selectedUsers = useStore((state) => state.selectedUsers); + const setSelectedUsers = useStore((state) => state.setSelectedUsers); + const [users, setUsers] = useState([]); + useEffect(() => { + const fetchUsers = () => { + appPermissionService.getUsers(appId, 'users').then((data) => { + console.log({ data }); + if (data?.length) { + const users = []; + data.map((user) => { + const firstName = user.firstName || ''; + const lastName = user.lastName || ''; + users.push({ + value: user.id, + label: `${firstName} ${lastName}`.trim(), + email: user.email, + initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(), + }); + }); + setUsers(users); + } + }); + }; + fetchUsers(); + }, []); + + const CustomOption = (props) => { + const { data, isFocused, isSelected } = props; + return ( + +
+
{data.initials}
+
+
{data.label}
+
{data.email}
+
+
+
+ ); + }; + + console.log({ users }); + + return ( +
+ + +
+
{data.label}
+
{data.count} users
+
+
+ + ); + }; return (
@@ -300,10 +334,12 @@ const UserGroupSelect = () => { options={userGroups} value={selectedUserGroups} width={'100%'} - // customOption={renderPermissionTypeOptions} + closeMenuOnSelect={false} + components={{ Option: CustomOption, MenuList: CustomMenuList }} useMenuPortal={false} - // menuIsOpen={true} + hideSelectedOptions={false} onChange={(groups) => setSelectedUserGroups(groups)} + info="Only user groups with access to this application can be selected" />
); @@ -318,7 +354,6 @@ const UserSelect = () => { useEffect(() => { const fetchUsers = () => { appPermissionService.getUsers(appId, 'users').then((data) => { - console.log({ data }); if (data?.length) { const users = []; data.map((user) => { @@ -343,6 +378,12 @@ const UserSelect = () => { return (
+
{data.initials}
{data.label}
@@ -353,8 +394,12 @@ const UserSelect = () => { ); }; - console.log({ users }); - + const selectStyles = { + option: (base) => ({ + ...base, + padding: '8px 0px', + }), + }; return (
@@ -363,15 +408,35 @@ const UserSelect = () => { options={users} value={selectedUsers} width={'100%'} - // customOption={renderUserSelectOptions} useMenuPortal={false} - components={{ Option: CustomOption }} - // menuIsOpen={true} + closeMenuOnSelect={false} + components={{ Option: CustomOption, MenuList: CustomMenuList }} + styles={selectStyles} + hideSelectedOptions={false} + info="Only user with access to this application can be selected" onChange={(users) => { - console.log({ userstemp: users }); setSelectedUsers(users); }} />
); }; + +const CustomMenuList = (props) => { + const { info } = props.selectProps; + return ( + +
+
+ +
+
+
+

{info}

+
+
+
+ {props.children} +
+ ); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index 203b68ec71..b3799e09ce 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -297,6 +297,14 @@ gap: 12px; } + .react-select__option { + padding: 8px 0px; + + input { + margin-right: 10px; + } + } + .user-select-option { display: flex; align-items: center; @@ -327,15 +335,32 @@ flex-direction: column; .name { - font-weight: 600; + font-weight: 500; font-size: 14px; color: var(--slate12); } .email { font-size: 12px; - color: var(--slate10); // gray-500 + color: var(--slate10); } } + + .group-info { + display: flex; + flex-direction: row; + gap: 8px; + + .name { + font-weight: 400; + font-size: 14px; + color: var(--slate12); + } + + .count { + font-size: 12px; + color: var(--slate9); + } + } } } \ No newline at end of file diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx index 4a47d350f4..120b5dfc68 100644 --- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx +++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx @@ -142,8 +142,7 @@ const RenderPageGroup = ({ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => { // Don't render empty folders if displaying only icons const tree = buildTree(pages, !!labelStyle?.label?.hidden); - const filteredPages = tree.filter((page) => !page?.isPageGroup || page.children?.length > 0); - + const filteredPages = tree.filter((page) => (!page?.isPageGroup || page.children?.length > 0) && !page?.restricted); const currentPageId = useStore((state) => state.currentPageId); const currentPage = pages.find((page) => page.id === currentPageId); const homePageId = useStore((state) => state.app.homePageId); diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index bbd4308403..d10e4343b9 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -329,25 +329,23 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v if (initialLoadRef.current) { // if initial load, check if the path has a page handle and set that as the starting page - const initialLoadPath = location.pathname.split('/')[3]; + const initialLoadPath = location.pathname.split('/').pop(); const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup); if (page) { // if page is disabled, and not editing redirect to home page - if (mode !== 'edit' && page?.disabled) { - const currentUrl = window.location.href; - const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle); - window.history.replaceState(null, null, replacedUrl); + const shouldRedirect = page?.restricted || (mode !== 'edit' && page?.disabled); + + if (shouldRedirect) { + const newUrl = window.location.href.replace(initialLoadPath, startingPage.handle); + window.history.replaceState(null, null, newUrl); + + if (page?.restricted) { + toast.error('Access to this page is restricted. Contact admin to know more.'); + } } else { startingPage = page; } - } else { - if (mode !== 'edit' && initialLoadPath) { - const currentUrl = window.location.href; - const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle); - window.history.replaceState(null, null, replacedUrl); - toast.error('Access to this page is restricted. Contact admin to know more.'); - } } // navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`); diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 60fbef8b52..f93f64b1c5 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -860,7 +860,9 @@ export const createEventsSlice = (set, get) => ({ const { switchPage } = get(); const page = get().modules.canvas.pages.find((page) => page.id === event.pageId); const queryParams = event.queryParams || []; - if (!page.disabled) { + if (page.restricted && mode !== 'edit') { + toast.error('Access to this page is restricted. Contact admin to know more.'); + } else if (!page.disabled) { const resolvedQueryParams = []; queryParams.forEach((param) => { resolvedQueryParams.push([ @@ -1118,10 +1120,6 @@ export const createEventsSlice = (set, get) => ({ toast('Valid page handle is required', { icon: '⚠️', }); - mode === 'view' && - toast.error('Access to this page is restricted. Contact admin to know more.', { - icon: '⚠️', - }); return Promise.resolve(); } diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js index ea17cc27e2..1fea8b7090 100644 --- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js @@ -444,5 +444,9 @@ export const createPageMenuSlice = (set, get) => { set((state) => { state.selectedUsers = users; }), + setEditingPage: (page) => + set((state) => { + state.editingPage = page; + }), }; }; diff --git a/server/ee b/server/ee index d14468b695..7d46f023ce 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit d14468b6954cc33616b02802c49db2deac6be105 +Subproject commit 7d46f023cea42533c4a8ff387dcc1149553c4671 diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 38b1ee1968..15b5903fb2 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -20,6 +20,7 @@ import { DataSourcesModule } from '@modules/data-sources/module'; import { AppsSubscriber } from './subscribers/apps.subscriber'; import { AiModule } from '@modules/ai/module'; import { AppPermissionsModule } from '@modules/app-permissions/module'; +import { RolesRepository } from '@modules/roles/repository'; @Module({}) export class AppsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -37,7 +38,15 @@ export class AppsModule { return { module: AppsModule, imports: [ - TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]), + TypeOrmModule.forFeature([ + App, + Page, + EventHandler, + Organization, + Component, + VersionRepository, + RolesRepository, + ]), await FolderAppsModule.register(configs), await ThemesModule.register(configs), await FoldersModule.register(configs), @@ -63,6 +72,7 @@ export class AppsModule { AppsSubscriber, DataSourcesRepository, AppImportExportService, + RolesRepository, ], exports: [AppsUtilService], }; diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 60c6f8bc15..81708667f9 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -18,7 +18,7 @@ import { VersionReleaseDto, } from './dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { APP_TYPES, FEATURE_KEY } from './constants'; +import { FEATURE_KEY } from './constants'; import { camelizeKeys, decamelizeKeys } from 'humps'; import { App } from '@entities/app.entity'; import { AppsUtilService } from './util.service'; diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 65721a264e..77dfbd0af3 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -30,6 +30,7 @@ import { App } from '@entities/app.entity'; import { AiModule } from '@modules/ai/module'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppPermissionsModule } from '@modules/app-permissions/module'; +import { RolesRepository } from '@modules/roles/repository'; export class WorkflowsModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs?.IS_GET_CONTEXT); @@ -70,6 +71,7 @@ export class WorkflowsModule { WorkflowExecutionNode, WorkflowExecutionNode, WorkflowExecutionEdge, + RolesRepository, ]), ThrottlerModule.forRootAsync({ imports: [ConfigModule], @@ -115,6 +117,7 @@ export class WorkflowsModule { WorkflowSchedulesService, TemporalService, FeatureAbilityFactory, + RolesRepository, ], controllers: [ WorkflowsController, From 30593d1f0599a8b019b895d73bf91979b68b1b0b Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Tue, 22 Apr 2025 22:49:09 +0530 Subject: [PATCH 32/43] Added toggle for showing 'All options selected' label in multiselect --- .../src/AppBuilder/WidgetManager/widgets/multiselectV2.js | 7 +++++++ .../Components/MultiselectV2/CustomValueContainer.jsx | 2 +- .../src/Editor/Components/MultiselectV2/MultiselectV2.jsx | 2 ++ frontend/src/Editor/WidgetManager/configs/multiselectV2.js | 7 +++++++ .../modules/apps/services/widget-config/multiselectV2.js | 7 +++++++ 5 files changed, 24 insertions(+), 1 deletion(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js index 4ab4af57ce..6ad8e6f7b5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js @@ -142,6 +142,12 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showAllSelectedLabel: { + type: 'toggle', + displayName: 'Show all options selected label', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +333,7 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showAllSelectedLabel: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx b/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx index 2901abc106..b648684fc2 100644 --- a/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx +++ b/frontend/src/Editor/Components/MultiselectV2/CustomValueContainer.jsx @@ -36,7 +36,7 @@ const CustomValueContainer = ({ children, ...props }) => { ) : ( - {isAllOptionsSelected ? 'All items are selected.' : values.join(', ')} + {selectProps?.showAllSelectedLabel && isAllOptionsSelected ? 'All items are selected.' : values.join(', ')} )} {/* Rendering children except Placeholder component to preserve the default behavior of react-select like focus diff --git a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx index 7d5109edee..1d9ab6bdc9 100644 --- a/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx +++ b/frontend/src/Editor/Components/MultiselectV2/MultiselectV2.jsx @@ -38,6 +38,7 @@ export const MultiselectV2 = ({ loadingState: multiSelectLoadingState, optionsLoadingState, sort, + showAllSelectedLabel, } = properties; const { selectedTextColor, @@ -521,6 +522,7 @@ export const MultiselectV2 = ({ doShowIcon={iconVisibility} containerRef={valueContainerRef} showAllOption={showAllOption} + showAllSelectedLabel={showAllSelectedLabel} isSelectAllSelected={isSelectAllSelected} setIsSelectAllSelected={(value) => { setIsSelectAllSelected(value); diff --git a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js index b603db9c4a..5bff71c29a 100644 --- a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js +++ b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js @@ -142,6 +142,12 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showAllSelectedLabel: { + type: 'toggle', + displayName: 'Show all options selected label', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +333,7 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showAllSelectedLabel: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, diff --git a/server/src/modules/apps/services/widget-config/multiselectV2.js b/server/src/modules/apps/services/widget-config/multiselectV2.js index 4ab4af57ce..6ad8e6f7b5 100644 --- a/server/src/modules/apps/services/widget-config/multiselectV2.js +++ b/server/src/modules/apps/services/widget-config/multiselectV2.js @@ -142,6 +142,12 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, + showAllSelectedLabel: { + type: 'toggle', + displayName: 'Show all options selected label', + validation: { schema: { type: 'boolean' }, defaultValue: true }, + section: 'additionalActions', + }, loadingState: { type: 'toggle', displayName: 'Loading state', @@ -327,6 +333,7 @@ export const multiselectV2Config = { optionsLoadingState: { value: '{{false}}' }, sort: { value: 'asc' }, placeholder: { value: 'Select the options' }, + showAllSelectedLabel: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, loadingState: { value: '{{false}}' }, From c9a4dfb8a3ed0398d208dea57754b85a9af06a4d Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 23 Apr 2025 22:01:00 +0530 Subject: [PATCH 33/43] Fixed position and label of toggle --- .../RightSideBar/Inspector/Components/Select.jsx | 11 +++++++++++ .../WidgetManager/widgets/multiselectV2.js | 12 ++++++------ .../Editor/WidgetManager/configs/multiselectV2.js | 12 ++++++------ .../apps/services/widget-config/multiselectV2.js | 12 ++++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx index d250a4342e..71d8f9ead8 100644 --- a/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx +++ b/frontend/src/AppBuilder/RightSideBar/Inspector/Components/Select.jsx @@ -539,6 +539,17 @@ export function Select({ componentMeta, darkMode, ...restProps }) { currentState, allComponents )} + {isMultiSelect && + renderElement( + component, + componentMeta, + paramUpdated, + dataQueries, + 'showAllSelectedLabel', + 'properties', + currentState, + allComponents + )} {isSortingEnabled && renderElement( component, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js index 6ad8e6f7b5..0f130aa130 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js @@ -121,6 +121,12 @@ export const multiselectV2Config = { }, accordian: 'Options', }, + showAllSelectedLabel: { + type: 'toggle', + displayName: "Show 'All items are selected.'", + validation: { schema: { type: 'boolean' }, defaultValue: true }, + accordian: 'Options', + }, optionsLoadingState: { type: 'toggle', displayName: 'Options loading state', @@ -142,12 +148,6 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, - showAllSelectedLabel: { - type: 'toggle', - displayName: 'Show all options selected label', - validation: { schema: { type: 'boolean' }, defaultValue: true }, - section: 'additionalActions', - }, loadingState: { type: 'toggle', displayName: 'Loading state', diff --git a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js index 5bff71c29a..c811bc7c65 100644 --- a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js +++ b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js @@ -121,6 +121,12 @@ export const multiselectV2Config = { }, accordian: 'Options', }, + showAllSelectedLabel: { + type: 'toggle', + displayName: "Show 'All items are selected.'", + validation: { schema: { type: 'boolean' }, defaultValue: true }, + accordian: 'Options', + }, optionsLoadingState: { type: 'toggle', displayName: 'Options loading state', @@ -142,12 +148,6 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, - showAllSelectedLabel: { - type: 'toggle', - displayName: 'Show all options selected label', - validation: { schema: { type: 'boolean' }, defaultValue: true }, - section: 'additionalActions', - }, loadingState: { type: 'toggle', displayName: 'Loading state', diff --git a/server/src/modules/apps/services/widget-config/multiselectV2.js b/server/src/modules/apps/services/widget-config/multiselectV2.js index 6ad8e6f7b5..0f130aa130 100644 --- a/server/src/modules/apps/services/widget-config/multiselectV2.js +++ b/server/src/modules/apps/services/widget-config/multiselectV2.js @@ -121,6 +121,12 @@ export const multiselectV2Config = { }, accordian: 'Options', }, + showAllSelectedLabel: { + type: 'toggle', + displayName: "Show 'All items are selected.'", + validation: { schema: { type: 'boolean' }, defaultValue: true }, + accordian: 'Options', + }, optionsLoadingState: { type: 'toggle', displayName: 'Options loading state', @@ -142,12 +148,6 @@ export const multiselectV2Config = { accordian: 'Options', isFxNotRequired: true, }, - showAllSelectedLabel: { - type: 'toggle', - displayName: 'Show all options selected label', - validation: { schema: { type: 'boolean' }, defaultValue: true }, - section: 'additionalActions', - }, loadingState: { type: 'toggle', displayName: 'Loading state', From 8beb841f950b9c3e4fb5eb722b077aa789e44546 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Wed, 23 Apr 2025 22:04:54 +0530 Subject: [PATCH 34/43] Changed toggle label --- frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js | 2 +- frontend/src/Editor/WidgetManager/configs/multiselectV2.js | 2 +- server/src/modules/apps/services/widget-config/multiselectV2.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js index 0f130aa130..ae962309e6 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/multiselectV2.js @@ -123,7 +123,7 @@ export const multiselectV2Config = { }, showAllSelectedLabel: { type: 'toggle', - displayName: "Show 'All items are selected.'", + displayName: 'Show "All items are selected"', validation: { schema: { type: 'boolean' }, defaultValue: true }, accordian: 'Options', }, diff --git a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js index c811bc7c65..08b90a71f1 100644 --- a/frontend/src/Editor/WidgetManager/configs/multiselectV2.js +++ b/frontend/src/Editor/WidgetManager/configs/multiselectV2.js @@ -123,7 +123,7 @@ export const multiselectV2Config = { }, showAllSelectedLabel: { type: 'toggle', - displayName: "Show 'All items are selected.'", + displayName: 'Show "All items are selected"', validation: { schema: { type: 'boolean' }, defaultValue: true }, accordian: 'Options', }, diff --git a/server/src/modules/apps/services/widget-config/multiselectV2.js b/server/src/modules/apps/services/widget-config/multiselectV2.js index 0f130aa130..ae962309e6 100644 --- a/server/src/modules/apps/services/widget-config/multiselectV2.js +++ b/server/src/modules/apps/services/widget-config/multiselectV2.js @@ -123,7 +123,7 @@ export const multiselectV2Config = { }, showAllSelectedLabel: { type: 'toggle', - displayName: "Show 'All items are selected.'", + displayName: 'Show "All items are selected"', validation: { schema: { type: 'boolean' }, defaultValue: true }, accordian: 'Options', }, From 51414f803f59eee3f610b37b0239f9990ac43068 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 24 Apr 2025 13:01:19 +0530 Subject: [PATCH 35/43] bug fixes --- .../LeftSidebar/PageMenu/PageHandlerMenu.jsx | 25 ++++++----- .../LeftSidebar/PageMenu/PageMenu.jsx | 2 +- .../LeftSidebar/PageMenu/PageMenuItem.jsx | 3 +- .../LeftSidebar/PageMenu/PagePermission.jsx | 6 ++- .../LeftSidebar/PageMenu/style.scss | 4 ++ frontend/src/AppBuilder/Viewer/PageGroup.jsx | 13 ++++-- .../_stores/slices/pageMenuSlice.js | 7 +++- server/ee | 2 +- .../versions/services/create.service.ts | 42 ++++++++++++++++++- 9 files changed, 83 insertions(+), 21 deletions(-) diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index ec84149418..23976b5b45 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Overlay, Popover } from 'react-bootstrap'; import { Button } from '@/_ui/LeftSidebar'; import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; export const PageHandlerMenu = ({ darkMode }) => { const setShowEditingPopover = useStore((state) => state.setShowEditingPopover); @@ -21,6 +22,8 @@ export const PageHandlerMenu = ({ darkMode }) => { const clonePage = useStore((state) => state.clonePage); const markAsHomePage = useStore((state) => state.markAsHomePage); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; // const popoverTargetRef = null; // console.log( @@ -166,16 +169,18 @@ export const PageHandlerMenu = ({ darkMode }) => { }} disabled={isHomePage} /> - { - togglePagePermissionModal(true); - }} - /> + {licenseValid && ( + { + togglePagePermissionModal(true); + }} + /> + )} { >
- + {isLicensed ? : <>} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index 7968184633..0f3b5d21a7 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -31,6 +31,7 @@ export const PageMenuItem = withRouter( const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const showEditingPopover = useStore((state) => state.showEditingPopover); + const restricted = page?.permissions && page?.permissions?.length > 0; const { definition: { styles, properties }, } = useStore((state) => state.pageSettings); @@ -199,7 +200,7 @@ export const PageMenuItem = withRouter(
- {licenseValid && page?.restricted && } + {licenseValid && restricted && }
{!shouldFreeze && ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index 095c33fd18..645bdcdc49 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -26,6 +26,7 @@ export default function PagePermission({ darkMode }) { const setSelectedUsers = useStore((state) => state.setSelectedUsers); const pagePermission = useStore((state) => state.pagePermission); const setPagePermission = useStore((state) => state.setPagePermission); + const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); const [pagePermissionType, setPagePermissionType] = useState('all'); const [showUserGroupSelect, toggleUserGroupSelect] = useState(false); @@ -45,7 +46,7 @@ export default function PagePermission({ darkMode }) { toggleUserGroupSelect(true); data?.length && setSelectedUserGroups( - data[0]?.users?.map((user) => ({ + data[0]?.groups?.map((user) => ({ label: user?.permissionGroup?.name, value: user?.permissionGroup?.id, count: user?.permissionGroup?.count, @@ -141,6 +142,7 @@ export default function PagePermission({ darkMode }) { .createPagePermission(appId, editingPage?.id, body) .then((data) => { toast.success('Permission successfully created!'); + updatePageWithPermissions(editingPage?.id, data); }) .catch(() => { toast.error('Permission could not be created. Please try again!'); @@ -164,6 +166,7 @@ export default function PagePermission({ darkMode }) { .updatePagePermission(appId, editingPage?.id, body) .then((data) => { toast.success('Permission successfully updated!'); + updatePageWithPermissions(editingPage?.id, data); }) .catch(() => { toast.error('Permission could not be updated. Please try again!'); @@ -180,6 +183,7 @@ export default function PagePermission({ darkMode }) { .deletePagePermission(appId, pageToDelete) .then((data) => { toast.success('Permission successfully deleted!'); + updatePageWithPermissions(pageToDelete, []); }) .catch(() => { toast.error('Permission could not be deleted. Please try again!'); diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index b3799e09ce..c4e4f97d33 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -305,6 +305,10 @@ } } + .react-select__menu-list { + overflow-y: unset !important; + } + .user-select-option { display: flex; align-items: center; diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx index 120b5dfc68..0311115b09 100644 --- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx +++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx @@ -15,7 +15,7 @@ const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, comput console.log({ isHomePage }); const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription']; - return page.hidden || page.disabled ? null : ( + return (page.hidden || page.disabled) && page?.restricted ? null : ( switchPageWrapper(page?.id)} @@ -150,10 +150,15 @@ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkM
{/* page.id)}> */} {filteredPages.map((page, index) => { - if (page.isPageGroup && page.children.length === 0 && labelStyle?.label?.hidden) { + if ( + page.isPageGroup && + page.children.length === 0 && + labelStyle?.label?.hidden && + !page.children.some((child) => child?.restricted === true) + ) { return null; } - if (page.children && page.isPageGroup) { + if (page.children && page.isPageGroup && !page.children.some((child) => child?.restricted === true)) { // if we are only displaying icons, we don't display the groups instead display separator to separate a page groups const renderSeparatorTop = index !== 0 && labelStyle?.label?.hidden; const renderSeparatorBottom = !filteredPages[index + 1]?.isPageGroup && labelStyle?.label?.hidden; @@ -193,7 +198,7 @@ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkM )} ); - } else { + } else if (!page.isPageGroup) { return ( {}) => + (updatePaths, afterUpdateFn = () => {}, enableSave = true) => (pageId, values) => { return (set, get) => { set((state) => { @@ -57,7 +57,7 @@ const createPageUpdateCommand = const { app, currentVersionId } = get(); const diff = _.zipObject(updatePaths, values); - savePageChanges(app.appId, currentVersionId, pageId, diff); + if (enableSave) savePageChanges(app.appId, currentVersionId, pageId, diff); }; }; @@ -82,6 +82,8 @@ export const createPageMenuSlice = (set, get) => { state.editingPage = null; }); + const updatePageWithPermissions = createPageUpdateCommand(['permissions'], (state) => {}, false); + return { editingPage: null, showEditingPopover: false, @@ -194,6 +196,7 @@ export const createPageMenuSlice = (set, get) => { updatePageHandle(pageId, [value])(set, get); }, updatePageGroupName: (pageId, value) => updatePageGroupName(pageId, [value])(set, get), + updatePageWithPermissions: (pageId, value) => updatePageWithPermissions(pageId, [value])(set, get), // unsure about this one clonePage: async (pageId) => { const { diff --git a/server/ee b/server/ee index 7d46f023ce..78c6a52262 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 7d46f023cea42533c4a8ff387dcc1149553c4671 +Subproject commit 78c6a52262ba2df4a0771ae778178d2d74bb7517 diff --git a/server/src/modules/versions/services/create.service.ts b/server/src/modules/versions/services/create.service.ts index 12a24ac7c6..94b7f9783c 100644 --- a/server/src/modules/versions/services/create.service.ts +++ b/server/src/modules/versions/services/create.service.ts @@ -6,7 +6,7 @@ import { DataSource } from '@entities/data_source.entity'; import { DataSourceOptions } from '@entities/data_source_options.entity'; import { EventHandler, Target } from '@entities/event_handler.entity'; import { dbTransactionWrap } from '@helpers/database.helper'; -import { EntityManager } from 'typeorm'; +import { EntityManager, In } from 'typeorm'; import { Credential } from 'src/entities/credential.entity'; import * as uuid from 'uuid'; import { Page } from '@entities/page.entity'; @@ -22,6 +22,8 @@ import { DataSourcesRepository } from '@modules/data-sources/repository'; import { DataQueryRepository } from '@modules/data-queries/repository'; import { AppEnvironmentUtilService } from '@modules/app-environments/util.service'; import { IVersionsCreateService } from '../interfaces/services/ICreateService'; +import { PagePermission } from '@entities/page_permissions.entity'; +import { PageUser } from '@entities/page_users.entity'; @Injectable() export class VersionsCreateService implements IVersionsCreateService { @@ -401,6 +403,44 @@ export class VersionsCreateService implements IVersionsCreateService { homePageId = savedPage.id; } + const oldPermissions = await manager.find(PagePermission, { + where: { pageId: page.id }, + }); + + const newPermissions = oldPermissions.map((permission) => { + return manager.create(PagePermission, { + ...permission, + id: undefined, + pageId: oldPageToNewPageMapping[permission.pageId], + }); + }); + + await manager.save(PagePermission, newPermissions); + + const permissionIdMap = new Map(); + oldPermissions.forEach((oldPerm, index) => { + const newPerm = newPermissions[index]; + permissionIdMap.set(oldPerm.id, newPerm.id); + }); + + const oldPermissionIds = oldPermissions.map((p) => p.id); + + const oldPageUsers = await manager.find(PageUser, { + where: { + pagePermissionsId: In(oldPermissionIds), + }, + }); + + const newPageUsers = oldPageUsers.map((pu) => + manager.create(PageUser, { + ...pu, + id: undefined, + pagePermissionsId: permissionIdMap.get(pu.pagePermissionsId), + }) + ); + + await manager.save(PageUser, newPageUsers); + const pageEvents = allEvents.filter((event) => event.sourceId === page.id); pageEvents.forEach(async (event, index) => { From 459799748549c1553f8411cec64e3be56b3e9431 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 24 Apr 2025 15:26:36 +0530 Subject: [PATCH 36/43] bug fixes --- .../LeftSidebar/PageMenu/PagePermission.jsx | 158 ++++++++++++------ .../LeftSidebar/PageMenu/style.scss | 7 + 2 files changed, 115 insertions(+), 50 deletions(-) diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index 645bdcdc49..83c017f883 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -7,6 +7,7 @@ import useStore from '@/AppBuilder/_stores/store'; import { appPermissionService } from '@/_services'; import { ConfirmDialog } from '@/_components'; import toast from 'react-hot-toast'; +import Spinner from '@/_ui/Spinner'; const PERMISSION_TYPES = { single: 'SINGLE', @@ -18,7 +19,6 @@ export default function PagePermission({ darkMode }) { const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const editingPage = useStore((state) => state.editingPage); - const setEditingPage = useStore((state) => state.setEditingPage); const appId = useStore((state) => state.app.appId); const selectedUserGroups = useStore((state) => state.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); @@ -33,48 +33,96 @@ export default function PagePermission({ darkMode }) { const [showUsersSelect, toggleUsersSelect] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isPermissionsLoading, setPermissionsLoading] = useState(true); const [pageToDelete, setPageToDelete] = useState(null); + const [initialSelectedGroups, setInitialSelectedGroups] = useState([]); + const [initialSelectedUsers, setInitialSelectedUsers] = useState([]); + const [initalPagePermissionType, setInitialPagePermissionType] = useState('all'); useEffect(() => { - if (!editingPage?.id && !showPagePermissionModal) return; + if (!showPagePermissionModal) return; const fetchPagePermission = () => { - appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { + appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => { if (data) { if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { + const groups = + data[0]?.groups?.map((user) => ({ + label: user?.permissionGroup?.name, + value: user?.permissionGroup?.id, + count: user?.permissionGroup?.count, + })) ?? []; setPagePermissionType(data[0]?.type?.toLowerCase()); + setInitialPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUserGroupSelect(true); - data?.length && - setSelectedUserGroups( - data[0]?.groups?.map((user) => ({ - label: user?.permissionGroup?.name, - value: user?.permissionGroup?.id, - count: user?.permissionGroup?.count, - })) - ); + setPageToDelete(null); + setInitialSelectedGroups(groups); + data?.length && setSelectedUserGroups(groups); } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) { + const users = + data[0]?.users?.map(({ user }) => { + const firstName = user.firstName || ''; + const lastName = user.lastName || ''; + return { + value: user.id, + label: `${firstName} ${lastName}`.trim(), + email: user.email, + initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(), + }; + }) ?? []; setPagePermissionType(data[0]?.type?.toLowerCase()); + setInitialPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUsersSelect(true); - data?.length && - setSelectedUsers( - data[0]?.users?.map(({ user }) => { - const firstName = user.firstName || ''; - const lastName = user.lastName || ''; - return { - value: user.id, - label: `${firstName} ${lastName}`.trim(), - email: user.email, - initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(), - }; - }) - ); + setPageToDelete(null); + setInitialSelectedUsers(users); + data?.length && setSelectedUsers(users); } } + setPermissionsLoading(false); }); }; fetchPagePermission(); - }, [editingPage]); + }, [showPagePermissionModal, pageToDelete]); + + const isSelectionUnchanged = useMemo(() => { + if (pagePermissionType === 'group') { + if (!selectedUserGroups.length) return true; + const current = selectedUserGroups + .map((g) => g.value) + .sort() + .join(','); + const initial = initialSelectedGroups + .map((g) => g.value) + .sort() + .join(','); + return current === initial; + } else if (pagePermissionType === 'single') { + if (!selectedUsers.length) return true; + const current = selectedUsers + .map((u) => u.value) + .sort() + .join(','); + const initial = initialSelectedUsers + .map((u) => u.value) + .sort() + .join(','); + return current === initial; + } else { + if (!pagePermission?.length) { + return true; + } else { + return initalPagePermissionType == pagePermissionType; + } + } + }, [ + pagePermissionType, + selectedUserGroups, + initialSelectedGroups, + selectedUsers, + initialSelectedUsers, + initalPagePermissionType, + ]); const permissionTypeOptions = useMemo( () => [ @@ -124,9 +172,10 @@ export default function PagePermission({ darkMode }) { toggleUsersSelect(false); setPagePermissionType('all'); setPagePermission(null); - setEditingPage(null); setSelectedUsers([]); setSelectedUserGroups([]); + setInitialSelectedGroups([]); + setInitialSelectedUsers([]); }; const createPagePermission = () => { @@ -184,15 +233,16 @@ export default function PagePermission({ darkMode }) { .then((data) => { toast.success('Permission successfully deleted!'); updatePageWithPermissions(pageToDelete, []); + setPageToDelete(null); }) .catch(() => { toast.error('Permission could not be deleted. Please try again!'); + setShowConfirmDelete(false); + togglePagePermissionModal(true); }) .finally(() => { setIsLoading(false); setShowConfirmDelete(false); - setPageToDelete(null); - handlePagePermissionModalClose(); }); }; @@ -223,7 +273,7 @@ export default function PagePermission({ darkMode }) { handleClose={handlePagePermissionModalClose} confirmBtnProps={{ title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission', - disabled: pagePermissionType == 'all' ? true : false, + disabled: isPermissionsLoading || isSelectionUnchanged, tooltipMessage: '', }} darkMode={darkMode} @@ -243,29 +293,37 @@ export default function PagePermission({ darkMode }) { } >
-
-
- + {isPermissionsLoading ? ( +
+
-
-
-

- Only selected users will be allowed to access this page. Read docs to know more. -

+ ) : ( + <> +
+
+ +
+
+
+

+ Only selected users will be allowed to access this page. Read docs to know more. +

+
+
-
-
- - + {showUserGroupSelect && } + {showUsersSelect && } + + )}
{showConfirmDelete && ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index c4e4f97d33..968218b106 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -354,6 +354,7 @@ display: flex; flex-direction: row; gap: 8px; + align-items: center; .name { font-weight: 400; @@ -367,4 +368,10 @@ } } } +} + +.page-permission { + .spinner-center { + min-height: 250px; + } } \ No newline at end of file From bf8495b6ea443e29ed5d16a4329303ae97557f2b Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 24 Apr 2025 16:23:03 +0530 Subject: [PATCH 37/43] bug fixes --- .../LeftSidebar/PageMenu/PagePermission.jsx | 24 ++++++++++++++----- .../Viewer/ViewerSidebarNavigation.jsx | 2 +- frontend/src/AppBuilder/_hooks/useAppData.js | 8 ++++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index 83c017f883..6a4a1c516a 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -190,11 +190,15 @@ export default function PagePermission({ darkMode }) { appPermissionService .createPagePermission(appId, editingPage?.id, body) .then((data) => { - toast.success('Permission successfully created!'); + toast.success('Permission successfully created!', { + className: 'text-nowrap w-auto mw-100', + }); updatePageWithPermissions(editingPage?.id, data); }) .catch(() => { - toast.error('Permission could not be created. Please try again!'); + toast.error('Permission could not be created. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); }) .finally(() => { setIsLoading(false); @@ -214,11 +218,15 @@ export default function PagePermission({ darkMode }) { appPermissionService .updatePagePermission(appId, editingPage?.id, body) .then((data) => { - toast.success('Permission successfully updated!'); + toast.success('Permission successfully updated!', { + className: 'text-nowrap w-auto mw-100', + }); updatePageWithPermissions(editingPage?.id, data); }) .catch(() => { - toast.error('Permission could not be updated. Please try again!'); + toast.error('Permission could not be updated. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); }) .finally(() => { setIsLoading(false); @@ -231,12 +239,16 @@ export default function PagePermission({ darkMode }) { appPermissionService .deletePagePermission(appId, pageToDelete) .then((data) => { - toast.success('Permission successfully deleted!'); + toast.success('Permission successfully deleted!', { + className: 'text-nowrap w-auto mw-100', + }); updatePageWithPermissions(pageToDelete, []); setPageToDelete(null); }) .catch(() => { - toast.error('Permission could not be deleted. Please try again!'); + toast.error('Permission could not be deleted. Please try again!', { + className: 'text-nowrap w-auto mw-100', + }); setShowConfirmDelete(false); togglePagePermissionModal(true); }) diff --git a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx index a7111a48a0..5312d60a8a 100644 --- a/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx +++ b/frontend/src/AppBuilder/Viewer/ViewerSidebarNavigation.jsx @@ -149,7 +149,7 @@ export const ViewerSidebarNavigation = ({ const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; // eslint-disable-next-line import/namespace const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription']; - return page.hidden || page.disabled ? null : ( + return page.hidden || page.disabled || page?.restricted ? null : ( switchPageWrapper(page?.id)} diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index d10e4343b9..f131b4eb59 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -323,8 +323,8 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v let startingPage = appData.pages.find((page) => page.id === homePageId); //no access to homepage, set to the next available page - if (!homePageId) { - startingPage = appData.pages[0]; + if (startingPage?.restricted) { + startingPage = appData.pages.find((page) => !page?.restricted); } if (initialLoadRef.current) { @@ -341,7 +341,9 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v window.history.replaceState(null, null, newUrl); if (page?.restricted) { - toast.error('Access to this page is restricted. Contact admin to know more.'); + toast.error('Access to this page is restricted. Contact admin to know more.', { + className: 'text-nowrap w-auto mw-100', + }); } } else { startingPage = page; From 445a03f51c12b515079f97d9b28bd1eebfa68be8 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Thu, 24 Apr 2025 16:42:35 +0530 Subject: [PATCH 38/43] Fix: Include admins and builders for setting page permissions and it applies to all on released apps --- server/ee | 2 +- server/src/modules/app-permissions/constants/index.ts | 1 + server/src/modules/app-permissions/interfaces/IUtilService.ts | 2 +- server/src/modules/app-permissions/util.service.ts | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/server/ee b/server/ee index 78c6a52262..90e11056eb 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 78c6a52262ba2df4a0771ae778178d2d74bb7517 +Subproject commit 90e11056ebdb40f0560ece55198c3ecc8ead4988 diff --git a/server/src/modules/app-permissions/constants/index.ts b/server/src/modules/app-permissions/constants/index.ts index ef0b13f325..c1d2afe78b 100644 --- a/server/src/modules/app-permissions/constants/index.ts +++ b/server/src/modules/app-permissions/constants/index.ts @@ -1,6 +1,7 @@ export enum PAGE_PERMISSION_TYPE { SINGLE = 'SINGLE', GROUP = 'GROUP', + ALL = 'ALL', } export enum FEATURE_KEY { diff --git a/server/src/modules/app-permissions/interfaces/IUtilService.ts b/server/src/modules/app-permissions/interfaces/IUtilService.ts index 0d2be25315..06654ed9e9 100644 --- a/server/src/modules/app-permissions/interfaces/IUtilService.ts +++ b/server/src/modules/app-permissions/interfaces/IUtilService.ts @@ -3,7 +3,7 @@ import { GroupPermissions } from '@entities/group_permissions.entity'; import { CreatePagePermissionDto } from '../dto'; export interface IUtilService { - getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise; + getUsersWithViewAccess(appId: string, organizationId: string): Promise; getUserGroupsWithViewAccess(appId: string, organizationId: string): Promise; diff --git a/server/src/modules/app-permissions/util.service.ts b/server/src/modules/app-permissions/util.service.ts index 377c693a26..71432a0e4b 100644 --- a/server/src/modules/app-permissions/util.service.ts +++ b/server/src/modules/app-permissions/util.service.ts @@ -12,7 +12,7 @@ export class AppPermissionsUtilService implements IUtilService { throw new Error('Method not implemented.'); } - async getUsersWithViewAccess(appId: string, organizationId: string, endUserIds: string[]): Promise { + async getUsersWithViewAccess(appId: string, organizationId: string): Promise { throw new Error('Method not implemented.'); } From c8e99bc67a61adb16029173e51f8ecd24adc03ea Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 24 Apr 2025 17:17:17 +0530 Subject: [PATCH 39/43] bug fixes --- .../LeftSidebar/PageMenu/PageHandlerMenu.jsx | 79 +++++++++++-------- frontend/src/_styles/components.scss | 10 +++ server/ee | 2 +- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index 23976b5b45..1c5c3124f0 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -3,6 +3,8 @@ import { Overlay, Popover } from 'react-bootstrap'; import { Button } from '@/_ui/LeftSidebar'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; export const PageHandlerMenu = ({ darkMode }) => { const setShowEditingPopover = useStore((state) => state.setShowEditingPopover); @@ -25,24 +27,6 @@ export const PageHandlerMenu = ({ darkMode }) => { const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; - // const popoverTargetRef = null; - // console.log( - // { - // setShowEditingPopover, - // setShowRenameHandlerModal, - // setEditingPage, - // setShowPageEventsModal, - // popoverTargetRef, - // editingPage, - // showRenameHandlerModal, - // showPageEventsModal, - // setEditingPageName, - // showEditingPopover, - // closeEditingPopover, - // }, - // 'editingPage' - // ); - const closeMenu = () => { closePageEditPopover(); }; @@ -124,7 +108,6 @@ export const PageHandlerMenu = ({ darkMode }) => { callback={() => markAsHomePage(editingPage.id)} /> )} - {!isDisabled && ( { disabled={isHomePage} /> )} - { clonePage(editingPage.id); }} /> - { }} disabled={isHomePage} /> - {licenseValid && ( - { - togglePagePermissionModal(true); - }} - /> - )} + + { + return ( + +
+
Page permission
+ {!licenseValid && } +
+
+ ); + }} + customClass={'delete-btn'} + iconSrc={`assets/images/icons/editor/left-sidebar/authorization.svg`} + closeMenu={closeMenu} + callback={(id) => { + togglePagePermissionModal(true); + }} + /> { ); }; -const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = false, callback = () => null }) => { +const Field = ({ + id, + text, + iconSrc, + customClass = '', + classNames, + closeMenu, + disabled = false, + callback = () => null, +}) => { const handleOnClick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -250,7 +254,12 @@ const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = fals return (
- +
diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss index e84756dca7..074338602e 100644 --- a/frontend/src/_styles/components.scss +++ b/frontend/src/_styles/components.scss @@ -237,6 +237,16 @@ $btn-dark-color: #FFFFFF; } } } + + .page-permission-btn { + display: flex; + align-items: baseline; + gap: 5px; + + &.disabled { + opacity: 1 !important; + } + } } .notification-dot { diff --git a/server/ee b/server/ee index 90e11056eb..78c6a52262 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 90e11056ebdb40f0560ece55198c3ecc8ead4988 +Subproject commit 78c6a52262ba2df4a0771ae778178d2d74bb7517 From ef51a0b8c8c6550c08a812f89447e71d0fcffcaa Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Thu, 24 Apr 2025 17:23:30 +0530 Subject: [PATCH 40/43] rebase --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 78c6a52262..90e11056eb 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 78c6a52262ba2df4a0771ae778178d2d74bb7517 +Subproject commit 90e11056ebdb40f0560ece55198c3ecc8ead4988 From 5f319e2d53c50b896cd477d93e8d252448eb839d Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:16:56 +0530 Subject: [PATCH 41/43] Fixes drag and drop on form with scrolls --- .../src/AppBuilder/AppCanvas/Grid/Grid.jsx | 41 +++++++---- .../AppBuilder/AppCanvas/Grid/gridUtils.js | 15 ++++ .../AppCanvas/Grid/helpers/dragEnd.js | 1 - .../AppBuilder/WidgetManager/widgets/form.js | 6 ++ frontend/src/AppBuilder/Widgets/Form/Form.jsx | 27 ++++++-- .../src/AppBuilder/Widgets/Form/FormUtils.js | 1 - .../_stores/slices/componentsSlice.js | 24 +++++++ .../src/Editor/WidgetManager/configs/form.js | 10 ++- .../apps/services/widget-config/form.js | 68 +++---------------- 9 files changed, 109 insertions(+), 84 deletions(-) diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx index 7043c78774..b9adc9d4c3 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/Grid.jsx @@ -22,6 +22,8 @@ import { handleActivateTargets, handleDeactivateTargets, handleActivateNonDraggingComponents, + computeScrollDelta, + computeScrollDeltaOnDrag, } from './gridUtils'; import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd'; import useStore from '@/AppBuilder/_stores/store'; @@ -56,6 +58,7 @@ export default function Grid({ gridWidth, currentLayout }) { const canvasWidth = NO_OF_GRIDS * gridWidth; const getHoveredComponentForGrid = useStore((state) => state.getHoveredComponentForGrid, shallow); const getResolvedComponent = useStore((state) => state.getResolvedComponent, shallow); + const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight, shallow); const [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS); const draggingComponentId = useStore((state) => state.draggingComponentId, shallow); const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow); @@ -345,6 +348,7 @@ export default function Grid({ gridWidth, currentLayout }) { const handleDragEnd = useCallback( (boxPositions) => { let newParent = null; + let oldParent = null; const updatedLayouts = boxPositions.reduce((layouts, { id, x, y, parent }) => { const currentWidget = boxList.find((box) => box.id === id); const containerWidth = parent ? useGridStore.getState().subContainerWidths[parent] : gridWidth; @@ -389,7 +393,7 @@ export default function Grid({ gridWidth, currentLayout }) { } } newParent = parent ? parent : null; - + oldParent = currentWidget.component?.parent; layouts[id] = { width: _width, height: _height, @@ -400,6 +404,11 @@ export default function Grid({ gridWidth, currentLayout }) { return layouts; }, {}); setComponentLayout(updatedLayouts, newParent, undefined, { updateParent: true }); + + // const currentWidget = boxList.find((box) => box.id === id); + updateContainerAutoHeight(newParent); + updateContainerAutoHeight(oldParent); + toggleCanvasUpdater(); }, // eslint-disable-next-line react-hooks/exhaustive-deps @@ -867,20 +876,19 @@ export default function Grid({ gridWidth, currentLayout }) { const targetSlotId = target?.slotId; const targetGridWidth = useGridStore.getState().subContainerWidths[targetSlotId] || gridWidth; - - // const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[source.widgetType] || []; - // const draggedWidgetType = dragged.widgetType; const isParentChangeAllowed = dragContext.isDroppable; // Compute new position let { left, top } = getAdjustedDropPosition(e, target, isParentChangeAllowed, targetGridWidth, dragged); const isModalToCanvas = source.isModal && target.slotId === 'real-canvas'; + let scrollDelta = computeScrollDelta({ source }); if (isParentChangeAllowed && !isModalToCanvas) { - const parent = target.slotId === 'real-canvas' ? null : target.slotId; // Special case for Modal; If source widget is modal, prevent drops to canvas - handleDragEnd([{ id: e.target.id, x: left, y: top, parent }]); + const parent = target.slotId === 'real-canvas' ? null : target.slotId; + + handleDragEnd([{ id: e.target.id, x: left, y: top + scrollDelta, parent }]); } else { const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth; @@ -889,9 +897,8 @@ export default function Grid({ gridWidth, currentLayout }) { !isModalToCanvas ?? toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`); } - // Apply transform for smooth transition - e.target.style.transform = `translate(${left}px, ${top}px)`; + e.target.style.transform = `translate(${left}px, ${top + scrollDelta}px)`; // Force reordering of conatiner if the parent has not changed const newParentId = target.slotId === 'real-canvas' ? 'canvas' : target.slotId; @@ -959,12 +966,6 @@ export default function Grid({ gridWidth, currentLayout }) { setCanvasBounds({ ...relativePosition }); } - e.target.style.transform = `translate(${left}px, ${top}px)`; - e.target.setAttribute( - 'widget-pos2', - `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` - ); - // This block is to show grid lines on the canvas when the dragged element is over a new canvas if (document.elementFromPoint(e.clientX, e.clientY)) { const targetElems = document.elementsFromPoint(e.clientX, e.clientY); @@ -992,6 +993,17 @@ export default function Grid({ gridWidth, currentLayout }) { handleActivateTargets(newParentId); } } + + // Build the drag context from the event + const source = { slotId: oldParentId }; + let scrollDelta = computeScrollDeltaOnDrag({ source }); + + e.target.style.transform = `translate(${left}px, ${top - scrollDelta}px)`; + e.target.setAttribute( + 'widget-pos2', + `translate: ${e.translate[0]} | Round: ${Math.round(e.translate[0] / gridWidth) * gridWidth} | ${gridWidth}` + ); + // Postion ghost element exactly as same at dragged element if (document.getElementById(`moveable-drag-ghost`)) { document.getElementById(`moveable-drag-ghost`).style.transform = `translate(${left}px, ${top}px)`; @@ -1081,6 +1093,7 @@ export default function Grid({ gridWidth, currentLayout }) { } }} snapGridAll={true} + scrollable={true} /> ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js index b6a801d863..f36f921be9 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/gridUtils.js @@ -502,3 +502,18 @@ export const handleDeactivateTargets = () => { component.classList.remove('non-dragging-component'); }); }; +export const computeScrollDelta = ({ source }) => { + // Only need to calculate scroll delta when moving from a sub-container + if (source.slotId !== 'real-canvas') { + const subContainerWrap = document + .querySelector(`#canvas-${source.slotId}`) + ?.closest('.sub-container-overflow-wrap'); + + return subContainerWrap?.scrollTop || 0; + } + + // Default case: No scroll adjustment needed + return 0; +}; + +export const computeScrollDeltaOnDrag = computeScrollDelta; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js index a9405d043e..da5a8341bf 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js +++ b/frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js @@ -175,7 +175,6 @@ export class DragContext { const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot]; return !restrictedWidgets.includes(dragged.widgetType); - ß; } } diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/form.js b/frontend/src/AppBuilder/WidgetManager/widgets/form.js index 2d0ff4b1c0..129322cf73 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/form.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/form.js @@ -147,6 +147,12 @@ export const formConfig = { isHidden: true, validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, footerHeight: { type: 'numberInput', displayName: 'Footer height', diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 798ed56bcf..29a576bd9e 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -45,6 +45,7 @@ export const Form = function Form(props) { showFooter = false, headerHeight = 80, footerHeight = 80, + canvasHeight, } = properties; const { isDisabled, isVisible, isLoading } = useExposeState( properties.loadingState, @@ -268,6 +269,8 @@ export const Form = function Form(props) { const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); + // const updateContainerAutoHeight = useStore((state) => state.updateContainerAutoHeight); + const updateHeaderSizeInStore = ({ newHeight }) => { const _height = parseInt(newHeight, 10); setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false); @@ -278,6 +281,18 @@ export const Form = function Form(props) { setComponentProperty(id, `footerHeight`, _height, 'properties', 'value', false); }; + const [canHeight, setCanHeight] = useState('100%'); + useEffect(() => { + // const newHeight = parseInt(height, 10) - 14; + + // const autoCanvasHeight = document.querySelector(`#canvas-${id}`)?.scrollHeight; + const wrapHeight = parseInt(computedFormBodyHeight, 10); + // Set height to the larger value between computed body height and canvas scroll height + const maxHeight = Math.max(wrapHeight, canvasHeight || 10); + + const roundedHeight = Math.round(maxHeight / 10) * 10; + setCanHeight(`${roundedHeight}px`); + }, [computedFormBodyHeight, canvasHeight]); const headerMaxHeight = parseInt(height, 10) - parseInt(footerHeight, 10) - 100 - 10; const footerMaxHeight = parseInt(height, 10) - parseInt(headerHeight, 10) - 100 - 10; const formFooter = { @@ -328,7 +343,7 @@ export const Form = function Form(props) { /> )} -
+
{isLoading ? (
@@ -336,17 +351,17 @@ export const Form = function Form(props) { ) : (
{!advanced && ( -
+
diff --git a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js index fe64b7adc8..228ca6f36e 100644 --- a/frontend/src/AppBuilder/Widgets/Form/FormUtils.js +++ b/frontend/src/AppBuilder/Widgets/Form/FormUtils.js @@ -550,6 +550,5 @@ export const getBodyHeight = (height, showHeader, showFooter, headerHeight = 60, const rounded = Math.ceil(modalHeight / 10) * 10; - console.log('rounded', rounded) return `${Math.max(rounded - 20, 40)}px`; }; diff --git a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js index b746f1237b..b500a4d912 100644 --- a/frontend/src/AppBuilder/_stores/slices/componentsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/componentsSlice.js @@ -1896,4 +1896,28 @@ export const createComponentsSlice = (set, get) => ({ state.modalsOpenOnCanvas = newModalOpenOnCanvas; }); }, + updateContainerAutoHeight: (componentId) => { + if ( + !componentId || + componentId === 'canvas' || + componentId.includes('-header') || + componentId.includes('-footer') + ) { + return; + } + const { currentLayout, getCurrentPageComponents, setComponentProperty } = get(); + const allComponents = getCurrentPageComponents(); + + const childComponents = getAllChildComponents(allComponents, componentId); + const maxHeight = Object.values(childComponents).reduce((max, component) => { + const layout = component?.layouts?.[currentLayout]; + if (!layout) { + return max; + } + const sum = layout.top + layout.height; + return Math.max(max, sum); + }, 0); + + setComponentProperty(componentId, `canvasHeight`, maxHeight, 'properties', 'value', false); + }, }); diff --git a/frontend/src/Editor/WidgetManager/configs/form.js b/frontend/src/Editor/WidgetManager/configs/form.js index 0ef410b908..129322cf73 100644 --- a/frontend/src/Editor/WidgetManager/configs/form.js +++ b/frontend/src/Editor/WidgetManager/configs/form.js @@ -147,6 +147,12 @@ export const formConfig = { isHidden: true, validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, footerHeight: { type: 'numberInput', displayName: 'Footer height', @@ -201,7 +207,7 @@ export const formConfig = { }, }, backgroundColor: { - type: 'color', + type: 'colorSwatches', displayName: 'Background color', validation: { schema: { type: 'string' }, @@ -219,7 +225,7 @@ export const formConfig = { }, }, borderColor: { - type: 'color', + type: 'colorSwatches', displayName: 'Border color', validation: { schema: { type: 'string' }, diff --git a/server/src/modules/apps/services/widget-config/form.js b/server/src/modules/apps/services/widget-config/form.js index 349f5e2f91..129322cf73 100644 --- a/server/src/modules/apps/services/widget-config/form.js +++ b/server/src/modules/apps/services/widget-config/form.js @@ -98,63 +98,6 @@ export const formConfig = { padding: 'default', }, }, - { - componentName: 'Text', - layout: { - top: 200, - left: 5, - height: 30, - width: 10, - }, - properties: ['text'], - accessorKey: 'text', - styles: ['fontWeight', 'textSize', 'textColor', 'direction'], - defaultValue: { - text: 'Who are you', - textSize: 12, - direction: 'left', - textColor: '#000', - }, - }, - { - componentName: 'TextArea', - layout: { - top: 200, - left: 14, - height: 80, - width: 22, - }, - properties: ['placeholder', 'value'], - styles: ['alignment', 'width', 'auto', 'padding', 'visibility'], - defaultValue: { - placeholder: 'Tomy', - value: 'Pet name', - width: '{{60}}', - alignment: 'side', - auto: '{{false}}', - padding: 'default', - visibility: '{{true}}', - }, - }, - { - componentName: 'MultiselectV2', - layout: { - top: 400, - left: 5, - height: 40, - width: 31, - }, - properties: ['placeholder', 'label'], - styles: ['alignment', 'width', 'auto', 'direction'], - defaultValue: { - label: 'Favorite color?', - width: '{{60}}', - alignment: 'side', - auto: '{{false}}', - padding: 'default', - direction: 'left', - }, - }, ], component: 'Form', others: { @@ -204,6 +147,12 @@ export const formConfig = { isHidden: true, validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, }, + canvasHeight: { + type: 'numberInput', + displayName: 'Canvas height', + isHidden: true, + validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, + }, footerHeight: { type: 'numberInput', displayName: 'Footer height', @@ -328,9 +277,8 @@ 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}}' }, + showHeader: { value: '{{true}}' }, + showFooter: { value: '{{true}}' }, visibility: { value: '{{true}}' }, disabledState: { value: '{{false}}' }, headerHeight: { value: 60 }, From a745ca07d9aa141cde7c7549b4f0928d876e6ee6 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Fri, 25 Apr 2025 12:38:25 +0530 Subject: [PATCH 42/43] be changes --- server/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/ee b/server/ee index 90e11056eb..12599a28b1 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 90e11056ebdb40f0560ece55198c3ecc8ead4988 +Subproject commit 12599a28b17d84e30b0ea4897a239ed89c011425 From dd49076c33fa6c0a10a1a88fc5c134cf777f478c Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Fri, 25 Apr 2025 14:36:26 +0530 Subject: [PATCH 43/43] fix: config file structure --- server/src/modules/apps/services/widget-config/icon.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/modules/apps/services/widget-config/icon.js b/server/src/modules/apps/services/widget-config/icon.js index 9f8c178def..bf373e526b 100644 --- a/server/src/modules/apps/services/widget-config/icon.js +++ b/server/src/modules/apps/services/widget-config/icon.js @@ -142,4 +142,5 @@ export const iconConfig = { padding: { value: 'default' }, boxShadow: { value: '0px 0px 0px 0px #00000040' }, }, -}; + } +}; \ No newline at end of file