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.

This commit is contained in:
Nakul Nagargade 2026-03-23 20:34:20 +05:30
parent 132fe34f98
commit a73888911d
16 changed files with 188 additions and 13 deletions

View file

@ -257,6 +257,7 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
<MobileLayout
pageKey={pageKey}
showCanvasHeader={showCanvasHeader}
showCanvasFooter={showCanvasFooter}
isMobileLayout={isMobileLayout}
currentMode={currentMode}
appType={appType}
@ -274,6 +275,7 @@ export const AppCanvas = ({ appId, switchDarkMode, darkMode }) => {
isModuleMode={isModuleMode}
isMobileLayout={isMobileLayout}
showCanvasHeader={showCanvasHeader}
showCanvasFooter={showCanvasFooter}
position={position}
isPagesSidebarHidden={isPagesSidebarHidden}
appType={appType}

View file

@ -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}
</CanvasContentTail>
</div>
<PageCanvasFooter showCanvasFooter={showCanvasFooter} isMobileLayout={isMobileLayout} currentMode={currentMode} />
</div>
);

View file

@ -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 = ({
<CanvasContentTail currentMode={currentMode} appType={appType} isAppDarkMode={isAppDarkMode}>
{mainCanvasContainer}
</CanvasContentTail>
<PageCanvasFooter showCanvasFooter={showCanvasFooter} isMobileLayout={isMobileLayout} currentMode={currentMode} />
</div>
);
};

View file

@ -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 (
<div
className={cx('canvas-footer-slot', {
'!tw-w-[450px] tw-mx-auto': isMobileLayout && (currentMode === 'edit' || isMobilePreviewMode),
'canvas-footer-slot--edit': currentMode === 'edit',
'canvas-footer-slot--selected': isCanvasFooterSelected,
})}
component-id="canvas-footer"
onClick={(e) => {
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' && (
<div className="canvas-footer-tooltip">
<ConfigHandleButton
className="no-hover"
customStyles={{
alignItems: 'center',
gap: '6px',
padding: '4px 6px',
borderRadius: '6px',
whiteSpace: 'nowrap',
}}
>
<span style={{ cursor: 'default' }}>App footer</span>
</ConfigHandleButton>
</div>
)}
<Container
id={`${moduleId}-footer`}
canvasHeight={PAGE_CANVAS_FOOTER_HEIGHT / 10}
canvasWidth={window.innerWidth}
darkMode={isAppDarkMode}
allowContainerSelect={false}
styles={{
margin: 0,
backgroundColor: 'transparent',
overflow: 'hidden',
}}
componentType="AppCanvas"
hasNoScroll={true}
/>
</div>
);
};

View file

@ -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;
}

View file

@ -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

View file

