ToolJet/frontend/src/Editor/DraggableBox.jsx
Sherfin Shamsudeen 642c5caa71
Feature/multi page applications (Task ID - CU-2h1bfvw) (#4729)
* Add routes for multi-page apps

* Modify Editor, Viewer and Inspector to accept new app structure

* Show a page selector on left side bar

* Align component deletion logic with new app schema

* Make subcontainer work with multi-page apps

* Load components state properly in viewer

* Use UUID instead of handle for pages

* Display sidebar on viewer to switch pages

* Add proper URL suffixing for pages in viewer

* Add action to switch page

* Revert translation file back to its pre-existing linting

* Fix bug that caused modal to not open/close

* Add support for query params in page switch

* Fix the issue that caused navigation to fail while accessed via slug

* Add missing SwitchPage file

* Add support for page level variables

* Add migration to convert existing apps to new schema

* Add rollback for converting multi-page definitions back to single-page

* Fix migration for multi-page apps

* Adapt import/export service for multi-pages

* [improvements] Multi-page applications (#4755)

* UI updates for page selector popup card

* delete page

* delete page check: if only one page exits

* switch to home page if the selected page is removed

* adds and switch to new page

* updating page name

* updates to home page and starting page

* handle updating the home page when home page is deleted

* search box for filtering pages and minor style updates for the page handler card

* header search box style fixes

* for creating a new page, page handle needs to be unique

* seperating into smaller components

* updated pinned icon for page selector styles and settinf styles

* Leftsidebar header ui component

* handle dark theme

* page handle ui and dark theme fixes for page menu

* page handler edit modal

* pinned state and update pinned state for menu options triggered

* dark theme fixes for edit modal

* handle on update should not be empty or prev

* page handler updater

* added loading state for saving

* handles cancels

* fixes slug ui

* fixes crash for older app versions

* updates the query params when handle gets an update

* update homePage to homePageId

* removes console.log

* go back to the popover for modal close

* fixes: Difficult to select page

* fixes: Difficult to select the three-dot menu

* fixes: on visiting the root url, navigate to homepage on viewer

* adds tooltip for url

* updates the page selector sidebar with sync with query manager

* refactor and cleanup

* refactor and cleanup

* Compute component state when page is switched

* modal should not close on click outside

* disable save button if there is not change in the page handle input

* should show/hide page menu when hovered

* page icon

* updates delete icon for disabled state

* query manager should always be on top of page selector

* checks if homePage key exists in pages def

* updates page handler menu

* updates the clear icon

* page handler menu position

* page handler menu position

* handle icon

* alert msg

* global settings handler for updating viewer page navigation

* show/hode page navigation for viewer

* info text for toggle

* Multipages:with sortable list [DnD] (#4783)

* applied sortable list

* on sort updates the definitions

* fixies: app crash for dnd

* viwer: canvas width should be 100% when navigation drawer is disbaled

* fixes: homepage/startpage  reload

* clean up

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* Multipage UI viewer (#4801)

* new ui changes for viewer pages

* fixes postions for debugger and datasources popover

* removes console.log

* Multipage : hide page and unhide page feature (#4803)

* adds: ability to hide pages

* hides pages in viewer

* unhide page

* hide icon

* allow accessing hidden pages from url

* add: duplicate page (#4802)

* add: duplicate page

* do not copy the  same references from the original page

* page name and page handler should be unique for duplicate pages too

* Add support for on-page-load events

* Add icon from page settings menu item

* Convert existing templates to multi-page schema

* error logs for page level and app level errors (#4842)

* Adapt comments feature for multi-pages

* [Bugfix] multipage - page menu interactions (#4844)

* fixes: menu popup interaction

* fixes: on modal input focus, we switch the page

* Adapt multi-player to multi-pages

* Add editingPageId to ymap

* Log self, others and editor props in real-time avatar generation

* Save editing page id to appDef

* Add editingPageId to presence in RealtimeCursors

* adds no results ui for empty search results (#4869)

* page icon updated (#4870)

* fixes:Version switching crashes if the target version does not contain the current page (#4868)

* Remove unnecessary setting of editingPageId on ymap

* Remove unnecessary console.log

* [Bugfix] Multipages: widget inspector event popover unmounts (#4887)

* introduced a local state for events

* cleaned up inspector.jsx

* fixes: table widget inspector event accordion

* Do not run switchPage twice when viewer is loaded

* Preview should open the currently editing page

* Properly place navigation and canvas in viewer

* Update app definition whenever event manager changes are made

* Add support for browser back and forward button in multi-pages

* Rename handleBackButton to handlePageSwitchingBasedOnURLparam

* Add support for cut/copy/paste and clone

* Fix the crash caused by boxShadow

* Add support for background colors in viewer in multi-pages

* Run queries to be run on load on viewer, in multi-pages

* Fix issue that caused inspector popovers to collapse

* resolves workspace vars in viewer mode (#4892)

* Multipage : Navigation for Mobile-ui (#4814)

* refactored to components

* burger menu for mobile ui

* merge conflict fix for hidden pages

* hamburger menu positioned in the header

* viewer header reafctored

* viewer mobile page manu styles

* handles dark theme

* mobile menu with dark mode toggle in the footer

* components are moved to page level, handle for mobile layout

* style fixes

* removing unwanted code block

* dark theme fixes

* style fixes

* fixes: events are sortable (#4895)

* fixes: events are sortable

* Remove uneccesarily repeated call of setEvents in EventManager

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* renamed settings to Event handlers (#4898)

* updates the page setting title to Page Events

* temp commit

* Add support for setting max width in percentage

* fixes: paramUpdates for boxes: 🙌🏻

* [Bugfix] Multipage - viewer canvas dark theme (#4897)

* fixes: darktheme bg for viewer canvas

* reverts canvas size

* Fix for inspector bouncing back to previous values

* resolves pages variables in pythong and js transformation (#4905)

* csa support to event manager for pages (#4907)

* Add support for setting canvas width in percentages

* Persist page level variables across page switches

* latest definitions is merged with the current appdef (#4914)

* latest definitions is merged with the current appdef

* mutating the local obj

* cleanup

* iterate through pages for new versions are created

Co-authored-by: Arpit <arpitnath42@gmail.com>
2022-12-08 17:51:09 +05:30

325 lines
9.7 KiB
JavaScript

/* eslint-disable react-hooks/exhaustive-deps */
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { useDrag } from 'react-dnd';
import { ItemTypes } from './ItemTypes';
import { getEmptyImage } from 'react-dnd-html5-backend';
import { Box } from './Box';
import { ConfigHandle } from './ConfigHandle';
import { Rnd } from 'react-rnd';
import { resolveWidgetFieldValue } from '@/_helpers/utils';
import ErrorBoundary from './ErrorBoundary';
const resizerClasses = {
topRight: 'top-right',
bottomRight: 'bottom-right',
bottomLeft: 'bottom-left',
topLeft: 'top-left',
};
const resizerStyles = {
topRight: {
width: '8px',
height: '8px',
right: '-4px',
top: '-4px',
},
bottomRight: {
width: '8px',
height: '8px',
right: '-4px',
bottom: '-4px',
},
bottomLeft: {
width: '8px',
height: '8px',
left: '-4px',
bottom: '-4px',
},
topLeft: {
width: '8px',
height: '8px',
left: '-4px',
top: '-4px',
},
};
function computeWidth(currentLayoutOptions) {
return `${currentLayoutOptions?.width}%`;
}
function getStyles(isDragging, isSelectedComponent) {
return {
position: 'absolute',
zIndex: isSelectedComponent ? 2 : 1,
// IE fallback: hide the real node using CSS when dragging
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
};
}
export const DraggableBox = function DraggableBox({
id,
className,
mode,
title,
_left,
_top,
parent,
allComponents,
component,
index,
inCanvas,
onEvent,
onComponentClick,
currentState,
onComponentOptionChanged,
onComponentOptionsChanged,
onResizeStop,
onDragStop,
paramUpdated,
resizingStatusChanged,
zoomLevel,
containerProps,
setSelectedComponent,
removeComponent,
currentLayout,
layouts,
_deviceWindowWidth,
isSelectedComponent,
draggingStatusChanged,
darkMode,
canvasWidth,
readOnly,
customResolvables,
parentId,
hoveredComponent,
onComponentHover,
sideBarDebugger,
isMultipleComponentsSelected,
dataQueries,
}) {
const [isResizing, setResizing] = useState(false);
const [isDragging2, setDragging] = useState(false);
const [canDrag, setCanDrag] = useState(true);
const [mouseOver, setMouseOver] = useState(false);
useEffect(() => {
setMouseOver(hoveredComponent === id);
}, [hoveredComponent]);
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
item: {
id,
title,
component,
zoomLevel,
parent,
layouts,
canvasWidth,
currentLayout,
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, title, component, index, zoomLevel, parent, layouts, currentLayout, canvasWidth]
);
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
}, [isDragging]);
useEffect(() => {
if (resizingStatusChanged) {
resizingStatusChanged(isResizing);
}
}, [isResizing]);
useEffect(() => {
if (draggingStatusChanged) {
draggingStatusChanged(isDragging2);
}
if (isDragging2 && !isSelectedComponent) {
setSelectedComponent(id, component);
}
}, [isDragging2]);
const style = {
display: 'inline-block',
alignItems: 'center',
justifyContent: 'center',
padding: '0px',
};
let _refProps = {};
if (mode === 'edit' && canDrag) {
_refProps = {
ref: drag,
};
}
function changeCanDrag(newState) {
setCanDrag(newState);
}
const defaultData = {
top: 100,
left: 0,
width: 445,
height: 500,
};
const layoutData = inCanvas ? layouts[currentLayout] || defaultData : defaultData;
const [currentLayoutOptions, setCurrentLayoutOptions] = useState(layoutData);
useEffect(() => {
console.log(layoutData);
setCurrentLayoutOptions(layoutData);
}, [layoutData.height, layoutData.width, layoutData.left, layoutData.top, currentLayout]);
const gridWidth = canvasWidth / 43;
const width = (canvasWidth * currentLayoutOptions.width) / 43;
const configWidgetHandlerForModalComponent =
!isSelectedComponent &&
component.component === 'Modal' &&
resolveWidgetFieldValue(component.definition.properties.useDefaultButton, currentState)?.value === false;
return (
<div
className={inCanvas ? '' : 'col-md-4 text-center align-items-center clearfix mb-2'}
style={!inCanvas ? {} : { width: computeWidth() }}
>
{inCanvas ? (
<div
className={cx(`draggable-box widget-${id}`, {
[className]: !!className,
'draggable-box-in-editor': mode === 'edit',
})}
onMouseEnter={(e) => {
if (e.currentTarget.className.includes(`widget-${id}`)) {
onComponentHover?.(id);
e.stopPropagation();
}
}}
onMouseLeave={() => onComponentHover?.(false)}
style={getStyles(isDragging, isSelectedComponent)}
>
<Rnd
style={{ ...style }}
resizeGrid={[gridWidth, 10]}
dragGrid={[gridWidth, 10]}
size={{
width: width,
height: currentLayoutOptions.height,
}}
position={{
x: currentLayoutOptions ? (currentLayoutOptions.left * canvasWidth) / 100 : 0,
y: currentLayoutOptions ? currentLayoutOptions.top : 0,
}}
defaultSize={{}}
className={`resizer ${
mouseOver || isResizing || isDragging2 || isSelectedComponent ? 'resizer-active' : ''
} `}
onResize={() => setResizing(true)}
onDrag={(e) => {
e.preventDefault();
e.stopImmediatePropagation();
if (!isDragging2) {
setDragging(true);
}
}}
resizeHandleClasses={isSelectedComponent || mouseOver ? resizerClasses : {}}
resizeHandleStyles={resizerStyles}
enableResizing={mode === 'edit' && !readOnly}
disableDragging={mode !== 'edit' || readOnly}
onDragStop={(e, direction) => {
setDragging(false);
onDragStop(e, id, direction, currentLayout, currentLayoutOptions);
}}
cancel={`div.table-responsive.jet-data-table, div.calendar-widget, div.text-input, .textarea, .map-widget, .range-slider, .kanban-container`}
onDragStart={(e) => e.stopPropagation()}
onResizeStop={(e, direction, ref, d, position) => {
setResizing(false);
onResizeStop(id, e, direction, ref, d, position);
}}
bounds={parent !== undefined ? `#canvas-${parent}` : '.real-canvas'}
widgetId={id}
>
<div ref={preview} role="DraggableBox" style={isResizing ? { opacity: 0.5 } : { opacity: 1 }}>
{mode === 'edit' &&
!readOnly &&
(configWidgetHandlerForModalComponent || mouseOver || isSelectedComponent) &&
!isResizing && (
<ConfigHandle
id={id}
removeComponent={removeComponent}
component={component}
position={currentLayoutOptions.top < 15 ? 'bottom' : 'top'}
widgetTop={currentLayoutOptions.top}
widgetHeight={currentLayoutOptions.height}
isMultipleComponentsSelected={isMultipleComponentsSelected}
configWidgetHandlerForModalComponent={configWidgetHandlerForModalComponent}
/>
)}
<ErrorBoundary showFallback={mode === 'edit'}>
<Box
component={component}
id={id}
width={width}
height={currentLayoutOptions.height - 4}
mode={mode}
changeCanDrag={changeCanDrag}
inCanvas={inCanvas}
paramUpdated={paramUpdated}
onEvent={onEvent}
onComponentOptionChanged={onComponentOptionChanged}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
currentState={currentState}
containerProps={containerProps}
darkMode={darkMode}
removeComponent={removeComponent}
canvasWidth={canvasWidth}
readOnly={readOnly}
customResolvables={customResolvables}
parentId={parentId}
allComponents={allComponents}
sideBarDebugger={sideBarDebugger}
dataQueries={dataQueries}
/>
</ErrorBoundary>
</div>
</Rnd>
</div>
) : (
<div ref={drag} role="DraggableBox" className="draggable-box" style={{ height: '100%' }}>
<ErrorBoundary showFallback={mode === 'edit'}>
<Box
component={component}
id={id}
mode={mode}
inCanvas={inCanvas}
onEvent={onEvent}
paramUpdated={paramUpdated}
onComponentOptionChanged={onComponentOptionChanged}
onComponentOptionsChanged={onComponentOptionsChanged}
onComponentClick={onComponentClick}
currentState={currentState}
darkMode={darkMode}
removeComponent={removeComponent}
sideBarDebugger={sideBarDebugger}
customResolvables={customResolvables}
containerProps={containerProps}
/>
</ErrorBoundary>
</div>
)}
</div>
);
};