mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-05 22:38:48 +00:00
Merge pull request #12122 from ToolJet/modularisation/modalV2
feat: Revamps modal widget to ModalV2 with header and footer (#11963)
This commit is contained in:
commit
dc248c831d
46 changed files with 2263 additions and 146 deletions
|
|
@ -137,6 +137,7 @@ const WidgetIcon = (props) => {
|
|||
case 'map':
|
||||
return <Map {...props} />;
|
||||
case 'modal':
|
||||
case 'modallegacy':
|
||||
return <Modal {...props} />;
|
||||
case 'multiselect':
|
||||
case 'multiselectv2':
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const ConfigHandle = ({
|
|||
);
|
||||
|
||||
const setComponentToInspect = useStore((state) => state.setComponentToInspect);
|
||||
|
||||
const isModal = componentType === 'Modal' || componentType === 'ModalV2';
|
||||
const _showHandle = useStore((state) => {
|
||||
const isWidgetHovered = state.getHoveredComponentForGrid() === id || state.hoveredComponentBoundaryId === id;
|
||||
const anyComponentHovered = state.getHoveredComponentForGrid() !== '' || state.hoveredComponentBoundaryId !== '';
|
||||
|
|
@ -37,7 +37,7 @@ export const ConfigHandle = ({
|
|||
return (
|
||||
isWidgetHovered ||
|
||||
(showHandle &&
|
||||
(!isMultipleComponentsSelected || (componentType === 'Modal' && isModalOpen)) &&
|
||||
(!isMultipleComponentsSelected || (isModal && isModalOpen)) &&
|
||||
!anyComponentHovered)
|
||||
);
|
||||
}, shallow);
|
||||
|
|
@ -63,7 +63,7 @@ export const ConfigHandle = ({
|
|||
>
|
||||
<span
|
||||
style={{
|
||||
background: componentType === 'Modal' && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
background: isModal && isModalOpen ? '#c6cad0' : '#4D72FA',
|
||||
}}
|
||||
className="badge handle-content"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from './gridUtils';
|
||||
import { useAppVersionStore } from '@/_stores/appVersionStore';
|
||||
import { resolveWidgetFieldValue } from '@/_helpers/utils';
|
||||
import { dragContextBuilder, getAdjustedDropPosition } from './helpers/dragEnd';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import './Grid.css';
|
||||
import { NO_OF_GRIDS, SUBCONTAINER_WIDGETS } from '../appCanvasConstants';
|
||||
|
|
@ -54,6 +55,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 [canvasBounds, setCanvasBounds] = useState(CANVAS_BOUNDS);
|
||||
const draggingComponentId = useGridStore((state) => state.draggingComponentId, shallow);
|
||||
const resizingComponentId = useGridStore((state) => state.resizingComponentId, shallow);
|
||||
const [dragParentId, setDragParentId] = useState(null);
|
||||
|
|
@ -794,7 +796,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
// to handle their own interactions like column resizing or card dragging
|
||||
let isDragOnInnerElement = false;
|
||||
|
||||
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
|
||||
/* If the drag or click is on a calender popup draggable interactions are not executed so that popups and other components inside calender popup works.
|
||||
Also user dont need to drag an calender from using popup */
|
||||
if (hasParentWithClass(e.inputEvent.target, 'react-datepicker-popper')) {
|
||||
return false;
|
||||
|
|
@ -835,109 +837,47 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
newDragParentId.current = null;
|
||||
setDragParentId(null);
|
||||
|
||||
if (!e.lastEvent) {
|
||||
return;
|
||||
if (!e.lastEvent) return;
|
||||
|
||||
// Build the drag context from the event
|
||||
const dragContext = dragContextBuilder({ event: e, widgets: boxList });
|
||||
const { target, source, dragged } = dragContext;
|
||||
|
||||
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';
|
||||
|
||||
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 }]);
|
||||
} else {
|
||||
const sourcegridWidth = useGridStore.getState().subContainerWidths[source.slotId] || gridWidth;
|
||||
|
||||
left = dragged.left * sourcegridWidth;
|
||||
top = dragged.top;
|
||||
|
||||
!isModalToCanvas ??
|
||||
toast.error(`${dragged.widgetType} is not compatible as a child component of ${target.widgetType}`);
|
||||
}
|
||||
|
||||
let draggedOverElemId = boxList.find((box) => box.id === e.target.id)?.parent;
|
||||
let draggedOverElemIdType;
|
||||
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
|
||||
let draggedOverElem;
|
||||
if (document.elementFromPoint(e.clientX, e.clientY) && parentComponent?.component?.component !== 'Modal') {
|
||||
const targetElems = document.elementsFromPoint(e.clientX, e.clientY);
|
||||
draggedOverElem = targetElems.find((ele) => {
|
||||
const isOwnChild = e.target.contains(ele); // if the hovered element is a child of actual draged element its not considered
|
||||
if (isOwnChild) return false;
|
||||
// Apply transform for smooth transition
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
||||
let isDroppable = ele.id !== e.target.id && ele.classList.contains('drag-container-parent');
|
||||
if (isDroppable) {
|
||||
let widgetId = ele?.getAttribute('component-id') || ele.id;
|
||||
let widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
||||
if (!widgetType) {
|
||||
widgetId = widgetId.split('-').slice(0, -1).join('-');
|
||||
widgetType = boxList.find(({ id }) => id === widgetId)?.component?.component;
|
||||
}
|
||||
if (
|
||||
!['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'Listview', 'Container', 'Table'].includes(
|
||||
widgetType
|
||||
)
|
||||
) {
|
||||
isDroppable = false;
|
||||
}
|
||||
}
|
||||
return isDroppable;
|
||||
});
|
||||
draggedOverElemId = draggedOverElem?.getAttribute('component-id') || draggedOverElem?.id;
|
||||
draggedOverElemIdType = draggedOverElem?.getAttribute('data-parent-type');
|
||||
}
|
||||
|
||||
const _gridWidth = useGridStore.getState().subContainerWidths[draggedOverElemId] || gridWidth;
|
||||
const currentParentId = boxList.find(({ id: widgetId }) => e.target.id === widgetId)?.component?.parent;
|
||||
let left = e.lastEvent?.translate[0];
|
||||
let top = e.lastEvent?.translate[1];
|
||||
if (
|
||||
['Listview', 'Kanban', 'Container'].includes(
|
||||
boxList.find((box) => box.id === draggedOverElemId)?.component?.component
|
||||
)
|
||||
) {
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
top = top > maxY ? maxY : top;
|
||||
}
|
||||
|
||||
const currentWidget = boxList.find(({ id }) => id === e.target.id)?.component?.component;
|
||||
const parentId = draggedOverElemId?.length > 36 ? draggedOverElemId.slice(0, 36) : draggedOverElemId;
|
||||
draggedOverElemIdType = getComponentTypeFromId(parentId);
|
||||
const parentWidget = draggedOverElemIdType === 'Kanban' ? 'Kanban_card' : draggedOverElemIdType;
|
||||
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
|
||||
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
|
||||
if (draggedOverElemId !== currentParentId) {
|
||||
if (isParentChangeAllowed) {
|
||||
const draggedOverWidget = boxList.find((box) => box.id === draggedOverElemId);
|
||||
|
||||
let parentWidgetType = boxList.find((box) => box.id === draggedOverElemId)?.component?.component;
|
||||
// @TODO - When dropping back to container from canvas, the boxList doesn't have canvas header,
|
||||
// boxList will return null. But we need to tell getMouseDistanceFromParentDiv parentWidgetType is container
|
||||
// As container id is like 'canvas-2375e23765e-123234'
|
||||
if (parentId && !parentWidgetType && draggedOverElemId.includes('-header')) {
|
||||
parentWidgetType = 'Container';
|
||||
}
|
||||
|
||||
let { left: _left, top: _top } = getMouseDistanceFromParentDiv(
|
||||
e,
|
||||
draggedOverWidget?.component?.component === 'Kanban' ? draggedOverElem : draggedOverElemId,
|
||||
parentWidgetType
|
||||
);
|
||||
left = _left;
|
||||
top = _top;
|
||||
} else {
|
||||
const currBox = boxList.find((l) => l.id === e.target.id);
|
||||
left = currBox.left * gridWidth;
|
||||
top = currBox.top;
|
||||
toast.error(`${currentWidget} is not compatible as a child component of ${parentWidget}`);
|
||||
}
|
||||
}
|
||||
|
||||
e.target.style.transform = `translate(${Math.round(left / _gridWidth) * _gridWidth}px, ${
|
||||
Math.round(top / GRID_HEIGHT) * GRID_HEIGHT
|
||||
}px)`;
|
||||
if (draggedOverElemId === currentParentId || isParentChangeAllowed) {
|
||||
handleDragEnd([
|
||||
{
|
||||
id: e.target.id,
|
||||
x: left,
|
||||
y: Math.round(top / GRID_HEIGHT) * GRID_HEIGHT,
|
||||
parent: isParentChangeAllowed ? draggedOverElemId : undefined,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const box = boxList.find((box) => box.id === e.target.id);
|
||||
//
|
||||
setTimeout(() => setSelectedComponents([box.id]));
|
||||
// Select the dragged component after drop
|
||||
setTimeout(() => setSelectedComponents([dragged.id]));
|
||||
} catch (error) {
|
||||
console.log('draggedOverElemId->error', error);
|
||||
console.error('Error in onDragEnd:', error);
|
||||
}
|
||||
setCanvasBounds({ ...CANVAS_BOUNDS });
|
||||
hideGridLines();
|
||||
toggleCanvasUpdater();
|
||||
}}
|
||||
|
|
@ -965,16 +905,31 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
}
|
||||
|
||||
// Special case for Modal
|
||||
const parentComponent = boxList.find((box) => box.id === boxList.find((b) => b.id === e.target.id)?.parent);
|
||||
if (parentComponent?.component?.component === 'Modal') {
|
||||
const elemContainer = e.target.closest('.real-canvas');
|
||||
const containerHeight = elemContainer.clientHeight;
|
||||
const containerWidth = elemContainer.clientWidth;
|
||||
const maxY = containerHeight - e.target.clientHeight;
|
||||
const maxLeft = containerWidth - e.target.clientWidth;
|
||||
const oldParentId = boxList.find((b) => b.id === e.target.id)?.parent;
|
||||
const parentId = oldParentId?.length > 36 ? oldParentId.slice(0, 36) : oldParentId;
|
||||
const parentComponent = boxList.find((box) => box.id === parentId);
|
||||
const parentWidgetType = parentComponent?.component?.component;
|
||||
const isOnHeaderOrFooter = oldParentId
|
||||
? oldParentId.includes('-header') || oldParentId.includes('-footer')
|
||||
: false;
|
||||
const isParentModalSlot = parentWidgetType === 'ModalV2' && isOnHeaderOrFooter;
|
||||
const isParentNewModal = parentComponent?.component?.component === 'ModalV2';
|
||||
const isParentLegacyModal = parentComponent?.component?.component === 'Modal';
|
||||
const isParentModal = isParentNewModal || isParentLegacyModal || isParentModalSlot;
|
||||
|
||||
top = top < 0 ? 0 : top > maxY ? maxY : top;
|
||||
left = left < 0 ? 0 : left > maxLeft ? maxLeft : left;
|
||||
if (isParentModal) {
|
||||
const modalContainer = e.target.closest('.tj-modal-widget-content');
|
||||
const mainCanvas = document.getElementById('real-canvas');
|
||||
|
||||
const mainRect = mainCanvas.getBoundingClientRect();
|
||||
const modalRect = modalContainer.getBoundingClientRect();
|
||||
const relativePosition = {
|
||||
top: modalRect.top - mainRect.top,
|
||||
right: mainRect.right - modalRect.right + modalContainer.offsetWidth,
|
||||
bottom: modalRect.height + (modalRect.top - mainRect.top),
|
||||
left: modalRect.left - mainRect.left,
|
||||
};
|
||||
setCanvasBounds({ ...relativePosition });
|
||||
}
|
||||
|
||||
e.target.style.transform = `translate(${left}px, ${top}px)`;
|
||||
|
|
@ -1063,6 +1018,7 @@ export default function Grid({ gridWidth, currentLayout }) {
|
|||
snapGap={false}
|
||||
isDisplaySnapDigit={false}
|
||||
snapThreshold={GRID_HEIGHT}
|
||||
bounds={canvasBounds}
|
||||
// Guidelines configuration
|
||||
elementGuidelines={elementGuidelines}
|
||||
snapDirections={{
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ export function getMouseDistanceFromParentDiv(event, id, parentWidgetType) {
|
|||
? document.getElementById(id)
|
||||
: id
|
||||
: document.getElementsByClassName('real-canvas')[0];
|
||||
parentDiv = id === 'real-canvas' ? document.getElementById('real-canvas') : document.getElementById('canvas-' + id);
|
||||
if (parentWidgetType === 'Container' || parentWidgetType === 'Modal') {
|
||||
parentDiv = document.getElementById('canvas-' + id);
|
||||
}
|
||||
|
|
|
|||
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
266
frontend/src/AppBuilder/AppCanvas/Grid/helpers/dragEnd.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/**
|
||||
* Drag Context Breakdown:
|
||||
*
|
||||
* This object encapsulates all relevant details about a drag event,
|
||||
* grouping the **source (where the widget came from)** and **target (where it's being dropped)**.
|
||||
*
|
||||
* Core Concepts:
|
||||
* - `draggedWidget` → The widget being dragged (`e.target`).
|
||||
* - `sourceSlot` → The original parent container of `draggedWidget`.
|
||||
* - This could be a **header, footer, or a sub-container (like a container body)**.
|
||||
* - `targetSlot` → The new parent container where `draggedWidget` is dropped.
|
||||
* - `sourceWidget` → The **widget that owns** `sourceSlot` (its direct parent).
|
||||
* - `targetWidget` → The **widget that owns** `targetSlot` (its direct parent).
|
||||
*
|
||||
* These entities are structured into a **contextual grouping**, allowing for easy access:
|
||||
*
|
||||
* {
|
||||
* source: {
|
||||
* widget: sourceWidget, // The original widget that holds the source slot.
|
||||
* slot: sourceSlot, // The slot where the widget was initially located.
|
||||
* id: sourceWidget.id, // Unique identifier of the source widget.
|
||||
* slotId: sourceSlot.id, // Unique identifier of the source slot.
|
||||
*
|
||||
* isModal: computed function, // Checks if sourceWidget is a Modal.
|
||||
* slotType: computed function, // Determines if the slot is a header, footer, or body.
|
||||
* widgetType: computed function, // Returns the type of the widget (e.g., Table, Form, etc.).
|
||||
* },
|
||||
*
|
||||
* target: {
|
||||
* widget: targetWidget, // The new widget where the dragged widget is being placed.
|
||||
* slot: targetSlot, // The slot inside `targetWidget` where the drop is happening.
|
||||
* id: targetWidget.id, // Unique identifier of the target widget.
|
||||
* slotId: targetSlot.id, // Unique identifier of the target slot.
|
||||
*
|
||||
* isModal: computed function, // Checks if targetWidget is a Modal.
|
||||
* slotType: computed function, // Determines if the slot is a header, footer, or body.
|
||||
* widgetType: computed function, // Returns the type of the target widget.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* Additional Checks:
|
||||
* - `isSourceModal` → **Is the source inside a modal?**
|
||||
* - `isTargetModal` → **Is the target inside a modal?**
|
||||
* - `isDraggingToModalSlots` → **Is the widget being dragged into a modal slot (header/footer)?**
|
||||
* - `targetSlotType` → **Determines whether the drop is happening in a header, footer, or body.**
|
||||
*
|
||||
* Why This Matters?
|
||||
* - This structure helps **validate and restrict movements**, ensuring widgets follow UI constraints.
|
||||
* - Prevents invalid drops (e.g., putting a button inside a Table component).
|
||||
* - Enables **modular and flexible** widget movement across different UI sections.
|
||||
*/
|
||||
import { getMouseDistanceFromParentDiv } from '../gridUtils';
|
||||
import {
|
||||
RESTRICTED_WIDGETS_CONFIG,
|
||||
RESTRICTED_WIDGET_SLOTS_CONFIG,
|
||||
} from '@/AppBuilder/WidgetManager/configs/restrictedWidgetsConfig';
|
||||
|
||||
const CANVAS_ID = 'canvas';
|
||||
const REAL_CANVAS_ID = 'real-canvas';
|
||||
|
||||
/**
|
||||
* Represents the widget being dragged.
|
||||
*
|
||||
* This class encapsulates all necessary information about the dragged widget,
|
||||
* including its type, position, and whether it is allowed to move into certain areas.
|
||||
*/
|
||||
export class DragEntity {
|
||||
constructor(widget) {
|
||||
this.widget = widget; // The widget object being dragged
|
||||
this.id = widget?.id || null; // Unique ID of the dragged widget
|
||||
this.left = widget.left; // Initial X position (relative to grid)
|
||||
this.top = widget.top; // Initial Y position (relative to grid)
|
||||
}
|
||||
|
||||
get widgetType() {
|
||||
return this.widget?.component?.component || null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines a **droppable area** in the canvas.
|
||||
*
|
||||
* A droppable area is a container that can accept dragged widgets.
|
||||
* This class helps determine if a slot is valid and handles various properties like modals.
|
||||
*/
|
||||
export class DropAreaEntity {
|
||||
static dropAreaWidgets = ['Calendar', 'Kanban', 'Form', 'Tabs', 'Modal', 'ModalV2', 'Listview', 'Container', 'Table'];
|
||||
|
||||
constructor(widget, slotId) {
|
||||
this.widget = widget; // The widget that owns this slot
|
||||
this.id = widget?.id || CANVAS_ID; // ID of the widget
|
||||
this.slotId = slotId || REAL_CANVAS_ID; // ID of the slot where the widget is located
|
||||
}
|
||||
|
||||
// Checks if the widget is a modal
|
||||
get isModal() {
|
||||
return ['Modal', 'ModalV2'].includes(this.widget?.component?.component);
|
||||
}
|
||||
|
||||
// Checks if the widget is the new version of modal
|
||||
get isNewModal() {
|
||||
return this.widget?.component?.component === 'ModalV2';
|
||||
}
|
||||
|
||||
// Checks if the widget is the legacy modal
|
||||
get isLegacyModal() {
|
||||
return this.widget?.component?.component === 'Modal';
|
||||
}
|
||||
|
||||
// Determines if the slot belongs to a modal's header/footer
|
||||
get isInModalSlot() {
|
||||
return this.isNewModal && this.isOnCustomSlot;
|
||||
}
|
||||
|
||||
// Identifies if the slot is a custom slot (e.g., modal header/footer)
|
||||
get isOnCustomSlot() {
|
||||
return this.slotId.includes('-header') || this.slotId.includes('-footer');
|
||||
}
|
||||
|
||||
// Determines if the slot is a valid drop target
|
||||
get isDroppable() {
|
||||
return DropAreaEntity.dropAreaWidgets.includes(this.widgetType);
|
||||
}
|
||||
|
||||
// Returns the type of slot (header, footer, body, etc.)
|
||||
get slotType() {
|
||||
return this.slotId ? this.slotId.split('-').pop() : CANVAS_ID;
|
||||
}
|
||||
|
||||
// Returns the type of the widget inside the slot
|
||||
get widgetType() {
|
||||
return this.widget?.component?.component || CANVAS_ID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the **dragging context**, encapsulating information
|
||||
* about the source, target, and the dragged widget.
|
||||
*
|
||||
* This helps determine:
|
||||
* - Whether the move is valid
|
||||
* - Where the widget should be placed
|
||||
* - Any restrictions based on parent-child relationships
|
||||
*/
|
||||
export class DragContext {
|
||||
constructor({ sourceSlotId, targetSlotId, draggedWidgetId, widgets }) {
|
||||
const sourceWidgetId = sourceSlotId?.slice(0, 36);
|
||||
const sourceWidget = getWidgetById(widgets, sourceWidgetId);
|
||||
|
||||
const targetWidgetId = targetSlotId?.slice(0, 36);
|
||||
const targetWidget = getWidgetById(widgets, targetWidgetId);
|
||||
|
||||
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
|
||||
|
||||
this.source = new DropAreaEntity(sourceWidget, sourceSlotId);
|
||||
this.target = new DropAreaEntity(targetWidget, targetSlotId);
|
||||
this.dragged = new DragEntity(draggedWidget);
|
||||
this.widgets = widgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the **target slot** dynamically as the drag event progresses.
|
||||
*/
|
||||
updateTarget(targetSlotId) {
|
||||
const targetWidgetId = targetSlotId?.slice(0, 36);
|
||||
const targetWidget = getWidgetById(this.widgets, targetWidgetId);
|
||||
this.target = new DropAreaEntity(targetWidget, targetSlotId);
|
||||
}
|
||||
|
||||
get isDroppable() {
|
||||
const { dragged, target } = this;
|
||||
|
||||
const restrictedWidgetsOnTarget = RESTRICTED_WIDGETS_CONFIG?.[target.widgetType] || [];
|
||||
const restrictedWidgetsOnTargetSlot = RESTRICTED_WIDGET_SLOTS_CONFIG?.[target.slotType] || [];
|
||||
|
||||
const restrictedWidgets = [...restrictedWidgetsOnTarget, ...restrictedWidgetsOnTargetSlot];
|
||||
return !restrictedWidgets.includes(dragged.widgetType);
|
||||
ß;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the **dragging context** by gathering all relevant details from the event.
|
||||
*/
|
||||
export function dragContextBuilder({ event, widgets }) {
|
||||
const draggedWidgetId = event.target.id;
|
||||
const draggedWidget = getWidgetById(widgets, draggedWidgetId);
|
||||
const sourceSlotId = draggedWidget.parent;
|
||||
|
||||
// Initialize drag context
|
||||
const context = new DragContext({ widgets, draggedWidgetId, sourceSlotId, targetSlotId: sourceSlotId });
|
||||
|
||||
// Determine the potential drop target
|
||||
const targetSlotId = getDroppableSlotIdOnScreen(event, widgets);
|
||||
context.updateTarget(targetSlotId);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event, finds the **nearest valid droppable slot**.
|
||||
*/
|
||||
export const getDroppableSlotIdOnScreen = (event, widgets) => {
|
||||
const [slotId] = document
|
||||
.elementsFromPoint(event.clientX, event.clientY)
|
||||
.filter(
|
||||
(ele) =>
|
||||
!event.target.contains(ele) && ele.id !== event.target.id && ele.classList.contains('drag-container-parent')
|
||||
)
|
||||
.map((ele) => extractSlotId(ele))
|
||||
.filter((slotId) => {
|
||||
const widgetType = getWidgetById(widgets, slotId.slice(0, 36))?.component?.component || CANVAS_ID;
|
||||
return DropAreaEntity.dropAreaWidgets.includes(widgetType);
|
||||
});
|
||||
|
||||
return slotId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds a widget by its ID.
|
||||
*/
|
||||
export function getWidgetById(boxList, targetId) {
|
||||
return boxList.find((box) => box.id === targetId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the **slot ID** from a given DOM element.
|
||||
*/
|
||||
const extractSlotId = (element) => {
|
||||
return element?.getAttribute('component-id') || element.id.replace(/^canvas-/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Computes the final (left, top) position for a dragged widget based on grid snapping and drop conditions.
|
||||
*
|
||||
* @param {Object} event - Drag event object containing movement data.
|
||||
* @param {DropAreaEntity} target - The target drop area entity (where widget is dropped).
|
||||
* @param {boolean} isParentChangeAllowed - Whether the widget can move to the target.
|
||||
* @param {number} gridWidth - The width of the grid for alignment.
|
||||
* @param {DragEntity} dragged - The entity being dragged.
|
||||
* @returns {Object} { left, top } - The computed position.
|
||||
*/
|
||||
export const getAdjustedDropPosition = (event, target, isParentChangeAllowed, gridWidth, dragged) => {
|
||||
let left = event.lastEvent?.translate[0];
|
||||
let top = event.lastEvent?.translate[1];
|
||||
|
||||
if (isParentChangeAllowed) {
|
||||
// Compute the relative position inside the new container
|
||||
const { left: adjustedLeft, top: adjustedTop } = getMouseDistanceFromParentDiv(
|
||||
event,
|
||||
target.slotId,
|
||||
target.widgetType
|
||||
);
|
||||
|
||||
return {
|
||||
left: Math.round(adjustedLeft / gridWidth) * gridWidth, // Snap to the nearest grid column
|
||||
top: Math.round(adjustedTop / 10) * 10, // Snap to the nearest 10px
|
||||
};
|
||||
}
|
||||
|
||||
// If movement is restricted, revert to original position
|
||||
return {
|
||||
left: dragged.left * gridWidth,
|
||||
top: dragged.top,
|
||||
};
|
||||
};
|
||||
|
|
@ -6,7 +6,7 @@ export const CANVAS_WIDTHS = Object.freeze({
|
|||
rightSideBarWidth: 300,
|
||||
});
|
||||
|
||||
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container'];
|
||||
export const WIDGETS_WITH_DEFAULT_CHILDREN = ['Listview', 'Tabs', 'Form', 'Kanban', 'Container', 'ModalV2'];
|
||||
|
||||
export const DEFAULT_CANVAS_WIDTH = 1292;
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,8 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
|
|||
const defaultChildren = deepClone(parentMeta)['defaultChildren'];
|
||||
|
||||
defaultChildren.forEach((child) => {
|
||||
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles } = child;
|
||||
const { componentName, layout, incrementWidth, properties, accessorKey, tab, defaultValue, styles, slotName } =
|
||||
child;
|
||||
|
||||
const componentMeta = deepClone(componentTypes.find((component) => component.component === componentName));
|
||||
const componentData = JSON.parse(JSON.stringify(componentMeta));
|
||||
|
|
@ -139,7 +140,12 @@ export function addChildrenWidgetsToParent(componentType, parentId, currentLayou
|
|||
}
|
||||
|
||||
const nonActiveLayout = currentLayout === 'desktop' ? 'mobile' : 'desktop';
|
||||
const _parent = getParentComponentIdByType(child, parentMeta.component, parentId);
|
||||
const _parent = getParentComponentIdByType({
|
||||
child,
|
||||
parentComponent: parentMeta.component,
|
||||
parentId,
|
||||
slotName,
|
||||
});
|
||||
|
||||
const newChildComponent = {
|
||||
id: uuidv4(),
|
||||
|
|
@ -199,7 +205,8 @@ export const getAllChildComponents = (allComponents, parentId) => {
|
|||
allComponents[parentId]?.component?.component === 'Tabs' ||
|
||||
allComponents[parentId]?.component?.component === 'Calendar' ||
|
||||
allComponents[parentId]?.component?.component === 'Kanban' ||
|
||||
allComponents[parentId]?.component?.component === 'Container';
|
||||
allComponents[parentId]?.component?.component === 'Container' ||
|
||||
allComponents[parentId]?.component?.component === 'ModalV2';
|
||||
|
||||
if (componentParentId && isParentTabORCalendar) {
|
||||
let childComponent = deepClone(allComponents[componentId]);
|
||||
|
|
@ -319,7 +326,8 @@ const isChildOfTabsOrCalendar = (component, allComponents = [], componentParentI
|
|||
return (
|
||||
parentComponent.component.component === 'Tabs' ||
|
||||
parentComponent.component.component === 'Calendar' ||
|
||||
parentComponent.component.component === 'Container'
|
||||
parentComponent.component.component === 'Container' ||
|
||||
parentComponent.component.component === 'ModalV2'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -657,10 +665,23 @@ export const computeViewerBackgroundColor = (isAppDarkMode, canvasBgColor) => {
|
|||
return canvasBgColor;
|
||||
};
|
||||
|
||||
export const getParentComponentIdByType = (child, parentComponent, parentId) => {
|
||||
export const getParentComponentIdByType = ({ child, parentComponent, parentId, slotName = 'header' }) => {
|
||||
const { tab } = child;
|
||||
|
||||
if (parentComponent === 'Tabs') return `${parentId}-${tab}`;
|
||||
else if (parentComponent === 'Container') return `${parentId}-header`;
|
||||
else if (parentComponent === 'Container' || parentComponent === 'ModalV2') {
|
||||
return `${parentId}-${slotName}`;
|
||||
}
|
||||
return parentId;
|
||||
};
|
||||
|
||||
export const getParentWidgetFromId = (parentType, parentId) => {
|
||||
const isAddingToSlot = parentId?.includes('-header') || parentId?.includes('-footer');
|
||||
|
||||
if (parentType === 'ModalV2' && isAddingToSlot) {
|
||||
return 'ModalSlot';
|
||||
} else if (parentType === 'Kanban') {
|
||||
return 'Kanban_card';
|
||||
}
|
||||
return parentType;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export const ComponentsManagerTab = ({ darkMode }) => {
|
|||
'StarRating',
|
||||
];
|
||||
const integrationItems = ['Map'];
|
||||
const layoutItems = ['Container', 'Listview', 'Tabs', 'Modal'];
|
||||
const layoutItems = ['Container', 'Listview', 'Tabs', 'ModalV2'];
|
||||
|
||||
filteredComponents.forEach((f) => {
|
||||
if (commonItems.includes(f)) commonSection.items.push(f);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
export const LEGACY_ITEMS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const SHOW_ADDITIONAL_ACTIONS = [
|
|||
'Button',
|
||||
'RichTextEditor',
|
||||
'Image',
|
||||
'ModalV2',
|
||||
];
|
||||
const PROPERTIES_VS_ACCORDION_TITLE = {
|
||||
Text: 'Data',
|
||||
|
|
@ -34,6 +35,7 @@ const PROPERTIES_VS_ACCORDION_TITLE = {
|
|||
Button: 'Data',
|
||||
Image: 'Data',
|
||||
Container: 'Data',
|
||||
ModalV2: 'Data',
|
||||
};
|
||||
|
||||
export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
|
||||
|
|
@ -151,7 +153,8 @@ export const baseComponentProperties = (
|
|||
'properties',
|
||||
currentState,
|
||||
allComponents,
|
||||
darkMode
|
||||
darkMode,
|
||||
''
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import React from 'react';
|
||||
import Accordion from '@/_ui/Accordion';
|
||||
import { renderElement } from '../Utils';
|
||||
import { baseComponentProperties } from './DefaultComponent';
|
||||
import { resolveReferences } from '@/_helpers/utils';
|
||||
|
||||
const INDEX_OF_TRIGGER = 2;
|
||||
|
||||
export const ModalV2 = ({ componentMeta, darkMode, ...restProps }) => {
|
||||
const {
|
||||
layoutPropertyChanged,
|
||||
component,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
currentState,
|
||||
eventsChanged,
|
||||
apps,
|
||||
allComponents,
|
||||
} = restProps;
|
||||
|
||||
let properties = [];
|
||||
let additionalActions = [];
|
||||
let dataProperties = [];
|
||||
|
||||
const events = Object.keys(componentMeta.events);
|
||||
const validations = Object.keys(componentMeta.validation || {});
|
||||
|
||||
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 renderCustomElement = (param, paramType = 'properties') => {
|
||||
return renderElement(component, componentMeta, paramUpdated, dataQueries, param, paramType, currentState);
|
||||
};
|
||||
const conditionalAccordionItems = (component) => {
|
||||
const useDefaultButton = resolveReferences(
|
||||
component.component.definition.properties.useDefaultButton?.value ?? false
|
||||
);
|
||||
const accordionItems = [];
|
||||
let renderOptions = [];
|
||||
const options = ['visibility', 'disabledTrigger', 'useDefaultButton'];
|
||||
|
||||
options.map((option) => renderOptions.push(renderCustomElement(option)));
|
||||
|
||||
const conditionalOptions = [{ name: 'triggerButtonLabel', condition: useDefaultButton }];
|
||||
|
||||
conditionalOptions.map(({ name, condition }) => {
|
||||
if (condition) renderOptions.push(renderCustomElement(name));
|
||||
});
|
||||
|
||||
accordionItems.push({
|
||||
title: 'Trigger',
|
||||
children: renderOptions,
|
||||
});
|
||||
|
||||
return accordionItems;
|
||||
};
|
||||
|
||||
if (component.component.definition.properties.size.value === 'fullscreen') {
|
||||
component.component.properties.modalHeight = {
|
||||
...component.component.properties.modalHeight,
|
||||
isHidden: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (component.component.definition.properties.showHeader.value === '{{false}}') {
|
||||
component.component.properties.headerHeight = {
|
||||
...component.component.properties.headerHeight,
|
||||
isHidden: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (component.component.definition.properties.showFooter.value === '{{false}}') {
|
||||
component.component.properties.footerHeight = {
|
||||
...component.component.properties.footerHeight,
|
||||
isHidden: true,
|
||||
};
|
||||
}
|
||||
|
||||
const accordionItems = baseComponentProperties(
|
||||
dataProperties,
|
||||
events,
|
||||
component,
|
||||
componentMeta,
|
||||
layoutPropertyChanged,
|
||||
paramUpdated,
|
||||
dataQueries,
|
||||
currentState,
|
||||
eventsChanged,
|
||||
apps,
|
||||
allComponents,
|
||||
validations,
|
||||
darkMode,
|
||||
[],
|
||||
additionalActions
|
||||
);
|
||||
|
||||
const [optionsItems] = conditionalAccordionItems(component);
|
||||
|
||||
// Insert the Trigger option as the third item
|
||||
accordionItems.splice(INDEX_OF_TRIGGER, 0, optionsItems);
|
||||
|
||||
return <Accordion items={accordionItems} />;
|
||||
};
|
||||
|
|
@ -19,6 +19,7 @@ export const Code = ({
|
|||
accordian,
|
||||
placeholder,
|
||||
validationFn,
|
||||
isHidden = false,
|
||||
}) => {
|
||||
const currentState = useCurrentState();
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ export const Code = ({
|
|||
onChange({ name: 'iconVisibility' }, 'value', value, 'styles');
|
||||
}
|
||||
|
||||
if (isHidden) return null;
|
||||
return (
|
||||
<div className={`field ${options.className}`} style={{ marginBottom: '8px' }}>
|
||||
<CodeEditor
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||
import { DefaultComponent } from './Components/DefaultComponent';
|
||||
import { FilePicker } from './Components/FilePicker';
|
||||
import { Modal } from './Components/Modal';
|
||||
import { ModalV2 } from './Components/ModalV2';
|
||||
import { CustomComponent } from './Components/CustomComponent';
|
||||
import { Icon } from './Components/Icon';
|
||||
import useFocus from '@/_hooks/use-focus';
|
||||
|
|
@ -78,6 +79,7 @@ const NEW_REVAMPED_COMPONENTS = [
|
|||
'Icon',
|
||||
'Image',
|
||||
'Container',
|
||||
'ModalV2',
|
||||
];
|
||||
|
||||
export const Inspector = ({ componentDefinitionChanged, darkMode, pages, selectedComponentId }) => {
|
||||
|
|
@ -703,6 +705,9 @@ const GetAccordion = React.memo(
|
|||
case 'FilePicker':
|
||||
return <FilePicker {...restProps} />;
|
||||
|
||||
case 'ModalV2':
|
||||
return <ModalV2 {...restProps} />;
|
||||
|
||||
case 'Modal':
|
||||
return <Modal {...restProps} />;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ export function renderCustomStyles(
|
|||
componentConfig.component == 'MultiselectV2' ||
|
||||
componentConfig.component == 'RadioButtonV2' ||
|
||||
componentConfig.component == 'Button' ||
|
||||
componentConfig.component == 'Image'
|
||||
componentConfig.component == 'Image' ||
|
||||
componentConfig.component == 'ModalV2'
|
||||
) {
|
||||
const paramTypeConfig = componentMeta[paramType] || {};
|
||||
const paramConfig = paramTypeConfig[param] || {};
|
||||
|
|
@ -131,6 +132,7 @@ export function renderElement(
|
|||
const paramTypeDefinition = componentDefinition[paramType] || {};
|
||||
const definition = paramTypeDefinition[param] || {};
|
||||
const meta = componentMeta[paramType][param];
|
||||
const isHidden = component.component.properties[param]?.isHidden ?? false;
|
||||
|
||||
if (
|
||||
componentConfig.component == 'DropDown' ||
|
||||
|
|
@ -170,6 +172,7 @@ export function renderElement(
|
|||
component={component}
|
||||
placeholder={placeholder}
|
||||
validationFn={validationFn}
|
||||
isHidden={isHidden}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import WidgetIcon from '@/../assets/images/icons/widgets';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker'];
|
||||
const LEGACY_WIDGETS = ['ToggleSwitch', 'DropDown', 'Multiselect', 'RadioButton', 'Datepicker', 'Modal'];
|
||||
const NEW_WIDGETS = [
|
||||
'ToggleSwitchV2',
|
||||
'DropdownV2',
|
||||
|
|
@ -12,6 +12,7 @@ const NEW_WIDGETS = [
|
|||
'DaterangePicker',
|
||||
'DatePickerV2',
|
||||
'TimePicker',
|
||||
'ModalV2',
|
||||
];
|
||||
|
||||
export const WidgetBox = ({ component, darkMode }) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,14 @@ export const RESTRICTED_WIDGETS_CONFIG = {
|
|||
Calendar: ['Calendar', 'Kanban'],
|
||||
Container: ['Calendar', 'Kanban'],
|
||||
Modal: ['Calendar', 'Kanban'],
|
||||
ModalV2: ['Calendar', 'Kanban'],
|
||||
ModalSlot: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
Tabs: ['Calendar', 'Kanban'],
|
||||
Kanban_popout: ['Calendar', 'Kanban'],
|
||||
Listview: ['Calendar', 'Kanban'],
|
||||
};
|
||||
|
||||
export const RESTRICTED_WIDGET_SLOTS_CONFIG = {
|
||||
header: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
footer: ['Calendar', 'Kanban', 'Table', 'Listview', 'Container'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
@ -64,6 +65,7 @@ export const widgets = [
|
|||
buttonConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { buttonConfig } from './button';
|
|||
import { tableConfig } from './table';
|
||||
import { chartConfig } from './chart';
|
||||
import { modalConfig } from './modal';
|
||||
import { modalV2Config } from './modalV2';
|
||||
import { formConfig } from './form';
|
||||
import { textinputConfig } from './textinput';
|
||||
import { numberinputConfig } from './numberinput';
|
||||
|
|
@ -62,7 +63,8 @@ export {
|
|||
buttonConfig,
|
||||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalConfig, //Deprecated
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
277
frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
export const modalV2Config = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'ModalV2',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 34,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Modal trigger visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
disabledTrigger: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal trigger',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
disabledModal: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal window',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
useDefaultButton: {
|
||||
type: 'toggle',
|
||||
displayName: 'Use default trigger button',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
triggerButtonLabel: {
|
||||
type: 'code',
|
||||
displayName: 'Trigger button label',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
defaultValue: 'Launch Modal',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Accordion
|
||||
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
|
||||
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
|
||||
|
||||
size: {
|
||||
type: 'select',
|
||||
displayName: 'Width',
|
||||
accordian: 'Data',
|
||||
options: [
|
||||
{ name: 'small', value: 'sm' },
|
||||
{ name: 'medium', value: 'lg' },
|
||||
{ name: 'large', value: 'xl' },
|
||||
{ name: 'fullscreen', value: 'fullscreen' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'lg',
|
||||
},
|
||||
},
|
||||
modalHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
},
|
||||
events: {
|
||||
onOpen: { displayName: 'On open' },
|
||||
onClose: { displayName: 'On close' },
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
slotName: 'header',
|
||||
layout: {
|
||||
top: 21,
|
||||
left: 1,
|
||||
height: 40,
|
||||
},
|
||||
displayName: 'ModalHeaderTitle',
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
text: 'Modal title',
|
||||
textSize: 20,
|
||||
textColor: '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 22,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterCancel',
|
||||
properties: ['text'],
|
||||
styles: ['type', 'borderColor', 'padding'],
|
||||
defaultValue: {
|
||||
text: 'Button1',
|
||||
type: 'outline',
|
||||
borderColor: '#CCD1D5',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 32,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterConfirm',
|
||||
properties: ['text'],
|
||||
defaultValue: {
|
||||
text: 'Button2',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
},
|
||||
bodyBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Body background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
triggerButtonBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
triggerButtonTextColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
show: false,
|
||||
isDisabledModal: false,
|
||||
isDisabledTrigger: false,
|
||||
isVisible: true,
|
||||
isLoading: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'open',
|
||||
displayName: 'Open',
|
||||
},
|
||||
{
|
||||
handle: 'close',
|
||||
displayName: 'Close',
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableTrigger',
|
||||
displayName: 'Set disable trigger',
|
||||
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableModal',
|
||||
displayName: 'Set disable modal',
|
||||
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setLoading',
|
||||
displayName: 'Set loading',
|
||||
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledTrigger: { value: '{{false}}' },
|
||||
disabledModal: { value: '{{false}}' },
|
||||
useDefaultButton: { value: `{{true}}` },
|
||||
triggerButtonLabel: { value: `Launch Modal` },
|
||||
size: { value: 'lg' },
|
||||
showHeader: { value: '{{true}}' },
|
||||
showFooter: { value: '{{true}}' },
|
||||
hideCloseButton: { value: '{{false}}' },
|
||||
hideOnEsc: { value: '{{true}}' },
|
||||
closeOnClickingOutside: { value: '{{false}}' },
|
||||
modalHeight: { value: 400 },
|
||||
headerHeight: { value: 80 },
|
||||
footerHeight: { value: 80 },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
headerBackgroundColor: { value: '#ffffffff' },
|
||||
footerBackgroundColor: { value: '#ffffffff' },
|
||||
bodyBackgroundColor: { value: '#ffffffff' },
|
||||
triggerButtonBackgroundColor: { value: '#4D72FA' },
|
||||
triggerButtonTextColor: { value: '#ffffffff' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -45,6 +45,7 @@ export const Container = ({
|
|||
border: `1px solid ${borderColor}`,
|
||||
height,
|
||||
display: isVisible ? 'flex' : 'none',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
boxShadow,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
|
||||
export const ModalFooter = React.memo(({ id, isDisabled, customStyles, darkMode, width, footerHeight, onClick }) => {
|
||||
const canvasFooterHeight = getCanvasHeight(footerHeight);
|
||||
return (
|
||||
<BootstrapModal.Footer style={{ ...customStyles.modalFooter }} data-cy={`modal-footer`} onClick={onClick}>
|
||||
<SubContainer
|
||||
id={`${id}-footer`}
|
||||
canvasHeight={canvasFooterHeight}
|
||||
canvasWidth={width}
|
||||
allowContainerSelect={false}
|
||||
darkMode={darkMode}
|
||||
styles={{
|
||||
margin: 0,
|
||||
backgroundColor: 'transparent',
|
||||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
}}
|
||||
/>
|
||||
{isDisabled && (
|
||||
<div
|
||||
id={`${id}-footer-disabled`}
|
||||
className="tj-modal-disabled-overlay"
|
||||
style={{ height: footerHeight || '100%' }}
|
||||
onClick={onClick}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
</BootstrapModal.Footer>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { getCanvasHeight } from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
|
||||
export const ModalHeader = React.memo(
|
||||
({ id, isDisabled, customStyles, hideCloseButton, darkMode, width, onHideModal, headerHeight, onClick }) => {
|
||||
const canvasHeaderHeight = getCanvasHeight(headerHeight);
|
||||
|
||||
return (
|
||||
<BootstrapModal.Header style={{ ...customStyles.modalHeader }} data-cy={`modal-header`} onClick={onClick}>
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
|
||||
<SubContainer
|
||||
id={`${id}-header`}
|
||||
canvasHeight={canvasHeaderHeight}
|
||||
canvasWidth={width}
|
||||
allowContainerSelect={false}
|
||||
darkMode={darkMode}
|
||||
styles={{
|
||||
backgroundColor: 'transparent',
|
||||
overflowX: 'hidden',
|
||||
overflowY: isDisabled ? 'hidden' : 'auto',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isDisabled && (
|
||||
<div
|
||||
id={`${id}-header-disabled`}
|
||||
className="tj-modal-disabled-overlay"
|
||||
style={{ height: headerHeight || '100%' }}
|
||||
onClick={onClick}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{!hideCloseButton && (
|
||||
<div className="tw-w-14 tw-h-14 tw-flex tw-items-center tw-justify-center tw-pr-4 tw-relative">
|
||||
<span
|
||||
className={`tj-modal-close-button ${isDisabled ? 'is-disabled' : ''}`}
|
||||
data-cy={`modal-close-button`}
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onHideModal();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icon-tabler-x"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</BootstrapModal.Header>
|
||||
);
|
||||
}
|
||||
);
|
||||
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal file
134
frontend/src/AppBuilder/Widgets/ModalV2/Components/Modal.jsx
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { default as BootstrapModal } from 'react-bootstrap/Modal';
|
||||
import { Container as SubContainer } from '@/AppBuilder/AppCanvas/Container';
|
||||
import { ConfigHandle } from '@/AppBuilder/AppCanvas/ConfigHandle/ConfigHandle';
|
||||
import { ModalHeader } from '@/AppBuilder/Widgets/ModalV2/Components/Header';
|
||||
import { ModalFooter } from '@/AppBuilder/Widgets/ModalV2/Components/Footer';
|
||||
|
||||
export const ModalWidget = ({ ...restProps }) => {
|
||||
const {
|
||||
customStyles,
|
||||
parentRef,
|
||||
id,
|
||||
showConfigHandler,
|
||||
isDisabled,
|
||||
isLoading,
|
||||
modalBodyHeight,
|
||||
onHideModal,
|
||||
hideCloseButton,
|
||||
darkMode,
|
||||
modalWidth,
|
||||
showHeader,
|
||||
hideOnEsc,
|
||||
showFooter,
|
||||
headerHeight,
|
||||
footerHeight,
|
||||
onSelectModal,
|
||||
} = restProps['modalProps'];
|
||||
|
||||
// When the modal body is clicked capture it and use the callback to set the selected component as modal
|
||||
const handleModalSlotClick = (event) => {
|
||||
const clickedComponentId = event.target.getAttribute('component-id');
|
||||
const clickedId = event.target.getAttribute('id');
|
||||
|
||||
// Check if the clicked element is part of the modal canvas & same widget with id
|
||||
if (clickedComponentId?.includes(id)) {
|
||||
onSelectModal(id);
|
||||
} else if (clickedId?.includes(id)) {
|
||||
onSelectModal(id);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
|
||||
const preventBackdropDrop = (e) => {
|
||||
if (e.target.className === 'fade modal show') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
document.addEventListener('drop', preventBackdropDrop);
|
||||
return () => {
|
||||
document.removeEventListener('drop', preventBackdropDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BootstrapModal
|
||||
{...restProps}
|
||||
contentClassName="modal-component tj-modal-widget-content"
|
||||
animation={true}
|
||||
onEscapeKeyDown={(e) => {
|
||||
e.preventDefault();
|
||||
if (hideOnEsc) {
|
||||
onHideModal();
|
||||
}
|
||||
}}
|
||||
onClick={handleModalSlotClick}
|
||||
>
|
||||
{showConfigHandler && (
|
||||
<ConfigHandle
|
||||
id={id}
|
||||
customClassName={showHeader ? '' : 'modalWidget-config-handle tw-h-0'}
|
||||
showHandle={showConfigHandler}
|
||||
setSelectedComponentAsModal={onSelectModal}
|
||||
componentType="Modal"
|
||||
isModalOpen={true}
|
||||
/>
|
||||
)}
|
||||
{showHeader && (
|
||||
<ModalHeader
|
||||
id={id}
|
||||
isDisabled={isDisabled}
|
||||
customStyles={customStyles}
|
||||
hideCloseButton={hideCloseButton}
|
||||
darkMode={darkMode}
|
||||
width={modalWidth}
|
||||
onHideModal={onHideModal}
|
||||
headerHeight={headerHeight}
|
||||
onClick={handleModalSlotClick}
|
||||
/>
|
||||
)}
|
||||
<BootstrapModal.Body style={{ ...customStyles.modalBody }} ref={parentRef} id={id} data-cy={`modal-body`}>
|
||||
{isDisabled && (
|
||||
<div
|
||||
id={`${id}-body-disabled`}
|
||||
className="tj-modal-disabled-overlay"
|
||||
style={{
|
||||
height: modalBodyHeight || '100%',
|
||||
}}
|
||||
onDrop={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
{!isLoading ? (
|
||||
<>
|
||||
<SubContainer
|
||||
id={`${id}`}
|
||||
canvasHeight={modalBodyHeight}
|
||||
styles={{ backgroundColor: customStyles.modalBody.backgroundColor, height: 'inherit' }}
|
||||
canvasWidth={modalWidth}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
<center>
|
||||
<div className="spinner-border mt-5" role="status"></div>
|
||||
</center>
|
||||
</div>
|
||||
)}
|
||||
</BootstrapModal.Body>
|
||||
{showFooter && (
|
||||
<ModalFooter
|
||||
id={id}
|
||||
isDisabled={isDisabled}
|
||||
darkMode={darkMode}
|
||||
customStyles={customStyles}
|
||||
width={modalWidth}
|
||||
footerHeight={footerHeight}
|
||||
onClick={handleModalSlotClick}
|
||||
/>
|
||||
)}
|
||||
</BootstrapModal>
|
||||
);
|
||||
};
|
||||
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal file
243
frontend/src/AppBuilder/Widgets/ModalV2/ModalV2.jsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import useStore from '@/AppBuilder/_stores/store';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useExposeState } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalCSA';
|
||||
import { useResetZIndex } from '@/AppBuilder/Widgets/ModalV2/hooks/useModalZIndex';
|
||||
import { useModalEventSideEffects } from '@/AppBuilder/Widgets/ModalV2/hooks/useResizeSideEffects';
|
||||
import { useEventListener } from '@/_hooks/use-event-listener';
|
||||
import { ModalWidget } from '@/AppBuilder/Widgets/ModalV2/Components/Modal';
|
||||
|
||||
import {
|
||||
getModalBodyHeight,
|
||||
getModalHeaderHeight,
|
||||
getModalFooterHeight,
|
||||
} from '@/AppBuilder/Widgets/ModalV2/helpers/utils';
|
||||
import { createModalStyles } from '@/AppBuilder/Widgets/ModalV2/helpers/stylesFactory';
|
||||
import { onShowSideEffects, onHideSideEffects } from '@/AppBuilder/Widgets/ModalV2/helpers/sideEffects';
|
||||
|
||||
import '@/AppBuilder/Widgets/ModalV2/style.scss';
|
||||
|
||||
export const ModalV2 = function Modal({
|
||||
id,
|
||||
component,
|
||||
darkMode,
|
||||
properties,
|
||||
styles,
|
||||
setExposedVariable,
|
||||
setExposedVariables,
|
||||
fireEvent,
|
||||
dataCy,
|
||||
height,
|
||||
}) {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const {
|
||||
closeOnClickingOutside = false,
|
||||
hideOnEsc,
|
||||
hideCloseButton,
|
||||
hideTitleBar,
|
||||
useDefaultButton,
|
||||
triggerButtonLabel,
|
||||
modalHeight,
|
||||
showHeader,
|
||||
showFooter,
|
||||
headerHeight,
|
||||
footerHeight,
|
||||
} = properties;
|
||||
const {
|
||||
headerBackgroundColor,
|
||||
footerBackgroundColor,
|
||||
bodyBackgroundColor,
|
||||
triggerButtonBackgroundColor,
|
||||
triggerButtonTextColor,
|
||||
boxShadow,
|
||||
} = styles;
|
||||
const isInitialRender = useRef(true);
|
||||
const title = properties.title ?? '';
|
||||
const titleAlignment = properties.titleAlignment ?? 'left';
|
||||
const size = properties.size ?? 'lg';
|
||||
const setSelectedComponentAsModal = useStore((state) => state.setSelectedComponentAsModal, shallow);
|
||||
const mode = useStore((state) => state.currentMode, shallow);
|
||||
|
||||
const computedModalBodyHeight = getModalBodyHeight(modalHeight, showHeader, showFooter, headerHeight, footerHeight);
|
||||
const headerHeightPx = getModalHeaderHeight(showHeader, headerHeight);
|
||||
const footerHeightPx = getModalFooterHeight(showFooter, footerHeight);
|
||||
const isFullScreen = properties.size === 'fullscreen';
|
||||
const computedCanvasHeight = isFullScreen
|
||||
? `calc(100vh - 48px - 40px - ${headerHeightPx} - ${footerHeightPx})`
|
||||
: computedModalBodyHeight;
|
||||
|
||||
useEffect(() => {
|
||||
const exposedVariables = {
|
||||
open: async function () {
|
||||
setExposedVariable('show', true);
|
||||
setShowModal(true);
|
||||
},
|
||||
close: async function () {
|
||||
setExposedVariable('show', false);
|
||||
setShowModal(false);
|
||||
},
|
||||
};
|
||||
setExposedVariables(exposedVariables);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
function hideModal() {
|
||||
setExposedVariable('show', false);
|
||||
setShowModal(false);
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
setExposedVariable('show', true);
|
||||
setShowModal(true);
|
||||
}
|
||||
|
||||
useEventListener('resize', onShowSideEffects, window);
|
||||
|
||||
const onShowModal = () => {
|
||||
openModal();
|
||||
onShowSideEffects();
|
||||
fireEvent('onOpen');
|
||||
setSelectedComponentAsModal(id);
|
||||
};
|
||||
|
||||
const onHideModal = () => {
|
||||
onHideSideEffects(() => fireEvent('onOpen'));
|
||||
hideModal();
|
||||
setSelectedComponentAsModal(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialRender.current) {
|
||||
isInitialRender.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const inputRef = document?.getElementsByClassName('tj-text-input-widget')?.[0];
|
||||
inputRef?.blur();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showModal]);
|
||||
|
||||
useEffect(() => {
|
||||
// When modal is active, prevent drop event on backdrop (else widgets droppped will get added to canvas)
|
||||
const preventBackdropDrop = (e) => {
|
||||
if (e.target.className === 'fade modal show') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
document.addEventListener('drop', preventBackdropDrop);
|
||||
return () => {
|
||||
document.removeEventListener('drop', preventBackdropDrop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { controlBoxRef } = useResetZIndex({ showModal, id, mode });
|
||||
const { isDisabledTrigger, isDisabledModal, isVisible, isLoading } = useExposeState({
|
||||
loadingState: properties.loadingState,
|
||||
visibleState: properties.visibility,
|
||||
disabledModalState: properties.disabledModal,
|
||||
disabledTriggerState: properties.disabledTrigger,
|
||||
setExposedVariables,
|
||||
setExposedVariable,
|
||||
onHideModal,
|
||||
onShowModal,
|
||||
});
|
||||
|
||||
const customStyles = createModalStyles({
|
||||
height,
|
||||
modalHeight,
|
||||
computedCanvasHeight,
|
||||
bodyBackgroundColor,
|
||||
darkMode,
|
||||
isDisabledModal,
|
||||
headerBackgroundColor,
|
||||
headerHeightPx,
|
||||
footerBackgroundColor,
|
||||
footerHeightPx,
|
||||
triggerButtonBackgroundColor,
|
||||
triggerButtonTextColor,
|
||||
isVisible,
|
||||
boxShadow,
|
||||
});
|
||||
|
||||
const { modalWidth, parentRef } = useModalEventSideEffects({
|
||||
showModal,
|
||||
size,
|
||||
id,
|
||||
onShowSideEffects,
|
||||
closeOnClickingOutside,
|
||||
onHideModal,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="container d-flex align-items-center"
|
||||
data-disabled={isDisabledTrigger}
|
||||
data-cy={dataCy}
|
||||
style={{ height }}
|
||||
>
|
||||
{useDefaultButton && isVisible && (
|
||||
<button
|
||||
disabled={isDisabledTrigger}
|
||||
className="jet-button btn btn-primary p-1 overflow-hidden"
|
||||
style={customStyles.buttonStyles}
|
||||
onClick={(event) => {
|
||||
/**** Start - Logic to reduce the zIndex of modal control box ****/
|
||||
controlBoxRef.current = document.querySelector(`.selected-component.sc-${id}`)?.parentElement;
|
||||
if (mode === 'edit' && controlBoxRef.current) {
|
||||
controlBoxRef.current.classList.add('modal-moveable');
|
||||
}
|
||||
/**** End - Logic to reduce the zIndex of modal control box ****/
|
||||
|
||||
event.stopPropagation();
|
||||
setShowModal(true);
|
||||
}}
|
||||
data-cy={`${dataCy}-launch-button`}
|
||||
>
|
||||
{triggerButtonLabel ?? 'Show Modal'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<ModalWidget
|
||||
show={showModal}
|
||||
contentClassName="modal-component"
|
||||
container={document.getElementsByClassName('real-canvas')[0]}
|
||||
size={size}
|
||||
keyboard={true}
|
||||
enforceFocus={false}
|
||||
animation={false}
|
||||
onShow={() => onShowModal()}
|
||||
onHide={() => onHideModal()}
|
||||
onEscapeKeyDown={() => hideOnEsc && onHideModal()}
|
||||
id="modal-container"
|
||||
component-id={id}
|
||||
backdrop={'static'}
|
||||
scrollable={true}
|
||||
modalProps={{
|
||||
customStyles,
|
||||
parentRef,
|
||||
id,
|
||||
title,
|
||||
titleAlignment,
|
||||
hideTitleBar,
|
||||
hideCloseButton,
|
||||
onHideModal,
|
||||
component,
|
||||
hideOnEsc,
|
||||
modalHeight,
|
||||
isLoading,
|
||||
isDisabled: isDisabledModal,
|
||||
showConfigHandler: mode === 'edit',
|
||||
fullscreen: isFullScreen,
|
||||
showHeader,
|
||||
showFooter,
|
||||
headerHeight: headerHeightPx,
|
||||
footerHeight: footerHeightPx,
|
||||
modalBodyHeight: computedCanvasHeight,
|
||||
modalWidth,
|
||||
onSelectModal: setSelectedComponentAsModal,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
// Side effects for modal, which include dom manipulation to hide overflow when opening
|
||||
// And cleaning up dom when modal is closed
|
||||
|
||||
export const onShowSideEffects = () => {
|
||||
const canvasElement = document.querySelector('.page-container.canvas-container');
|
||||
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
|
||||
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
|
||||
const modalContainer = allModalContainers[allModalContainers.length - 1];
|
||||
|
||||
if (canvasElement && realCanvasEl && modalContainer) {
|
||||
const currentScroll = canvasElement.scrollTop;
|
||||
canvasElement.style.overflowY = 'hidden';
|
||||
|
||||
modalContainer.style.height = `${canvasElement.offsetHeight}px`;
|
||||
modalContainer.style.top = `${currentScroll}px`;
|
||||
}
|
||||
};
|
||||
|
||||
export const onHideSideEffects = (callback = () => {}) => {
|
||||
const canvasElement = document.querySelector('.page-container.canvas-container');
|
||||
const realCanvasEl = document.getElementsByClassName('real-canvas')[0];
|
||||
const allModalContainers = realCanvasEl.querySelectorAll('.modal');
|
||||
const modalContainer = allModalContainers[allModalContainers.length - 1];
|
||||
const hasManyModalsOpen = allModalContainers.length > 1;
|
||||
|
||||
if (canvasElement && realCanvasEl && modalContainer) {
|
||||
modalContainer.style.height = ``;
|
||||
modalContainer.style.top = ``;
|
||||
callback();
|
||||
// fireEvent('onClose');
|
||||
}
|
||||
if (canvasElement && !hasManyModalsOpen) {
|
||||
canvasElement.style.overflow = 'auto';
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
var tinycolor = require('tinycolor2');
|
||||
|
||||
export function createModalStyles({
|
||||
height,
|
||||
modalHeight,
|
||||
computedCanvasHeight,
|
||||
bodyBackgroundColor,
|
||||
darkMode,
|
||||
isDisabledModal,
|
||||
headerBackgroundColor,
|
||||
headerHeightPx,
|
||||
footerBackgroundColor,
|
||||
footerHeightPx,
|
||||
triggerButtonBackgroundColor,
|
||||
triggerButtonTextColor,
|
||||
isVisible,
|
||||
boxShadow,
|
||||
}) {
|
||||
const backwardCompatibilityCheck = height == '34' || modalHeight != undefined ? true : false;
|
||||
|
||||
return {
|
||||
modalBody: {
|
||||
height: backwardCompatibilityCheck ? computedCanvasHeight : height,
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(bodyBackgroundColor) && darkMode ? '#1F2837' : bodyBackgroundColor,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(headerBackgroundColor) && darkMode ? '#1F2837' : headerBackgroundColor,
|
||||
height: headerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
},
|
||||
modalFooter: {
|
||||
backgroundColor:
|
||||
['#fff', '#ffffffff'].includes(footerBackgroundColor) && darkMode ? '#1F2837' : footerBackgroundColor,
|
||||
height: footerHeightPx,
|
||||
overflowY: isDisabledModal ? 'hidden' : 'auto',
|
||||
},
|
||||
buttonStyles: {
|
||||
backgroundColor: triggerButtonBackgroundColor,
|
||||
color: triggerButtonTextColor,
|
||||
width: '100%',
|
||||
display: isVisible ? '' : 'none',
|
||||
'--tblr-btn-color-darker': tinycolor(triggerButtonBackgroundColor).darken(8).toString(),
|
||||
boxShadow,
|
||||
},
|
||||
};
|
||||
}
|
||||
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal file
44
frontend/src/AppBuilder/Widgets/ModalV2/helpers/utils.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const MODAL_HEADER = {
|
||||
HEIGHT: 80,
|
||||
};
|
||||
const MODAL_FOOTER = {
|
||||
HEIGHT: 80,
|
||||
};
|
||||
|
||||
export const getCanvasHeight = (height) => {
|
||||
const parsedHeight = height.includes('px') ? parseInt(height, 10) : height;
|
||||
|
||||
return Math.ceil(parsedHeight);
|
||||
};
|
||||
|
||||
export const getModalBodyHeight = (
|
||||
height,
|
||||
showHeader,
|
||||
showFooter,
|
||||
headerHeight = MODAL_HEADER.HEIGHT,
|
||||
footerHeight = MODAL_FOOTER.HEIGHT
|
||||
) => {
|
||||
let modalHeight = height ? parseInt(height, 10) : 0;
|
||||
let parsedHeaderHeight = showHeader ? parseInt(headerHeight, 10) : 0;
|
||||
let parsedFooterHeight = showFooter ? parseInt(footerHeight, 10) : 0;
|
||||
|
||||
if (showHeader) {
|
||||
modalHeight = modalHeight - parsedHeaderHeight;
|
||||
}
|
||||
if (showFooter) {
|
||||
modalHeight = modalHeight - parsedFooterHeight;
|
||||
}
|
||||
return `${Math.max(modalHeight, 40)}px`;
|
||||
};
|
||||
|
||||
export const getModalHeaderHeight = (showHeader, headerHeight = MODAL_FOOTER.HEIGHT) => {
|
||||
let parsedHeight = showHeader ? parseInt(headerHeight, 10) : 0;
|
||||
|
||||
return `${parsedHeight}px`;
|
||||
};
|
||||
|
||||
export const getModalFooterHeight = (showFooter, footerHeight = MODAL_FOOTER.HEIGHT) => {
|
||||
let parsedHeight = showFooter ? parseInt(footerHeight, 10) : 0;
|
||||
|
||||
return `${parsedHeight}px`;
|
||||
};
|
||||
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal file
84
frontend/src/AppBuilder/Widgets/ModalV2/hooks/useModalCSA.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
export const useExposeState = ({
|
||||
loadingState,
|
||||
visibleState,
|
||||
disabledModalState,
|
||||
disabledTriggerState,
|
||||
setExposedVariables,
|
||||
setExposedVariable,
|
||||
onHideModal,
|
||||
onShowModal,
|
||||
}) => {
|
||||
const [isVisible, setVisibility] = useState(visibleState ?? true);
|
||||
const [isLoading, setLoading] = useState(loadingState ?? false);
|
||||
const [isDisabledModal, setDisabledModal] = useState(disabledModalState ?? false);
|
||||
const [isDisabledTrigger, setDisabledTrigger] = useState(disabledTriggerState ?? false);
|
||||
|
||||
// Track previous values to prevent redundant updates
|
||||
const prevValues = useRef({});
|
||||
|
||||
// Effect to sync state with props (only when props change)
|
||||
useEffect(() => {
|
||||
setDisabledModal(disabledModalState);
|
||||
}, [disabledModalState]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisabledTrigger(disabledTriggerState);
|
||||
}, [disabledTriggerState]);
|
||||
|
||||
useEffect(() => {
|
||||
setVisibility(visibleState);
|
||||
}, [visibleState]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(loadingState);
|
||||
}, [loadingState]);
|
||||
|
||||
// Expose state-modifying functions only once
|
||||
useEffect(() => {
|
||||
setExposedVariables({
|
||||
setDisableTrigger: async (value) => setDisabledTrigger(value),
|
||||
setDisableModal: async (value) => setDisabledModal(value),
|
||||
setVisibility: async (value) => setVisibility(value),
|
||||
setLoading: async (value) => setLoading(value),
|
||||
open: async () => onShowModal(),
|
||||
close: async () => onHideModal(),
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Prevent redundant updates to `setExposedVariable`
|
||||
const updateExposedVariable = (key, value) => {
|
||||
if (prevValues.current[key] !== value) {
|
||||
prevValues.current[key] = value;
|
||||
setExposedVariable(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateExposedVariable('isDisabledModal', isDisabledModal);
|
||||
}, [isDisabledModal]);
|
||||
|
||||
useEffect(() => {
|
||||
updateExposedVariable('isDisabledTrigger', isDisabledTrigger);
|
||||
}, [isDisabledTrigger]);
|
||||
|
||||
useEffect(() => {
|
||||
updateExposedVariable('isVisible', isVisible);
|
||||
}, [isVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
updateExposedVariable('isLoading', isLoading);
|
||||
}, [isLoading]);
|
||||
|
||||
return {
|
||||
isDisabledTrigger,
|
||||
setDisabledTrigger,
|
||||
isDisabledModal,
|
||||
setDisabledModal,
|
||||
isVisible,
|
||||
setVisibility,
|
||||
isLoading,
|
||||
setLoading,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
import { useGridStore } from '@/_stores/gridStore';
|
||||
|
||||
/**** Start - Logic to reset the zIndex of modal control box ****/
|
||||
export const useResetZIndex = ({ showModal, id, mode }) => {
|
||||
const controlBoxRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showModal && mode === 'edit') {
|
||||
controlBoxRef.current?.classList?.remove('modal-moveable');
|
||||
controlBoxRef.current = null;
|
||||
}
|
||||
if (showModal) {
|
||||
useGridStore.getState().actions.setOpenModalWidgetId(id);
|
||||
} else {
|
||||
if (useGridStore.getState().openModalWidgetId === id) {
|
||||
useGridStore.getState().actions.setOpenModalWidgetId(null);
|
||||
}
|
||||
}
|
||||
}, [showModal, id, mode]);
|
||||
/**** End - Logic to reset the zIndex of modal control box ****/
|
||||
|
||||
return {
|
||||
controlBoxRef,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export function useModalEventSideEffects({
|
||||
showModal,
|
||||
size,
|
||||
id,
|
||||
onShowSideEffects,
|
||||
closeOnClickingOutside,
|
||||
onHideModal,
|
||||
}) {
|
||||
const [modalWidth, setModalWidth] = useState();
|
||||
const parentRef = useRef(null);
|
||||
|
||||
// When query panel opens or closes, the modal container height should change to
|
||||
// accomodate the new height of the canvas
|
||||
|
||||
useEffect(() => {
|
||||
// Select the DOM element
|
||||
const canvasElement = document.querySelector('.page-container.canvas-container');
|
||||
|
||||
if (!canvasElement) return; // Ensure the element exists
|
||||
|
||||
// Create a ResizeObserver
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
// Update the height state when the element's height changes
|
||||
onShowSideEffects();
|
||||
|
||||
// When modal is in fullscreen and width of browser changes, update the modal width
|
||||
if (size === 'fullscreen') {
|
||||
const width = entry.target.offsetWidth;
|
||||
setModalWidth(width);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Observe the canvas element
|
||||
resizeObserver.observe(canvasElement);
|
||||
|
||||
return () => {
|
||||
// Cleanup observer on component unmount
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [size, onShowSideEffects]);
|
||||
|
||||
useEffect(() => {
|
||||
if (showModal && parentRef.current) {
|
||||
if (size === 'fullscreen') {
|
||||
// First time modal is opened, the whole modal is part of the full body, later put into the canvas
|
||||
// This delay is to get the correct width of the modal
|
||||
|
||||
const canvasElement = document.querySelector('.page-container.canvas-container');
|
||||
const width = canvasElement.offsetWidth;
|
||||
setModalWidth(width);
|
||||
} else {
|
||||
setModalWidth(parentRef.current.offsetWidth);
|
||||
}
|
||||
}
|
||||
}, [showModal, size, id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (closeOnClickingOutside) {
|
||||
const handleClickOutside = (event) => {
|
||||
const modalRef = parentRef?.current?.parentElement?.parentElement?.parentElement;
|
||||
|
||||
if (modalRef && modalRef === event.target) {
|
||||
onHideModal();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [closeOnClickingOutside, parentRef]);
|
||||
|
||||
return { modalWidth, parentRef };
|
||||
}
|
||||
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal file
66
frontend/src/AppBuilder/Widgets/ModalV2/style.scss
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
.modal-open .modal.show {
|
||||
/* To fix the padding issue */
|
||||
scrollbar-gutter: auto;
|
||||
// padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.modal-content.modal-component.tj-modal-widget-content {
|
||||
border: 0;
|
||||
|
||||
.tj-modal-close-button {
|
||||
padding: 8px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
bottom: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tj-modal-disabled-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
z-index: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border-top: 1px solid var(--border-weak);
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,7 @@ import { Tabs } from '@/AppBuilder/Widgets/Tabs';
|
|||
import { Kanban } from '@/AppBuilder/Widgets/Kanban/Kanban';
|
||||
import { Form } from '@/AppBuilder/Widgets/Form/Form';
|
||||
import { Modal } from '@/AppBuilder/Widgets/Modal';
|
||||
import { ModalV2 } from '@/AppBuilder/Widgets/ModalV2/ModalV2';
|
||||
import { Calendar } from '@/AppBuilder/Widgets/Calendar/Calendar';
|
||||
// import './requestIdleCallbackPolyfill';
|
||||
|
||||
|
|
@ -106,6 +107,7 @@ export const AllComponents = {
|
|||
Multiselect,
|
||||
MultiselectV2,
|
||||
Modal,
|
||||
ModalV2,
|
||||
Chart,
|
||||
Map: MapComponent,
|
||||
QrScanner,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import {
|
|||
import { extractAndReplaceReferencesFromString } from '@/AppBuilder/_stores/ast';
|
||||
import { deepClone } from '@/_helpers/utilities/utils.helpers';
|
||||
import { cloneDeep, merge, set as lodashSet } from 'lodash';
|
||||
import { computeComponentName, getAllChildComponents } from '@/AppBuilder/AppCanvas/appCanvasUtils';
|
||||
import {
|
||||
computeComponentName,
|
||||
getAllChildComponents,
|
||||
getParentWidgetFromId,
|
||||
} from '@/AppBuilder/AppCanvas/appCanvasUtils';
|
||||
import { pageConfig } from '@/AppBuilder/RightSideBar/PageSettingsTab/pageConfig';
|
||||
import { RIGHT_SIDE_BAR_TAB } from '@/AppBuilder/RightSideBar/rightSidebarConstants';
|
||||
import { DEFAULT_COMPONENT_STRUCTURE } from './resolvedSlice';
|
||||
|
|
@ -761,7 +765,7 @@ export const createComponentsSlice = (set, get) => ({
|
|||
const { getComponentTypeFromId } = get();
|
||||
const transformedParentId = parentId?.length > 36 ? parentId.slice(0, 36) : parentId;
|
||||
let parentType = getComponentTypeFromId(transformedParentId, moduleId);
|
||||
const parentWidget = parentType === 'Kanban' ? 'Kanban_card' : parentType;
|
||||
const parentWidget = getParentWidgetFromId(parentType, parentId);
|
||||
const restrictedWidgets = RESTRICTED_WIDGETS_CONFIG?.[parentWidget] || [];
|
||||
const isParentChangeAllowed = !restrictedWidgets.includes(currentWidget);
|
||||
if (!isParentChangeAllowed)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ function deepEqualityCheckusingLoDash(obj1, obj2) {
|
|||
export const shouldUpdate = (prevProps, nextProps) => {
|
||||
const listToRender = getComponentsToRenders();
|
||||
// evaluate change in exposedVariables only for Modal component, because open/close in modal relies on exposedVariables
|
||||
const compareExposedVariables = nextProps.componentName === 'Modal';
|
||||
const compareExposedVariables = nextProps.componentName === 'Modal' || nextProps.componentName === 'ModalV2';
|
||||
|
||||
let needToRender = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { buttonConfig } from './button';
|
|||
import { tableConfig } from './table';
|
||||
import { chartConfig } from './chart';
|
||||
import { modalConfig } from './modal';
|
||||
import { modalV2Config } from './modalV2';
|
||||
import { formConfig } from './form';
|
||||
import { textinputConfig } from './textinput';
|
||||
import { numberinputConfig } from './numberinput';
|
||||
|
|
@ -59,7 +60,8 @@ export {
|
|||
buttonConfig,
|
||||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalConfig, //!Depreciated
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal file
277
frontend/src/Editor/WidgetManager/configs/modalV2.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
export const modalV2Config = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'ModalV2',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 34,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Modal trigger visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
disabledTrigger: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal trigger',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
disabledModal: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal window',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
useDefaultButton: {
|
||||
type: 'toggle',
|
||||
displayName: 'Use default trigger button',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
triggerButtonLabel: {
|
||||
type: 'code',
|
||||
displayName: 'Trigger button label',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
defaultValue: 'Launch Modal',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Accordion
|
||||
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
|
||||
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
|
||||
|
||||
size: {
|
||||
type: 'select',
|
||||
displayName: 'Width',
|
||||
accordian: 'Data',
|
||||
options: [
|
||||
{ name: 'small', value: 'sm' },
|
||||
{ name: 'medium', value: 'lg' },
|
||||
{ name: 'large', value: 'xl' },
|
||||
{ name: 'fullscreen', value: 'fullscreen' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'lg',
|
||||
},
|
||||
},
|
||||
modalHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
},
|
||||
events: {
|
||||
onOpen: { displayName: 'On open' },
|
||||
onClose: { displayName: 'On close' },
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
slotName: 'header',
|
||||
layout: {
|
||||
top: 21,
|
||||
left: 1,
|
||||
height: 40,
|
||||
},
|
||||
displayName: 'ModalHeaderTitle',
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
text: 'Modal title',
|
||||
textSize: 20,
|
||||
textColor: '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 22,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterCancel',
|
||||
properties: ['text'],
|
||||
styles: ['type', 'borderColor', 'padding'],
|
||||
defaultValue: {
|
||||
text: 'Button1',
|
||||
type: 'outline',
|
||||
borderColor: '#CCD1D5',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 32,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterConfirm',
|
||||
properties: ['text'],
|
||||
defaultValue: {
|
||||
text: 'Button2',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
},
|
||||
bodyBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Body background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
triggerButtonBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
triggerButtonTextColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
show: false,
|
||||
isDisabledModal: false,
|
||||
isDisabledTrigger: false,
|
||||
isVisible: true,
|
||||
isLoading: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'open',
|
||||
displayName: 'Open',
|
||||
},
|
||||
{
|
||||
handle: 'close',
|
||||
displayName: 'Close',
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableTrigger',
|
||||
displayName: 'Set disable trigger',
|
||||
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableModal',
|
||||
displayName: 'Set disable modal',
|
||||
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setLoading',
|
||||
displayName: 'Set loading',
|
||||
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledTrigger: { value: '{{false}}' },
|
||||
disabledModal: { value: '{{false}}' },
|
||||
useDefaultButton: { value: `{{true}}` },
|
||||
triggerButtonLabel: { value: `Launch Modal` },
|
||||
size: { value: 'lg' },
|
||||
showHeader: { value: '{{true}}' },
|
||||
showFooter: { value: '{{true}}' },
|
||||
hideCloseButton: { value: '{{false}}' },
|
||||
hideOnEsc: { value: '{{true}}' },
|
||||
closeOnClickingOutside: { value: '{{false}}' },
|
||||
modalHeight: { value: 400 },
|
||||
headerHeight: { value: 80 },
|
||||
footerHeight: { value: 80 },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
headerBackgroundColor: { value: '#ffffffff' },
|
||||
footerBackgroundColor: { value: '#ffffffff' },
|
||||
bodyBackgroundColor: { value: '#ffffffff' },
|
||||
triggerButtonBackgroundColor: { value: '#4D72FA' },
|
||||
triggerButtonTextColor: { value: '#ffffffff' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -1 +1,7 @@
|
|||
export const LEGACY_ITEMS = ['ToggleSwitchLegacy', 'DropdownLegacy', 'MultiselectLegacy', 'RadioButtonLegacy'];
|
||||
export const LEGACY_ITEMS = [
|
||||
'ToggleSwitchLegacy',
|
||||
'DropdownLegacy',
|
||||
'MultiselectLegacy',
|
||||
'RadioButtonLegacy',
|
||||
'ModalLegacy',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
@ -62,6 +63,7 @@ export const widgets = [
|
|||
buttonConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -5277,7 +5277,8 @@ fieldset:disabled .btn {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
outline: 0
|
||||
outline: 0;
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
|
|
@ -5311,7 +5312,7 @@ fieldset:disabled .btn {
|
|||
}
|
||||
|
||||
.modal-dialog-scrollable .modal-content {
|
||||
max-height: 100%;
|
||||
max-height: 88%;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
|
|
@ -5447,6 +5448,10 @@ fieldset:disabled .btn {
|
|||
margin: 0
|
||||
}
|
||||
|
||||
.real-canvas .modal-dialog.modal-fullscreen {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-fullscreen .modal-content {
|
||||
height: 100%;
|
||||
border: 0;
|
||||
|
|
@ -5465,6 +5470,12 @@ fieldset:disabled .btn {
|
|||
border-radius: 0
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
|
||||
// Modal header height
|
||||
padding-bottom: 56px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width:575.98px) {
|
||||
.modal-fullscreen-sm-down {
|
||||
width: 100vw;
|
||||
|
|
@ -19129,4 +19140,4 @@ img {
|
|||
background: #1f2936;
|
||||
border-color: #dadcde
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2665,10 +2665,18 @@ hr {
|
|||
overflow-y: initial !important
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable .modal-content {
|
||||
.modal-dialog-scrollable:not(.modal-fullscreen) .modal-content {
|
||||
max-height: 88% !important;
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content {
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
||||
.modal-dialog-scrollable.modal-fullscreen .modal-content.modal-component {
|
||||
// Modal header height
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -14022,8 +14030,8 @@ tbody {
|
|||
}
|
||||
|
||||
/*
|
||||
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
|
||||
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
|
||||
* remove this once whole app is migrated to new styles. use only `theme-dark` class everywhere.
|
||||
* This is added since some of the pages are in old theme and making changes to `theme-dark` styles can break UI style somewhere else
|
||||
*/
|
||||
.tj-dark-mode {
|
||||
background-color: var(--base) !important;
|
||||
|
|
@ -18616,4 +18624,4 @@ section.ai-message-prompt-input-wrapper {
|
|||
background: #FFFAEB !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { buttonConfig } from './button';
|
|||
import { tableConfig } from './table';
|
||||
import { chartConfig } from './chart';
|
||||
import { modalConfig } from './modal';
|
||||
import { modalV2Config } from './modalV2';
|
||||
import { formConfig } from './form';
|
||||
import { textinputConfig } from './textinput';
|
||||
import { numberinputConfig } from './numberinput';
|
||||
|
|
@ -62,7 +63,8 @@ const widgets = {
|
|||
buttonConfig,
|
||||
tableConfig,
|
||||
chartConfig,
|
||||
modalConfig,
|
||||
modalConfig, //!Depreciated
|
||||
modalV2Config,
|
||||
formConfig,
|
||||
textinputConfig,
|
||||
numberinputConfig,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export const modalConfig = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
name: 'ModalLegacy',
|
||||
displayName: 'Modal (Legacy)',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'Modal',
|
||||
defaultSize: {
|
||||
|
|
|
|||
277
server/src/modules/apps/services/widget-config/modalV2.js
Normal file
277
server/src/modules/apps/services/widget-config/modalV2.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
export const modalV2Config = {
|
||||
name: 'Modal',
|
||||
displayName: 'Modal',
|
||||
description: 'Show pop-up windows',
|
||||
component: 'ModalV2',
|
||||
defaultSize: {
|
||||
width: 10,
|
||||
height: 34,
|
||||
},
|
||||
others: {
|
||||
showOnDesktop: { type: 'toggle', displayName: 'Show on desktop' },
|
||||
showOnMobile: { type: 'toggle', displayName: 'Show on mobile' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: {
|
||||
type: 'toggle',
|
||||
displayName: 'Loading state',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
visibility: {
|
||||
type: 'toggle',
|
||||
displayName: 'Modal trigger visibility',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
disabledTrigger: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal trigger',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
disabledModal: {
|
||||
type: 'toggle',
|
||||
displayName: 'Disable modal window',
|
||||
validation: {
|
||||
schema: { type: 'boolean' },
|
||||
defaultValue: false,
|
||||
},
|
||||
section: 'additionalActions',
|
||||
},
|
||||
useDefaultButton: {
|
||||
type: 'toggle',
|
||||
displayName: 'Use default trigger button',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'boolean',
|
||||
},
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
triggerButtonLabel: {
|
||||
type: 'code',
|
||||
displayName: 'Trigger button label',
|
||||
validation: {
|
||||
schema: {
|
||||
type: 'string',
|
||||
},
|
||||
defaultValue: 'Launch Modal',
|
||||
},
|
||||
},
|
||||
|
||||
// Data Accordion
|
||||
showHeader: { type: 'toggle', displayName: 'Header', accordian: 'Data' },
|
||||
showFooter: { type: 'toggle', displayName: 'Footer', accordian: 'Data' },
|
||||
|
||||
size: {
|
||||
type: 'select',
|
||||
displayName: 'Width',
|
||||
accordian: 'Data',
|
||||
options: [
|
||||
{ name: 'small', value: 'sm' },
|
||||
{ name: 'medium', value: 'lg' },
|
||||
{ name: 'large', value: 'xl' },
|
||||
{ name: 'fullscreen', value: 'fullscreen' },
|
||||
],
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: 'lg',
|
||||
},
|
||||
},
|
||||
modalHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 },
|
||||
},
|
||||
headerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Header height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
footerHeight: {
|
||||
type: 'numberInput',
|
||||
displayName: 'Footer height',
|
||||
accordian: 'Data',
|
||||
validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 },
|
||||
},
|
||||
hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' },
|
||||
closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' },
|
||||
hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' },
|
||||
},
|
||||
events: {
|
||||
onOpen: { displayName: 'On open' },
|
||||
onClose: { displayName: 'On close' },
|
||||
},
|
||||
defaultChildren: [
|
||||
{
|
||||
componentName: 'Text',
|
||||
slotName: 'header',
|
||||
layout: {
|
||||
top: 21,
|
||||
left: 1,
|
||||
height: 40,
|
||||
},
|
||||
displayName: 'ModalHeaderTitle',
|
||||
properties: ['text'],
|
||||
accessorKey: 'text',
|
||||
styles: ['fontWeight', 'textSize', 'textColor'],
|
||||
defaultValue: {
|
||||
text: 'Modal title',
|
||||
textSize: 20,
|
||||
textColor: '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 22,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterCancel',
|
||||
properties: ['text'],
|
||||
styles: ['type', 'borderColor', 'padding'],
|
||||
defaultValue: {
|
||||
text: 'Button1',
|
||||
type: 'outline',
|
||||
borderColor: '#CCD1D5',
|
||||
},
|
||||
},
|
||||
{
|
||||
componentName: 'Button',
|
||||
slotName: 'footer',
|
||||
layout: {
|
||||
top: 24,
|
||||
left: 32,
|
||||
height: 36,
|
||||
},
|
||||
displayName: 'ModalFooterConfirm',
|
||||
properties: ['text'],
|
||||
defaultValue: {
|
||||
text: 'Button2',
|
||||
padding: 'none',
|
||||
},
|
||||
},
|
||||
],
|
||||
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',
|
||||
},
|
||||
},
|
||||
bodyBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Body background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: '#ffffffff',
|
||||
},
|
||||
},
|
||||
triggerButtonBackgroundColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button background color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
triggerButtonTextColor: {
|
||||
type: 'color',
|
||||
displayName: 'Trigger button text color',
|
||||
validation: {
|
||||
schema: { type: 'string' },
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
exposedVariables: {
|
||||
show: false,
|
||||
isDisabledModal: false,
|
||||
isDisabledTrigger: false,
|
||||
isVisible: true,
|
||||
isLoading: false,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
handle: 'open',
|
||||
displayName: 'Open',
|
||||
},
|
||||
{
|
||||
handle: 'close',
|
||||
displayName: 'Close',
|
||||
},
|
||||
{
|
||||
handle: 'setVisibility',
|
||||
displayName: 'Set visibility',
|
||||
params: [{ handle: 'setVisibility', displayName: 'Value', defaultValue: '{{true}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableTrigger',
|
||||
displayName: 'Set disable trigger',
|
||||
params: [{ handle: 'setDisableTrigger', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setDisableModal',
|
||||
displayName: 'Set disable modal',
|
||||
params: [{ handle: 'setDisableModal', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
{
|
||||
handle: 'setLoading',
|
||||
displayName: 'Set loading',
|
||||
params: [{ handle: 'setLoading', displayName: 'Value', defaultValue: '{{false}}', type: 'toggle' }],
|
||||
},
|
||||
],
|
||||
definition: {
|
||||
others: {
|
||||
showOnDesktop: { value: '{{true}}' },
|
||||
showOnMobile: { value: '{{false}}' },
|
||||
},
|
||||
properties: {
|
||||
loadingState: { value: `{{false}}` },
|
||||
visibility: { value: '{{true}}' },
|
||||
disabledTrigger: { value: '{{false}}' },
|
||||
disabledModal: { value: '{{false}}' },
|
||||
useDefaultButton: { value: `{{true}}` },
|
||||
triggerButtonLabel: { value: `Launch Modal` },
|
||||
size: { value: 'lg' },
|
||||
showHeader: { value: '{{true}}' },
|
||||
showFooter: { value: '{{true}}' },
|
||||
hideCloseButton: { value: '{{false}}' },
|
||||
hideOnEsc: { value: '{{true}}' },
|
||||
closeOnClickingOutside: { value: '{{false}}' },
|
||||
modalHeight: { value: 400 },
|
||||
headerHeight: { value: 80 },
|
||||
footerHeight: { value: 80 },
|
||||
},
|
||||
events: [],
|
||||
styles: {
|
||||
headerBackgroundColor: { value: '#ffffffff' },
|
||||
footerBackgroundColor: { value: '#ffffffff' },
|
||||
bodyBackgroundColor: { value: '#ffffffff' },
|
||||
triggerButtonBackgroundColor: { value: '#4D72FA' },
|
||||
triggerButtonTextColor: { value: '#ffffffff' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -815,7 +815,7 @@ export class AppImportExportService {
|
|||
if (newButtonToSubmitValue) set(component, 'properties.buttonToSubmit.value', newButtonToSubmitValue);
|
||||
}
|
||||
|
||||
const isParentTabOrCalendar = isChildOfTabsOrCalendar(component, pageComponents, parentId, true);
|
||||
const isParentTabOrCalendar = hasSlottedChildren(component, pageComponents, parentId, true);
|
||||
|
||||
if (isParentTabOrCalendar) {
|
||||
const childTabId = component?.parent ? component.parent?.match(/([a-fA-F0-9-]{36})-(.+)/)?.[2] : null;
|
||||
|
|
@ -1926,7 +1926,7 @@ function transformComponentData(
|
|||
|
||||
let parentId = component.parent ? component.parent : null;
|
||||
|
||||
const isParentTabOrCalendar = isChildOfTabsOrCalendar(
|
||||
const isParentTabOrCalendar = hasSlottedChildren(
|
||||
component,
|
||||
allComponents,
|
||||
parentId,
|
||||
|
|
@ -1982,7 +1982,7 @@ function transformComponentData(
|
|||
return transformedComponents;
|
||||
}
|
||||
|
||||
const isChildOfTabsOrCalendar = (
|
||||
const hasSlottedChildren = (
|
||||
component,
|
||||
allComponents = [],
|
||||
componentParentId = undefined,
|
||||
|
|
@ -1993,6 +1993,10 @@ const isChildOfTabsOrCalendar = (
|
|||
|
||||
const parentComponent = allComponents.find((comp) => comp.id === parentId);
|
||||
|
||||
const hasSlotedHeaderOrFooter = componentParentId.includes('-header') || componentParentId.includes('-footer');
|
||||
const isParentModal = parentComponent.type === 'Modal' || parentComponent.type === 'ModalV2';
|
||||
|
||||
if (isParentModal && hasSlotedHeaderOrFooter) return true;
|
||||
if (parentComponent) {
|
||||
if (!isNormalizedAppDefinitionSchema) {
|
||||
return parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar';
|
||||
|
|
|
|||
Loading…
Reference in a new issue