@ -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) => {
</div>
<div className="section-header pb-2 pt-2">Page footer</div>
<div className=" d-flex justify-content-between align-items-center pb-2">
<label className="form-label font-weight-400 mb-0">Show page footer on desktop</label>
<label style={{ gap: '6px' }} className="form-label font-weight-400 mb-0 d-flex">
Show page footer on desktop
<LicenseTooltip
message={"You don't have access to page footers. Upgrade your plan to access this feature."}
placement="bottom"
show={!hasCanvasPageFooterEnabled}
>
<div className="d-flex align-items-center">
{!hasCanvasPageFooterEnabled && <SolidIcon name="enterprisecrown" />}
</div>
</LicenseTooltip>
</label>
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={showPageFooterOnDesktop ?? false}
disabled={!hasCanvasPageFooterEnabled}
onChange={(e) => {
const checked = e.target.checked;
togglePageFooter(page?.id, checked, 'desktop');
@ -463,12 +477,24 @@ export const AddEditPagePopup = forwardRef(({ darkMode, ...props }, ref) => {
</label>
</div>
<div className=" d-flex justify-content-between align-items-center pb-2">
<label className="form-label font-weight-400 mb-0">Show page footer on mobile</label>
<label style={{ gap: '6px' }} className="form-label font-weight-400 mb-0 d-flex">
Show page footer on mobile
<LicenseTooltip
message={"You don't have access to page footers. Upgrade your plan to access this feature."}
placement="bottom"
show={!hasCanvasPageFooterEnabled}
>
<div className="d-flex align-items-center">
{!hasCanvasPageFooterEnabled && <SolidIcon name="enterprisecrown" />}
</div>
</LicenseTooltip>
</label>
<label className={`form-switch`}>
<input
className="form-check-input"
type="checkbox"
checked={showPageFooterOnMobile ?? false}
disabled={!hasCanvasPageFooterEnabled}
onChange={(e) => {
const checked = e.target.checked;
togglePageFooter(page?.id, checked, 'mobile');

View file

@ -110,7 +110,7 @@ export const createInspectorSlice = (set, get) => ({
.filter(([key]) => {
const component = getComponentDefinition(key, moduleId);
const parent = component?.component?.parent;
return !parent || parent === 'canvas-header';
return !parent || parent === 'canvas-header' || parent === 'canvas-footer';
})
.map(([key, name]) => {
const component = getComponentDefinition(key, moduleId);

@ -1 +1 @@
Subproject commit 942c7d8b24f57170a13ba6a2a66f80cb3f6553a7
Subproject commit 92a7d18fad216e8c0e08090957baeb8479c73e5c

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPageFooterColumnToPagesTable1767700000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'pages',
new TableColumn({
name: 'page_footer',
type: 'jsonb',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('pages', 'page_footer');
}
}

View file

@ -90,6 +90,9 @@ export class Page {
@Column('jsonb', { name: 'page_header', nullable: true })
pageHeader;
@Column('jsonb', { name: 'page_footer', nullable: true })
pageFooter;
@Column({ name: 'app_id', type: 'varchar', nullable: true }) // Assuming appId is a varchar/string
appId: string | null;

View file

@ -50,6 +50,9 @@ export class CreatePageDto {
@IsOptional()
pageHeader: Record<string, any>;
@IsOptional()
pageFooter: Record<string, any>;
@IsOptional()
isPageGroup: boolean;

View file

@ -830,6 +830,12 @@ export class AppImportExportService {
headerbackgroundColor: 'var(--cc-surface1-surface)',
border: 'var(--cc-default-border)',
},
pageFooter: page.pageFooter || {
showOnDesktop: false,
showOnMobile: false,
footerbackgroundColor: 'var(--cc-surface1-surface)',
border: 'var(--cc-default-border)',
},
autoComputeLayout: page.autoComputeLayout || false,
isPageGroup: page.isPageGroup,
pageGroupIndex: page.pageGroupIndex || null,
@ -1113,6 +1119,7 @@ export class AppImportExportService {
disabled: page.disabled || false,
hidden: page.hidden || false,
pageHeader: page.pageHeader || null,
pageFooter: page.pageFooter || null,
autoComputeLayout: page.autoComputeLayout || false,
icon: page.icon || null,
isPageGroup: !!page.isPageGroup,

View file

@ -221,6 +221,7 @@ export class PageService implements IPageService {
newPage.disabled = pageToClone.disabled;
newPage.hidden = pageToClone.hidden;
newPage.pageHeader = pageToClone.pageHeader;
newPage.pageFooter = pageToClone.pageFooter;
clonedPage = await manager.save(newPage);

View file

@ -96,6 +96,12 @@ export class PageHelperService implements IPageHelperService {
headerbackgroundColor: 'var(--cc-surface1-surface)',
border: 'var(--cc-default-border)',
};
page.pageFooter = {
showOnDesktop: false,
showOnMobile: false,
footerbackgroundColor: 'var(--cc-surface1-surface)',
border: 'var(--cc-default-border)',
};
return page;
}

View file

@ -423,6 +423,7 @@ export class VersionsCreateService implements IVersionsCreateService {
disabled: page.disabled,
hidden: page.hidden,
pageHeader: page.pageHeader,
pageFooter: page.pageFooter,
appVersionId: appVersion.id,
})
);