mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
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:
parent
132fe34f98
commit
a73888911d
16 changed files with 188 additions and 13 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
96
frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx
Normal file
96
frontend/src/AppBuilder/AppCanvas/Grid/PageCanvasFooter.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export class CreatePageDto {
|
|||
@IsOptional()
|
||||
pageHeader: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
pageFooter: Record<string, any>;
|
||||
|
||||
@IsOptional()
|
||||
isPageGroup: boolean;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ export class VersionsCreateService implements IVersionsCreateService {
|
|||
disabled: page.disabled,
|
||||
hidden: page.hidden,
|
||||
pageHeader: page.pageHeader,
|
||||
pageFooter: page.pageFooter,
|
||||
appVersionId: appVersion.id,
|
||||
})
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue