Adds header and footer slots for Form

This commit is contained in:
Nithin David Thomas 2025-03-08 00:25:24 +05:30
parent 679811ad76
commit 3933a9e7a2
6 changed files with 503 additions and 171 deletions

View file

@ -206,6 +206,7 @@ export const getAllChildComponents = (allComponents, parentId) => {
allComponents[parentId]?.component?.component === 'Calendar' ||
allComponents[parentId]?.component?.component === 'Kanban' ||
allComponents[parentId]?.component?.component === 'Container' ||
allComponents[parentId]?.component?.component === 'Form' ||
allComponents[parentId]?.component?.component === 'ModalV2';
if (componentParentId && isParentTabORCalendar) {
@ -327,6 +328,7 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
parentComponent.component.component === 'Tabs' ||
parentComponent.component.component === 'Calendar' ||
parentComponent.component.component === 'Container' ||
parentComponent.component.component === 'Form' ||
parentComponent.component.component === 'ModalV2'
);
}
@ -665,11 +667,14 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
return canvasBgColor;
};
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName }) => {
const { tab } = child;
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
else if (
slotName &&
(parentComponent === 'Form' || parentComponent === 'Container' || parentComponent === 'ModalV2')
) {
return `${parentId}-${slotName}`;
}
return parentId;

View file

@ -19,16 +19,34 @@ export const Form = ({
allComponents,
pages,
}) => {
const properties = Object.keys(componentMeta.properties);
const tempComponentMeta = deepClone(componentMeta);
let properties = [];
let additionalActions = [];
let dataProperties = [];
const events = Object.keys(componentMeta.events);
const validations = Object.keys(componentMeta.validation || {});
const tempComponentMeta = deepClone(componentMeta);
for (const [key] of Object.entries(componentMeta?.properties)) {
if (componentMeta?.properties[key]?.section === 'additionalActions') {
additionalActions.push(key);
} else if (componentMeta?.properties[key]?.accordian === 'Data') {
dataProperties.push(key);
} else {
properties.push(key);
}
}
const { id } = component;
const newOptions = [{ name: 'None', value: 'none' }];
Object.entries(allComponents).forEach(([componentId, component]) => {
if (component.component.parent === id && component?.component?.component === 'Button') {
newOptions.push({ name: component.component.name, value: componentId });
Object.entries(allComponents).forEach(([componentId, _component]) => {
const validParent =
_component.component.parent === id ||
_component.component.parent === `${id}-footer` ||
_component.component.parent === `${id}-header`;
if (validParent && _component?.component?.component === 'Button') {
newOptions.push({ name: _component.component.name, value: componentId });
}
});
@ -48,7 +66,8 @@ export const Form = ({
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
);
return <Accordion items={accordionItems} />;
@ -68,7 +87,8 @@ export const baseComponentProperties = (
allComponents,
validations,
darkMode,
pages
pages,
additionalActions
) => {
let items = [];
if (properties.length > 0) {
@ -90,6 +110,24 @@ export const baseComponentProperties = (
});
}
items.push({
title: 'Additional actions',
isOpen: true,
children: additionalActions?.map((property) =>
renderElement(
component,
componentMeta,
paramUpdated,
dataQueries,
property,
'properties',
currentState,
allComponents,
darkMode
)
),
});
if (events.length > 0) {
items.push({
title: `${i18next.t('widget.common.events', 'Events')}`,

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,22 +358,6 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
@ -317,15 +385,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
buttonToSubmit: { value: '{{"none"}}' },
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -13,6 +13,12 @@ import RenderSchema from './RenderSchema';
import useStore from '@/AppBuilder/_stores/store';
import { shallow } from 'zustand/shallow';
const getCanvasHeight = (height) => {
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
return Math.ceil(parsedHeight);
};
export const Form = function Form(props) {
const {
id,
@ -29,8 +35,25 @@ export const Form = function Form(props) {
dataCy,
} = props;
const childComponents = useStore((state) => state.getChildComponents(id), shallow);
const { visibility, disabledState, borderRadius, borderColor, boxShadow } = styles;
const { buttonToSubmit, loadingState, advanced, JSONSchema } = properties;
const {
borderRadius,
borderColor,
boxShadow,
headerHeight,
footerHeight,
footerBackgroundColor,
headerBackgroundColor,
} = styles;
const {
buttonToSubmit,
loadingState,
advanced,
JSONSchema,
showHeader = false,
showFooter = false,
visibility,
disabledState,
} = properties;
const backgroundColor =
['#fff', '#ffffffff'].includes(styles.backgroundColor) && darkMode ? '#232E3C' : styles.backgroundColor;
const computedStyles = {
@ -40,16 +63,32 @@ export const Form = function Form(props) {
height,
display: visibility ? 'flex' : 'none',
position: 'relative',
overflow: 'hidden auto',
boxShadow,
flexDirection: 'column',
};
const childIdNameMap = useMemo(() => {
return Object.keys(childComponents).reduce((acc, id) => {
const component = childComponents[id]?.component?.component;
return { ...acc, [id]: component?.name };
}, {});
}, [childComponents]);
const formHeader = {
flexShrink: 0,
// height: headerHeight,
padding: '10px 6px',
borderBottom: '1px solid var(--border-weak)',
backgroundColor:
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
};
const formFooter = {
flexShrink: 0,
// height: footerHeight,
padding: '10px 6px',
borderTop: '1px solid var(--border-weak)',
backgroundColor:
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
};
const formContent = {
overflow: 'hidden auto',
display: 'flex',
height: '100%',
padding: '10px 6px',
};
const parentRef = useRef(null);
const childDataRef = useRef({});
@ -58,6 +97,8 @@ export const Form = function Form(props) {
const [isValid, setValidation] = useState(true);
const [uiComponents, setUIComponents] = useState([]);
const mounted = useMounted();
const canvasHeaderHeight = getCanvasHeight(headerHeight) / 10;
const canvasFooterHeight = getCanvasHeight(footerHeight) / 10;
useEffect(() => {
const exposedVariables = {
@ -155,7 +196,7 @@ export const Form = function Form(props) {
};
setExposedVariables(exposedVariables);
setValidation(childValidation);
}, [childrenData, advanced, JSON.stringify(childIdNameMap)]);
}, [childrenData, advanced]);
useEffect(() => {
document.addEventListener('submitForm', handleFormSubmission);
@ -245,105 +286,138 @@ export const Form = function Form(props) {
if (e.target.className === 'real-canvas') onComponentClick(id, component);
}} //Hack, should find a better solution - to prevent losing z index+1 when container element is clicked
>
{loadingState ? (
<div className="p-2" style={{ margin: '0px auto' }}>
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
{showHeader && (
<div style={formHeader}>
<SubContainer
id={`${id}-header`}
canvasHeight={canvasHeaderHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
backgroundColor: 'transparent',
height: headerHeight,
}}
/>
</div>
) : (
<fieldset disabled={disabledState} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
/>
{/* <SubContainer
parentComponent={component}
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
removeComponent={removeComponent}
onOptionChange={function ({ component, optionName, value, componentId }) {
if (componentId) {
onOptionChange({ component, optionName, value, componentId });
}
}}
currentPageId={props.currentPageId}
{...props}
{...containerProps}
height={'100%'} // This height is required since Subcontainer has a issue if height is provided, it stores it in the ref and never updates that ref
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={currentLayout}
/> */}
</div>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
)}
<div className="jet-form-body" style={formContent}>
{loadingState ? (
<div className="p-2" style={{ margin: '0px auto' }}>
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
</div>
) : (
<fieldset disabled={disabledState} style={{ width: '100%' }}>
{!advanced && (
<div className={'json-form-wrapper-disabled'} style={{ width: '100%', height: '100%' }}>
<SubContainer
id={id}
canvasHeight={computedStyles.height}
canvasWidth={width}
onOptionChange={onOptionChange}
onOptionsChange={onOptionsChange}
styles={{ backgroundColor: computedStyles.backgroundColor }}
darkMode={darkMode}
/>
{/* <SubContainer
parentComponent={component}
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
removeComponent={removeComponent}
onOptionChange={function ({ component, optionName, value, componentId }) {
if (componentId) {
onOptionChange({ component, optionName, value, componentId });
}
}}
currentPageId={props.currentPageId}
{...props}
{...containerProps}
height={'100%'} // This height is required since Subcontainer has a issue if height is provided, it stores it in the ref and never updates that ref
/>
<SubCustomDragLayer
containerCanvasWidth={width}
parent={id}
parentRef={parentRef}
currentLayout={currentLayout}
/> */}
</div>
)}
{advanced &&
uiComponents?.map((item, index) => {
return (
<div
//check to avoid labels for these widgets as label is already present for them
className={
![
'Checkbox',
'StarRating',
'Multiselect',
'DropDown',
'RadioButton',
'ToggleSwitch',
'ToggleSwitchV2',
].includes(uiComponents?.[index + 1]?.component)
? `json-form-wrapper json-form-wrapper-disabled`
: `json-form-wrapper json-form-wrapper-disabled form-label-restricted`
}
key={index}
>
<div style={{ position: 'relative' }} className={`form-ele form-${id}-${index}`}>
<RenderSchema
component={item}
parent={id}
id={index}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
</div>
{/* <Box
{...props}
component={item}
parent={id}
id={index}
width={width}
height={item.defaultSize.height}
mode={mode}
inCanvas={true}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentClick={onComponentClick}
darkMode={darkMode}
onOptionChange={onComponentOptionChangedForSubcontainer}
onOptionsChange={onComponentOptionsChangedForSubcontainer}
/>
removeComponent={removeComponent}
// canvasWidth={width}
// readOnly={readOnly}
// customResolvables={customResolvables}
parentId={id}
getContainerProps={getContainerProps}
onOptionChanged={onComponentOptionChangedForSubcontainer}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={true}
/> */}
</div>
{/* <Box
{...props}
component={item}
id={index}
width={width}
height={item.defaultSize.height}
mode={mode}
inCanvas={true}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentClick={onComponentClick}
darkMode={darkMode}
removeComponent={removeComponent}
// canvasWidth={width}
// readOnly={readOnly}
// customResolvables={customResolvables}
parentId={id}
getContainerProps={getContainerProps}
onOptionChanged={onComponentOptionChangedForSubcontainer}
onOptionsChanged={onComponentOptionsChanged}
isFromSubContainer={true}
/> */}
</div>
);
})}
</fieldset>
);
})}
</fieldset>
)}
</div>
{showFooter && (
<div className="jet-form-footer" style={formFooter}>
<SubContainer
id={`${id}-footer`}
canvasHeight={canvasFooterHeight}
canvasWidth={width}
allowContainerSelect={false}
darkMode={darkMode}
styles={{
margin: 0,
backgroundColor: 'transparent',
height: footerHeight,
}}
/>
</div>
)}
</form>
);

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,22 +358,6 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
@ -317,14 +385,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};

View file

@ -4,9 +4,40 @@ export const formConfig = {
description: 'Wrapper for multiple components',
defaultSize: {
width: 13,
height: 330,
height: 480,
},
defaultChildren: [
{
componentName: 'Text',
slotName: 'header',
layout: {
top: 10,
left: 1,
height: 40,
},
properties: ['text'],
accessorKey: 'text',
styles: ['fontWeight', 'textSize', 'textColor'],
defaultValue: {
text: 'Form title',
textSize: 20,
textColor: '#000',
},
},
{
componentName: 'Button',
slotName: 'footer',
layout: {
top: 12,
left: 32,
height: 36,
},
properties: ['text'],
defaultValue: {
text: 'Button2',
padding: 'none',
},
},
{
componentName: 'Text',
layout: {
@ -225,6 +256,7 @@ export const formConfig = {
loadingState: {
type: 'toggle',
displayName: 'Loading state',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
@ -242,12 +274,64 @@ export const formConfig = {
value: true,
},
},
showHeader: { type: 'toggle', displayName: 'Header' },
showFooter: { type: 'toggle', displayName: 'Footer' },
visibility: {
type: 'toggle',
displayName: 'Visibility',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
section: 'additionalActions',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
events: {
onSubmit: { displayName: 'On submit' },
onInvalid: { displayName: 'On invalid' },
},
styles: {
headerBackgroundColor: {
type: 'color',
displayName: 'Header background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
footerBackgroundColor: {
type: 'color',
displayName: 'Footer background color',
validation: {
schema: { type: 'string' },
defaultValue: '#ffffffff',
},
},
headerHeight: {
type: 'code',
displayName: 'Header height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
footerHeight: {
type: 'code',
displayName: 'Footer height',
validation: {
schema: { type: 'string' },
defaultValue: '80px',
},
},
backgroundColor: {
type: 'color',
displayName: 'Background color',
@ -274,22 +358,6 @@ export const formConfig = {
defaultValue: '#fff',
},
},
visibility: {
type: 'toggle',
displayName: 'Visibility',
validation: {
schema: { type: 'boolean' },
defaultValue: true,
},
},
disabledState: {
type: 'toggle',
displayName: 'Disable',
validation: {
schema: { type: 'boolean' },
defaultValue: false,
},
},
},
exposedVariables: {
data: {},
@ -317,14 +385,18 @@ export const formConfig = {
value:
"{{ {title: 'User registration form', properties: {firstname: {type: 'textinput',value: 'Maria',label:'First name', validation:{maxLength:6}, styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},lastname:{type: 'textinput',value: 'Doe', label:'Last name', styles: {backgroundColor: '#f6f5ff',textColor: 'black'},},age:{type:'number', label:'Age'},}, submitButton: {value: 'Submit', styles: {backgroundColor: '#3a433b',borderColor:'#595959'}}} }}",
},
showHeader: { value: '{{false}}' },
showFooter: { value: '{{false}}' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
},
events: [],
styles: {
backgroundColor: { value: '#fff' },
borderRadius: { value: '0' },
borderColor: { value: '#fff' },
visibility: { value: '{{true}}' },
disabledState: { value: '{{false}}' },
headerHeight: { value: '60px' },
footerHeight: { value: '60px' },
},
},
};