diff --git a/frontend/src/Editor/ConfigHandle.jsx b/frontend/src/Editor/ConfigHandle.jsx
index b857ff0fd8..e59b6017b9 100644
--- a/frontend/src/Editor/ConfigHandle.jsx
+++ b/frontend/src/Editor/ConfigHandle.jsx
@@ -9,6 +9,7 @@ export const ConfigHandle = function ConfigHandle({
position,
widgetTop,
widgetHeight,
+ isMultipleComponentsSelected = false,
}) {
return (
{
e.preventDefault();
e.stopPropagation();
- setSelectedComponent(id, component);
+ setSelectedComponent(id, component, e.shiftKey);
}}
role="button"
>
@@ -37,17 +38,19 @@ export const ConfigHandle = function ConfigHandle({
/>
{component.name}
-
-
removeComponent({ id })}
- />
-
+ {!isMultipleComponentsSelected && (
+
+
removeComponent({ id })}
+ />
+
+ )}
);
diff --git a/frontend/src/Editor/Container.jsx b/frontend/src/Editor/Container.jsx
index 3a3fc69c3f..8bd5cb5be5 100644
--- a/frontend/src/Editor/Container.jsx
+++ b/frontend/src/Editor/Container.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-named-as-default */
import React, { useCallback, useState, useEffect, useRef } from 'react';
import cx from 'classnames';
import { v4 as uuidv4 } from 'uuid';
@@ -14,6 +15,7 @@ import { commentsService } from '@/_services';
import config from 'config';
import Spinner from '@/_ui/Spinner';
import { useHotkeys } from 'react-hotkeys-hook';
+import produce from 'immer';
export const Container = ({
canvasWidth,
@@ -33,7 +35,7 @@ export const Container = ({
currentLayout,
removeComponent,
deviceWindowWidth,
- selectedComponent,
+ selectedComponents,
darkMode,
showComments,
appVersionsId,
@@ -211,7 +213,7 @@ export const Container = ({
);
function onDragStop(e, componentId, direction, currentLayout) {
- const id = componentId ? componentId : uuidv4();
+ // const id = componentId ? componentId : uuidv4();
// Get the width of the canvas
const canvasBounds = document.getElementsByClassName('real-canvas')[0].getBoundingClientRect();
@@ -220,25 +222,24 @@ export const Container = ({
// Computing the left offset
const leftOffset = nodeBounds.x - canvasBounds.x;
- const left = convertXToPercentage(leftOffset, canvasWidth);
+ const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
+ const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
// Computing the top offset
- const top = nodeBounds.y - canvasBounds.y;
+ // const currentTopOffset = boxes[componentId].layouts[currentLayout].top;
+ const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
- let newBoxes = {
- ...boxes,
- [id]: {
- ...boxes[id],
- layouts: {
- ...boxes[id]['layouts'],
- [currentLayout]: {
- ...boxes[id]['layouts'][currentLayout],
- top: top,
- left: left,
- },
- },
- },
- };
+ let newBoxes = { ...boxes };
+
+ 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);
}
@@ -310,7 +311,7 @@ export const Container = ({
}
}
- React.useEffect(() => {}, [selectedComponent]);
+ React.useEffect(() => {}, [selectedComponents]);
const handleAddThread = async (e) => {
e.stopPropogation && e.stopPropogation();
@@ -466,10 +467,11 @@ export const Container = ({
removeComponent={removeComponent}
currentLayout={currentLayout}
deviceWindowWidth={deviceWindowWidth}
- isSelectedComponent={selectedComponent ? selectedComponent.id === key : false}
+ isSelectedComponent={selectedComponents.find((component) => component.id === key)}
darkMode={darkMode}
onComponentHover={onComponentHover}
hoveredComponent={hoveredComponent}
+ isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false}
dataQueries={dataQueries}
containerProps={{
mode,
@@ -487,7 +489,7 @@ export const Container = ({
removeComponent,
currentLayout,
deviceWindowWidth,
- selectedComponent,
+ selectedComponents,
darkMode,
onComponentHover,
hoveredComponent,
diff --git a/frontend/src/Editor/DraggableBox.jsx b/frontend/src/Editor/DraggableBox.jsx
index e4b90c92e8..6a634fe6ef 100644
--- a/frontend/src/Editor/DraggableBox.jsx
+++ b/frontend/src/Editor/DraggableBox.jsx
@@ -95,6 +95,7 @@ export const DraggableBox = function DraggableBox({
parentId,
hoveredComponent,
onComponentHover,
+ isMultipleComponentsSelected,
dataQueries,
}) {
const [isResizing, setResizing] = useState(false);
@@ -220,7 +221,7 @@ export const DraggableBox = function DraggableBox({
mouseOver || isResizing || isDragging2 || isSelectedComponent ? 'resizer-active' : ''
} `}
onResize={() => setResizing(true)}
- onDrag={(e) => {
+ onDrag={(e, direction) => {
e.preventDefault();
e.stopImmediatePropagation();
if (!isDragging2) {
@@ -252,7 +253,10 @@ export const DraggableBox = function DraggableBox({
position={currentLayoutOptions.top < 15 ? 'bottom' : 'top'}
widgetTop={currentLayoutOptions.top}
widgetHeight={currentLayoutOptions.height}
- setSelectedComponent={(id, component) => setSelectedComponent(id, component)}
+ setSelectedComponent={(id, component, multiSelect) =>
+ setSelectedComponent(id, component, multiSelect)
+ }
+ isMultipleComponentsSelected={isMultipleComponentsSelected}
/>
)}
diff --git a/frontend/src/Editor/Editor.jsx b/frontend/src/Editor/Editor.jsx
index 534f8ec6f3..a01121e183 100644
--- a/frontend/src/Editor/Editor.jsx
+++ b/frontend/src/Editor/Editor.jsx
@@ -7,6 +7,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { computeComponentName } from '@/_helpers/utils';
import { defaults, cloneDeep, isEqual, isEmpty, debounce } from 'lodash';
import { Container } from './Container';
+import { EditorKeyHooks } from './EditorKeyHooks';
import { CustomDragLayer } from './CustomDragLayer';
import { LeftSidebar } from './LeftSidebar';
import { componentTypes } from './Components/components';
@@ -148,7 +149,7 @@ class Editor extends React.Component {
this.initEventListeners();
this.setState({
currentSidebarTab: 2,
- selectedComponent: null,
+ selectedComponents: [],
});
}
@@ -436,9 +437,6 @@ class Editor extends React.Component {
};
switchSidebarTab = (tabIndex) => {
- if (tabIndex === 2) {
- this.setState({ selectedComponent: null });
- }
this.setState({
currentSidebarTab: tabIndex,
});
@@ -542,20 +540,49 @@ class Editor extends React.Component {
computeComponentState(this, newDefinition.components);
};
- handleInspectorView = (component) => {
- if (this.state.selectedComponent?.hasOwnProperty('component')) {
- const { id: selectedComponentId } = this.state.selectedComponent;
- if (selectedComponentId === component.id) {
- this.setState({ selectedComponent: null });
- this.switchSidebarTab(2);
- }
- }
+ handleInspectorView = () => {
+ this.switchSidebarTab(2);
};
handleSlugChange = (newSlug) => {
this.setState({ slug: newSlug });
};
+ removeComponents = () => {
+ if (!this.isVersionReleased() && this.state?.selectedComponents?.length > 1) {
+ let newDefinition = cloneDeep(this.state.appDefinition);
+ const selectedComponents = this.state?.selectedComponents;
+
+ selectedComponents.forEach((component) => {
+ let childComponents = [];
+
+ if (newDefinition.components[component.id].component.component === 'Tabs') {
+ childComponents = Object.keys(newDefinition.components).filter((key) =>
+ newDefinition.components[key].parent?.startsWith(component.id)
+ );
+ } else {
+ childComponents = Object.keys(newDefinition.components).filter(
+ (key) => newDefinition.components[key].parent === component.id
+ );
+ }
+
+ childComponents.forEach((componentId) => {
+ delete newDefinition.components[componentId];
+ });
+
+ delete newDefinition.components[component.id];
+ });
+
+ toast('Selected components deleted! (⌘Z to undo)', {
+ icon: '🗑️',
+ });
+ this.appDefinitionChanged(newDefinition, {
+ skipAutoSave: this.isVersionReleased(),
+ });
+ this.handleInspectorView();
+ }
+ };
+
removeComponent = (component) => {
if (!this.isVersionReleased()) {
let newDefinition = cloneDeep(this.state.appDefinition);
@@ -584,7 +611,7 @@ class Editor extends React.Component {
this.appDefinitionChanged(newDefinition, {
skipAutoSave: this.isVersionReleased(),
});
- this.handleInspectorView(component);
+ this.handleInspectorView();
}
};
@@ -615,6 +642,47 @@ class Editor extends React.Component {
});
};
+ handleEditorEscapeKeyPress = () => {
+ if (this.state?.selectedComponents?.length > 0) {
+ this.setState({ selectedComponents: [] });
+ this.handleInspectorView();
+ }
+ };
+
+ moveComponents = (direction) => {
+ let appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
+ let newComponents = appDefinition.components;
+
+ for (const selectedComponent of this.state.selectedComponents) {
+ newComponents = produce(newComponents, (draft) => {
+ let top = draft[selectedComponent.id].layouts[this.state.currentLayout].top;
+ let left = draft[selectedComponent.id].layouts[this.state.currentLayout].left;
+
+ const gridWidth = (1 * 100) / 43; // width of the canvas grid in percentage
+
+ switch (direction) {
+ case 'ArrowLeft':
+ left = left - gridWidth;
+ break;
+ case 'ArrowRight':
+ left = left + gridWidth;
+ break;
+ case 'ArrowDown':
+ top = top + 10;
+ break;
+ case 'ArrowUp':
+ top = top - 10;
+ break;
+ }
+
+ draft[selectedComponent.id].layouts[this.state.currentLayout].top = top;
+ draft[selectedComponent.id].layouts[this.state.currentLayout].left = left;
+ });
+ }
+ appDefinition.components = newComponents;
+ this.appDefinitionChanged(appDefinition);
+ };
+
cloneComponent = (newComponent) => {
const appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
@@ -824,9 +892,22 @@ class Editor extends React.Component {
this.setState({ showComments: !this.state.showComments });
};
- setSelectedComponent = (id, component) => {
- this.switchSidebarTab(1);
- this.setState({ selectedComponent: { id, component } });
+ setSelectedComponent = (id, component, multiSelect = false) => {
+ if (this.state.selectedComponents.length === 0 || !multiSelect) {
+ this.switchSidebarTab(1);
+ } else {
+ this.switchSidebarTab(2);
+ }
+
+ const isAlreadySelected = this.state.selectedComponents.find((component) => component.id === id);
+
+ if (!isAlreadySelected) {
+ this.setState((prevState) => {
+ return {
+ selectedComponents: [...(multiSelect ? prevState.selectedComponents : []), { id, component }],
+ };
+ });
+ }
};
filterQueries = (value) => {
@@ -982,7 +1063,7 @@ class Editor extends React.Component {
render() {
const {
currentSidebarTab,
- selectedComponent = {},
+ selectedComponents = [],
appDefinition,
appId,
slug,
@@ -1148,7 +1229,7 @@ class Editor extends React.Component {
appDefinition={{
components: appDefinition.components,
queries: dataQueries,
- selectedComponent: this.state?.selectedComponent,
+ selectedComponent: selectedComponents ? selectedComponents[selectedComponents.length - 1] : {},
}}
setSelectedComponent={this.setSelectedComponent}
removeComponent={this.removeComponent}
@@ -1162,7 +1243,7 @@ class Editor extends React.Component {
style={{ transform: `scale(${zoomLevel})` }}
onClick={(e) => {
if (['real-canvas', 'modal'].includes(e.target.className)) {
- this.switchSidebarTab(2);
+ this.setState({ selectedComponents: [], currentSidebarTab: 2 });
}
}}
>
@@ -1198,7 +1279,7 @@ class Editor extends React.Component {
zoomLevel={zoomLevel}
currentLayout={currentLayout}
deviceWindowWidth={deviceWindowWidth}
- selectedComponent={selectedComponent}
+ selectedComponents={selectedComponents}
appLoading={isLoading}
onEvent={this.handleEvent}
onComponentOptionChanged={this.handleOnComponentOptionChanged}
@@ -1440,26 +1521,34 @@ class Editor extends React.Component {
{this.renderLayoutIcon(currentLayout === 'desktop')}
+
+
+
{currentSidebarTab === 1 && (
- {selectedComponent &&
+ {selectedComponents.length === 1 &&
!isEmpty(appDefinition.components) &&
- !isEmpty(appDefinition.components[selectedComponent.id]) ? (
+ !isEmpty(appDefinition.components[selectedComponents[0].id]) ? (
) : (
-
Please select a component to inspect
+
Please select a component to inspect
)}
)}
diff --git a/frontend/src/Editor/EditorKeyHooks.jsx b/frontend/src/Editor/EditorKeyHooks.jsx
new file mode 100644
index 0000000000..09ce9f3742
--- /dev/null
+++ b/frontend/src/Editor/EditorKeyHooks.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import useKeyHooks from '@/_hooks/useKeyHooks';
+
+export const EditorKeyHooks = ({ moveComponents, handleEditorEscapeKeyPress, removeMultipleComponents }) => {
+ const handleHotKeysCallback = (key) => {
+ switch (key) {
+ case 'Escape':
+ handleEditorEscapeKeyPress();
+ break;
+ case 'Backspace':
+ removeMultipleComponents();
+ break;
+ default:
+ moveComponents(key);
+ }
+ };
+
+ useKeyHooks(['up, down, left, right', 'esc', 'backspace'], handleHotKeysCallback);
+
+ return <>>;
+};
diff --git a/frontend/src/Editor/SubContainer.jsx b/frontend/src/Editor/SubContainer.jsx
index d98ea21f5f..6b45620a33 100644
--- a/frontend/src/Editor/SubContainer.jsx
+++ b/frontend/src/Editor/SubContainer.jsx
@@ -7,6 +7,7 @@ import { snapToGrid as doSnapToGrid } from './snapToGrid';
import update from 'immutability-helper';
import { componentTypes } from './Components/components';
import { computeComponentName } from '@/_helpers/utils';
+import produce from 'immer';
export const SubContainer = ({
mode,
@@ -35,6 +36,7 @@ export const SubContainer = ({
listViewItemOptions,
onComponentHover,
hoveredComponent,
+ selectedComponents,
}) => {
const [_containerCanvasWidth, setContainerCanvasWidth] = useState(0);
@@ -251,9 +253,6 @@ export const SubContainer = ({
}
function onDragStop(e, componentId, direction, currentLayout) {
- const id = componentId ? componentId : uuidv4();
-
- // Get the width of the canvas
const canvasWidth = getContainerCanvasWidth();
const nodeBounds = direction.node.getBoundingClientRect();
@@ -261,25 +260,24 @@ export const SubContainer = ({
// Computing the left offset
const leftOffset = nodeBounds.x - canvasBounds.x;
- const left = convertXToPercentage(leftOffset, canvasWidth);
+ const currentLeftOffset = boxes[componentId].layouts[currentLayout].left;
+ const leftDiff = currentLeftOffset - convertXToPercentage(leftOffset, canvasWidth);
- // Computing the top offset
- const top = nodeBounds.y - canvasBounds.y;
+ const topDiff = boxes[componentId].layouts[currentLayout].top - (nodeBounds.y - canvasBounds.y);
- let newBoxes = {
- ...boxes,
- [id]: {
- ...boxes[id],
- layouts: {
- ...boxes[id]['layouts'],
- [currentLayout]: {
- ...boxes[id]['layouts'][currentLayout],
- top: top,
- left: left,
- },
- },
- },
- };
+ 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);
}
@@ -418,7 +416,7 @@ export const SubContainer = ({
currentLayout={currentLayout}
selectedComponent={selectedComponent}
deviceWindowWidth={deviceWindowWidth}
- isSelectedComponent={selectedComponent ? selectedComponent.id === key : false}
+ isSelectedComponent={selectedComponents.find((component) => component.id === key)}
removeComponent={customRemoveComponent}
canvasWidth={_containerCanvasWidth}
readOnly={readOnly}
@@ -427,6 +425,7 @@ export const SubContainer = ({
onComponentHover={onComponentHover}
hoveredComponent={hoveredComponent}
parentId={parentComponent?.name}
+ isMultipleComponentsSelected={selectedComponents?.length > 1 ? true : false}
containerProps={{
mode,
snapToGrid,
diff --git a/frontend/src/_hooks/useKeyHooks.js b/frontend/src/_hooks/useKeyHooks.js
new file mode 100644
index 0000000000..4344adf59b
--- /dev/null
+++ b/frontend/src/_hooks/useKeyHooks.js
@@ -0,0 +1,9 @@
+import { useHotkeys } from 'react-hotkeys-hook';
+
+const useKeyHooks = (hotkeys = [], callback) =>
+ useHotkeys(hotkeys.toString(), (e) => {
+ e.preventDefault();
+ callback(e.code);
+ });
+
+export default useKeyHooks;
diff --git a/plugins/package-lock.json b/plugins/package-lock.json
index 1571570ce3..c55a7d310a 100644
--- a/plugins/package-lock.json
+++ b/plugins/package-lock.json
@@ -16818,6 +16818,7 @@
}
},
"packages/notion": {
+ "name": "@tooljet-plugins/notion",
"version": "1.0.0",
"dependencies": {
"@notionhq/client": "^1.0.4",