From a73888911dbd675abf2348677eb08612dc24c59e Mon Sep 17 00:00:00 2001 From: Nakul Nagargade Date: Mon, 23 Mar 2026 20:34:20 +0530 Subject: [PATCH] Add page footer functionality to AppCanvas, including new PageCanvasFooter component and related styles. Updated layout components to support footer visibility and added necessary state management. Enhanced page settings to include footer options with licensing checks. --- .../src/AppBuilder/AppCanvas/AppCanvas.jsx | 2 + .../AppCanvas/Grid/DesktopLayout.jsx | 3 + .../AppCanvas/Grid/MobileLayout.jsx | 3 + .../AppCanvas/Grid/PageCanvasFooter.jsx | 96 +++++++++++++++++++ frontend/src/AppBuilder/AppCanvas/Selecto.jsx | 10 +- .../src/AppBuilder/AppCanvas/appCanvas.scss | 14 ++- .../PageMenu/AddNewPagePopup.jsx | 30 +++++- .../_stores/slices/inspectorSlice.js | 2 +- server/ee | 2 +- ...0000002-AddPageFooterColumnToPagesTable.ts | 18 ++++ server/src/entities/page.entity.ts | 3 + server/src/modules/apps/dto/page.ts | 3 + .../services/app-import-export.service.ts | 7 ++ .../src/modules/apps/services/page.service.ts | 1 + .../apps/services/page.util.service.ts | 6 ++ .../versions/services/create.service.ts | 1 + 16 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx create mode 100644 server/migrations/1767700000002-AddPageFooterColumnToPagesTable.ts diff --git a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx index 8d2dcb302d..559ef0446d 100644 --- a/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx +++ b/frontend/src/AppBuilder/AppCanvas/AppCanvas.jsx @@ -257,6 +257,7 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => { { isModuleMode={isModuleMode} isMobileLayout={isMobileLayout} showCanvasHeader={showCanvasHeader} + showCanvasFooter={showCanvasFooter} position={position} isPagesSidebarHidden={isPagesSidebarHidden} appType={appType} diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/DesktopLayout.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/DesktopLayout.jsx index 55b5a220a3..680616bfee 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/DesktopLayout.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/DesktopLayout.jsx @@ -3,6 +3,7 @@ import cx from 'classnames'; import { PAGE_CANVAS_HEADER_HEIGHT } from '../appCanvasConstants'; import { PageCanvasHeader } from './PageCanvasHeader'; +import { PageCanvasFooter } from './PageCanvasFooter'; import PagesSidebarNavigation from '../../RightSideBar/PageSettingsTab/PageMenu/PagesSidebarNavigation'; import { CanvasContentTail } from './CanvasContentTail'; @@ -11,6 +12,7 @@ export const DesktopLayout = ({ isModuleMode, isMobileLayout, showCanvasHeader, + showCanvasFooter, position, isPagesSidebarHidden, appType, @@ -67,5 +69,6 @@ export const DesktopLayout = ({ {mainCanvasContainer} + ); diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/MobileLayout.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/MobileLayout.jsx index 7b27f810bb..8d76bc09ad 100644 --- a/frontend/src/AppBuilder/AppCanvas/Grid/MobileLayout.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Grid/MobileLayout.jsx @@ -3,6 +3,7 @@ import cx from 'classnames'; import { PAGE_CANVAS_HEADER_HEIGHT } from '../appCanvasConstants'; import { PageCanvasHeader } from './PageCanvasHeader'; +import { PageCanvasFooter } from './PageCanvasFooter'; import MobileNavigationHeader from '../../RightSideBar/PageSettingsTab/PageMenu/MobileNavigationHeader'; import { CanvasContentTail } from './CanvasContentTail'; @@ -11,6 +12,7 @@ export const MobileLayout = ({ // mobileCanvasFrameRef, // mobileNavSheetContainerRef, showCanvasHeader, + showCanvasFooter, isMobileLayout, currentMode, appType, @@ -62,6 +64,7 @@ export const MobileLayout = ({ {mainCanvasContainer} + ); }; diff --git a/frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx b/frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx new file mode 100644 index 0000000000..384d286e4c --- /dev/null +++ b/frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import cx from 'classnames'; +import { shallow } from 'zustand/shallow'; + +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; +import useStore from '@/AppBuilder/_stores/store'; +import useAppDarkMode from '@/_hooks/useAppDarkMode'; +import { CONTAINER_FORM_CANVAS_PADDING, PAGE_CANVAS_FOOTER_HEIGHT } from '../appCanvasConstants'; +import { Container } from '../Container'; +import ConfigHandleButton from '@/_components/ConfigHandleButton'; + +export const PageCanvasFooter = ({ showCanvasFooter, isMobileLayout, currentMode }) => { + const { moduleId } = useModuleContext(); + const currentPageId = useStore((state) => state.modules[moduleId].currentPageId); + const selectedVersion = useStore((state) => state.selectedVersion, shallow); + const isMobilePreviewMode = selectedVersion?.id && isMobileLayout && currentMode === 'view'; + + const footerBackgroundColor = useStore( + (state) => state.modules[moduleId].pages.find((p) => p.id === currentPageId)?.pageFooter?.backgroundColor, + shallow + ); + const footerBorderColor = useStore( + (state) => state.modules[moduleId].pages.find((p) => p.id === currentPageId)?.pageFooter?.borderColor, + shallow + ); + + const setCanvasFooterSelected = useStore((state) => state.setCanvasFooterSelected, shallow); + const isCanvasFooterSelected = useStore((state) => state.isCanvasFooterSelected, shallow); + const clearSelectedComponents = useStore((state) => state.clearSelectedComponents, shallow); + + const { isAppDarkMode } = useAppDarkMode(); + + if (!showCanvasFooter) return null; + + return ( +
{ + if (currentMode === 'edit') { + e.stopPropagation(); + clearSelectedComponents(); + setCanvasFooterSelected(true); + } + }} + style={{ + position: 'sticky', + bottom: 0, + zIndex: 10, + flexShrink: 0, + padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, + height: `${PAGE_CANVAS_FOOTER_HEIGHT}px`, + border: `1px solid ${ + isCanvasFooterSelected ? 'var(--border-accent-strong)' : footerBorderColor ?? 'var(--cc-default-border)' + }`, + backgroundColor: footerBackgroundColor ?? (isAppDarkMode ? '#232E3C' : '#fff'), + width: '100%', + }} + > + {currentMode === 'edit' && ( +
+ + App footer + +
+ )} + +
+ ); +}; diff --git a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx index 52dc0acd79..83a510c4fd 100644 --- a/frontend/src/AppBuilder/AppCanvas/Selecto.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Selecto.jsx @@ -34,8 +34,8 @@ const EditorSelecto = () => { const target = e.inputEvent.target; const componentId = target.getAttribute('component-id'); - // For canvas header, we don't have a specific canvasStartId to track - if (componentId === 'canvas-header') { + // For canvas header/footer, we don't have a specific canvasStartId to track + if (componentId === 'canvas-header' || componentId === 'canvas-footer') { canvasStartId.current = null; return; } @@ -124,8 +124,10 @@ const EditorSelecto = () => { const isAppCanvas = target.getAttribute('component-id') === 'canvas'; const isSubContainer = target.getAttribute('component-id') !== 'canvas' || target.getAttribute('data-parentId'); const isShiftKeyPressed = e.inputEvent.shiftKey; - const isPageCanvasHeader = target.getAttribute('component-id') === 'canvas-header'; - if (isAppCanvas || (isShiftKeyPressed && isSubContainer) || isPageCanvasHeader) { + const isPageCanvasHeaderOrFooter = + target.getAttribute('component-id') === 'canvas-header' || + target.getAttribute('component-id') === 'canvas-footer'; + if (isAppCanvas || (isShiftKeyPressed && isSubContainer) || isPageCanvasHeaderOrFooter) { return true; } diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvas.scss b/frontend/src/AppBuilder/AppCanvas/appCanvas.scss index f19cb03091..a49444f364 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvas.scss +++ b/frontend/src/AppBuilder/AppCanvas/appCanvas.scss @@ -42,7 +42,6 @@ } &.canvas-footer-slot--edit { - &:hover, &.canvas-footer-slot--selected { .canvas-footer-tooltip { display: flex !important; @@ -142,10 +141,15 @@ overflow: visible !important; } -#main-editor-canvas.disable-moveable-line .canvas-header-slot + .canvas-wrapper { - position: relative; - z-index: 11; -} +// #main-editor-canvas.disable-moveable-line .canvas-header-slot + .canvas-wrapper { +// position: relative; +// z-index: 11; +// } + +// #main-editor-canvas.disable-moveable-line .canvas-wrapper + .canvas-footer-slot { +// position: relative; +// z-index: 11; +// } // This is required to maintain the height of the subcontainer when dragging a widget inside it diff --git a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPagePopup.jsx b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPagePopup.jsx index 3d3c82d8b4..03021adda0 100644 --- a/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPagePopup.jsx +++ b/frontend/src/AppBuilder/RightSideBar/PageSettingsTab/PageMenu/AddNewPagePopup.jsx @@ -97,6 +97,8 @@ export const AddEditPagePopup = forwardRef(({ darkMode, ...props }, ref) => { ); const hasCanvasPageHeaderEnabled = useStore((state) => state.license?.featureAccess?.canvasPageHeaderEnabled); + const hasCanvasPageFooterEnabled = useStore((state) => state.license?.featureAccess?.canvasPageFooterEnabled); + const [page, setPage] = useState(editingPage || props?.page); const [pageName, setPageName] = useState(''); const [handle, setHandle] = useState(''); @@ -449,12 +451,24 @@ export const AddEditPagePopup = forwardRef(({ darkMode, ...props }, ref) => {
Page footer
- +
- +