ToolJet/frontend/src/Editor/SubContainer.jsx

478 lines
15 KiB
React
Raw Normal View History

/* eslint-disable import/no-named-as-default */
2021-05-09 18:06:11 +00:00
import React, { useCallback, useState, useEffect } from 'react';
import { useDrop, useDragLayer } from 'react-dnd';
import { v4 as uuidv4 } from 'uuid';
2021-05-09 18:06:11 +00:00
import { ItemTypes } from './ItemTypes';
import { DraggableBox } from './DraggableBox';
import { snapToGrid as doSnapToGrid } from './snapToGrid';
import update from 'immutability-helper';
import { componentTypes } from './WidgetManager/components';
2021-05-09 18:06:11 +00:00
import { computeComponentName } from '@/_helpers/utils';
import produce from 'immer';
2021-05-09 18:06:11 +00:00
export const SubContainer = ({
mode,
snapToGrid,
onComponentClick,
onEvent,
appDefinition,
appDefinitionChanged,
currentState,
onComponentOptionChanged,
onComponentOptionsChanged,
appLoading,
zoomLevel,
parent,
parentRef,
setSelectedComponent,
deviceWindowWidth,
selectedComponent,
currentLayout,
removeComponent,
darkMode,
containerCanvasWidth,
readOnly,
customResolvables,
parentComponent,
listViewItemOptions,
onComponentHover,
hoveredComponent,
selectedComponents,
2021-05-09 18:06:11 +00:00
}) => {
const [_containerCanvasWidth, setContainerCanvasWidth] = useState(0);
useEffect(() => {
if (parentRef.current) {
const canvasWidth = getContainerCanvasWidth();
setContainerCanvasWidth(canvasWidth);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [parentRef, getContainerCanvasWidth()]);
2021-05-09 18:06:11 +00:00
zoomLevel = zoomLevel || 1;
// eslint-disable-next-line react-hooks/exhaustive-deps
2021-05-09 18:06:11 +00:00
const allComponents = appDefinition ? appDefinition.components : {};
let childComponents = [];
2021-05-09 18:06:11 +00:00
Object.keys(allComponents).forEach((key) => {
if (allComponents[key].parent === parent) {
childComponents[key] = { ...allComponents[key], component: { ...allComponents[key]['component'], parent } };
2021-05-09 18:06:11 +00:00
}
});
2021-05-09 18:06:11 +00:00
const [boxes, setBoxes] = useState(allComponents);
const [isDragging, setIsDragging] = useState(false);
const [isResizing, setIsResizing] = useState(false);
useEffect(() => {
setBoxes(allComponents);
}, [allComponents]);
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top },
},
2021-05-09 18:06:11 +00:00
})
);
},
[boxes]
);
useEffect(() => {
if (appDefinitionChanged) {
2021-05-09 18:06:11 +00:00
appDefinitionChanged({ ...appDefinition, components: boxes });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
2021-05-09 18:06:11 +00:00
}, [boxes]);
const { draggingState } = useDragLayer((monitor) => {
// TODO: Need to move to a performant version of the block below
if (monitor.getItem()) {
if (monitor.getItem().id === undefined) {
if (parentRef.current) {
const currentOffset = monitor.getSourceClientOffset();
if (currentOffset) {
const canvasBoundingRect = parentRef.current
.getElementsByClassName('real-canvas')[0]
.getBoundingClientRect();
if (
currentOffset.x > canvasBoundingRect.x &&
currentOffset.x < canvasBoundingRect.x + canvasBoundingRect.width
) {
return { draggingState: true };
}
}
}
}
}
if (monitor.isDragging() && monitor.getItem().parent) {
if (monitor.getItem().parent === parent) {
return { draggingState: true };
} else {
return { draggingState: false };
}
} else {
return { draggingState: false };
}
});
2021-05-09 18:06:11 +00:00
useEffect(() => {
setIsDragging(draggingState);
}, [draggingState]);
function convertXToPercentage(x, canvasWidth) {
return (x * 100) / canvasWidth;
}
function convertXFromPercentage(x, canvasWidth) {
return (x * canvasWidth) / 100;
}
2021-05-09 18:06:11 +00:00
const [, drop] = useDrop(
() => ({
accept: ItemTypes.BOX,
drop(item, monitor) {
let componentData = {};
let componentMeta = {};
let id = item.id;
let left = 0;
let top = 0;
let layouts = item['layouts'];
const currentLayoutOptions = layouts ? layouts[item.currentLayout] : {};
2021-05-09 18:06:11 +00:00
const canvasBoundingRect = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
// Component already exists and this is just a reposition event
if (id) {
const delta = monitor.getDifferenceFromInitialOffset();
componentData = item.component;
left = Math.round(convertXFromPercentage(currentLayoutOptions.left, canvasBoundingRect.width) + delta.x);
top = Math.round(currentLayoutOptions.top + delta.y);
if (snapToGrid) {
[left, top] = doSnapToGrid(canvasBoundingRect.width, left, top);
}
left = convertXToPercentage(left, canvasBoundingRect.width);
let newBoxes = {
...boxes,
[id]: {
...boxes[id],
parent: parent,
layouts: {
...boxes[id]['layouts'],
[item.currentLayout]: {
...boxes[id]['layouts'][item.currentLayout],
top: top,
left: left,
},
},
},
};
setBoxes(newBoxes);
2021-05-09 18:06:11 +00:00
} else {
// This is a new component
componentMeta = componentTypes.find((component) => component.component === item.component.component);
componentData = JSON.parse(JSON.stringify(componentMeta));
componentData.name = computeComponentName(componentData.component, boxes);
const offsetFromTopOfWindow = canvasBoundingRect.top;
const offsetFromLeftOfWindow = canvasBoundingRect.left;
const currentOffset = monitor.getSourceClientOffset();
const initialClientOffset = monitor.getInitialClientOffset();
const delta = monitor.getDifferenceFromInitialOffset();
2021-05-09 18:06:11 +00:00
left = Math.round(currentOffset.x + currentOffset.x * (1 - zoomLevel) - offsetFromLeftOfWindow);
top = Math.round(
initialClientOffset.y - 10 + delta.y + initialClientOffset.y * (1 - zoomLevel) - offsetFromTopOfWindow
);
2021-05-09 18:06:11 +00:00
id = uuidv4();
}
const subContainerWidth = canvasBoundingRect.width;
2021-05-09 18:06:11 +00:00
if (snapToGrid) {
[left, top] = doSnapToGrid(subContainerWidth, left, top);
2021-05-09 18:06:11 +00:00
}
if (item.currentLayout === 'mobile') {
componentData.definition.others.showOnDesktop.value = false;
componentData.definition.others.showOnMobile.value = true;
}
// convert the left offset to percentage
left = (left * 100) / subContainerWidth;
const width = (componentMeta.defaultSize.width * 100) / 43;
2021-05-09 18:06:11 +00:00
setBoxes({
...boxes,
[id]: {
component: componentData,
parent: parentRef.current.id,
layouts: {
[item.currentLayout]: {
top: top,
left: left,
width: width,
height: componentMeta.defaultSize.height,
},
},
},
2021-05-09 18:06:11 +00:00
});
return undefined;
},
2021-05-09 18:06:11 +00:00
}),
[moveBox]
);
function getContainerCanvasWidth() {
if (containerCanvasWidth !== undefined) {
return containerCanvasWidth;
}
let width = 0;
if (parentRef.current) {
const realCanvas = parentRef.current.getElementsByClassName('real-canvas')[0];
if (realCanvas) {
const canvasBoundingRect = realCanvas.getBoundingClientRect();
width = canvasBoundingRect.width;
}
}
return width;
}
function onDragStop(e, componentId, direction, currentLayout) {
const canvasWidth = getContainerCanvasWidth();
const nodeBounds = direction.node.getBoundingClientRect();
const canvasBounds = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
// Computing the left offset
const leftOffset = nodeBounds.x - canvasBounds.x;
const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
let newBoxes = { ...boxes };
if (selectedComponents) {
for (const selectedComponent of selectedComponents) {
newBoxes = produce(newBoxes, (draft) => {
const topOffset = draft[selectedComponent.id].layouts[currentLayout].top;
const leftOffset = draft[selectedComponent.id].layouts[currentLayout].left;
draft[selectedComponent.id].layouts[currentLayout].top = topOffset - topDiff;
draft[selectedComponent.id].layouts[currentLayout].left = leftOffset - leftDiff;
});
}
}
setBoxes(newBoxes);
}
function onResizeStop(id, e, direction, ref, d, position) {
2021-05-09 18:06:11 +00:00
const deltaWidth = d.width;
const deltaHeight = d.height;
let { x, y } = position;
const defaultData = {
top: 100,
left: 0,
width: 445,
height: 500,
};
let { left, top, width, height } = boxes[id]['layouts'][currentLayout] || defaultData;
const canvasBoundingRect = parentRef.current.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
const subContainerWidth = canvasBoundingRect.width;
top = y;
left = (x * 100) / subContainerWidth;
width = width + (deltaWidth * 43) / subContainerWidth;
height = height + deltaHeight;
let newBoxes = {
...boxes,
[id]: {
...boxes[id],
layouts: {
...boxes[id]['layouts'],
[currentLayout]: {
...boxes[id]['layouts'][currentLayout],
width,
height,
top,
left,
},
},
},
};
setBoxes(newBoxes);
2021-05-09 18:06:11 +00:00
}
function paramUpdated(id, param, value) {
if (Object.keys(value).length > 0) {
setBoxes(
update(boxes, {
[id]: {
$merge: {
component: {
...boxes[id].component,
definition: {
...boxes[id].component.definition,
properties: {
...boxes[id].component.definition.properties,
[param]: value,
},
},
},
},
},
2021-05-09 18:06:11 +00:00
})
);
}
}
const styles = {
width: '100%',
height: '100%',
position: 'absolute',
backgroundSize: `${getContainerCanvasWidth() / 43}px 10px`,
};
function onComponentOptionChangedForSubcontainer(component, optionName, value, extraProps) {
if (parentComponent?.component === 'Listview') {
let newData = currentState.components[parentComponent.name]?.data || [];
newData[listViewItemOptions.index] = {
...newData[listViewItemOptions.index],
[component.name]: {
...(newData[listViewItemOptions.index] ? newData[listViewItemOptions.index][component.name] : {}),
[optionName]: value,
},
};
return onComponentOptionChanged(parentComponent, 'data', newData);
} else {
return onComponentOptionChanged(component, optionName, value, extraProps);
}
}
function customRemoveComponent(component) {
const componentName = appDefinition.components[component.id]['component'].name;
removeComponent(component);
if (parentComponent.component === 'Listview') {
const currentData = currentState.components[parentComponent.name]?.data || [];
const newData = currentData.map((widget) => {
delete widget[componentName];
return widget;
});
onComponentOptionChanged(parentComponent, 'data', newData);
}
}
2021-05-09 18:06:11 +00:00
return (
<div
ref={drop}
style={styles}
id={`canvas-${parent}`}
className={`real-canvas ${(isDragging || isResizing) && !readOnly ? ' show-grid' : ''}`}
>
{Object.keys(childComponents).map((key) => (
2021-05-09 18:06:11 +00:00
<DraggableBox
onComponentClick={onComponentClick}
onEvent={onEvent}
onComponentOptionChanged={onComponentOptionChangedForSubcontainer}
2021-05-09 18:06:11 +00:00
onComponentOptionsChanged={onComponentOptionsChanged}
key={key}
currentState={currentState}
onResizeStop={onResizeStop}
onDragStop={onDragStop}
2021-05-09 18:06:11 +00:00
paramUpdated={paramUpdated}
id={key}
extraProps={{ listviewItemIndex: listViewItemOptions?.index }}
allComponents={allComponents}
{...childComponents[key]}
2021-05-10 11:36:33 +00:00
mode={mode}
2021-05-09 18:06:11 +00:00
resizingStatusChanged={(status) => setIsResizing(status)}
draggingStatusChanged={(status) => setIsDragging(status)}
2021-05-09 18:06:11 +00:00
inCanvas={true}
zoomLevel={zoomLevel}
setSelectedComponent={setSelectedComponent}
currentLayout={currentLayout}
selectedComponent={selectedComponent}
deviceWindowWidth={deviceWindowWidth}
isSelectedComponent={mode === 'edit' ? selectedComponents.find((component) => component.id === key) : false}
removeComponent={customRemoveComponent}
canvasWidth={_containerCanvasWidth}
readOnly={readOnly}
darkMode={darkMode}
customResolvables={customResolvables}
onComponentHover={onComponentHover}
hoveredComponent={hoveredComponent}
parentId={parentComponent?.name}
isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false}
containerProps={{
mode,
snapToGrid,
onComponentClick,
onEvent,
appDefinition,
appDefinitionChanged,
currentState,
onComponentOptionChanged,
onComponentOptionsChanged,
appLoading,
zoomLevel,
setSelectedComponent,
removeComponent,
currentLayout,
deviceWindowWidth,
selectedComponents,
darkMode,
readOnly,
onComponentHover,
hoveredComponent,
}}
2021-05-09 18:06:11 +00:00
/>
))}
{Object.keys(boxes).length === 0 && !appLoading && !isDragging && (
<div className="mx-auto mt-5 w-50 p-5 bg-light no-components-box">
<center className="text-muted">
Feature: User access management 🔥 (#918) * create migrations for group permissions setup * define new entities and relationships * revise migrations * rename columns * add migration to populate permission groups for existing users * Feature: User access permission group usage (#883) * create migrations for group permissions setup * define new entities and relationships * revise migrations * rename columns * add migration to populate permission groups for existing users * revise migrations * hide roles usage * setup group permissions for apps and users * fix defaultChecked * fix update permission checkbox * fix casl ability check to have params passed * fix casl apps abilities to check with app specific permission * add ability to delete groups * conditionally render edit and delete options for all and admin users * fix user role to group migration * revise group management pages to disallow updating default group * move manage users and groups to navbar dropdown * show only addable apps and users on dropdowns * rename header as profile settings * scope addable apps and users by organization * scope viewable apps on homepage * hide manage groups link from non admins * make permissions to be used with radio input * add loading state for add apps/users buttons * revise unit tests * revise migrations * fix e2e tests * comment out dead code * fix seeds script * handle folder count * captalize error toast * hide manage users dropdown for non admins * show fobidden error on blank homepage * fix folder app count * fix invalid state set * make group name clickable for edit instead * users with edit permission can deploy apps * not show edit link on homepage if user dont have update permission * remove unused entity from merge * remove roles usage from manage org users page * fix folder count and blank slate on homepage * disable add buttons if there is no selections * humanize default groups on view * make app added onto groups have read permission by default * not show app menu if user is not admin * remove admin users from group user addition dropdown * create default permissions for app cloned * fix querying index page without page params * fix admin scoped out from group add * remove apps from header * fix invitation url not shown * scope admin deletion check by org * scope public apps by organization * add specs for group permissions e2e * removed unused entity and add group permissions spec * remove console logs * remove unused permission * scope public app count by org * remove console log * refactor manage group permission resources component * update group permssion in org scope
2021-10-11 15:15:58 +00:00
Drag components from the right sidebar and drop here. Check out our{' '}
<a href="https://docs.tooljet.io/docs/tutorial/adding-widget" target="_blank" rel="noreferrer">
guide
</a>{' '}
on adding widgets.
</center>
2021-05-09 18:06:11 +00:00
</div>
)}
{appLoading && (
<div className="mx-auto mt-5 w-50 p-5">
<center>
<div className="progress progress-sm w-50">
<div className="progress-bar progress-bar-indeterminate"></div>
</div>
</center>
</div>
)}
</div>
);
};