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",