[Feature]: Copy/Paste widgets (#3252)

* Copy and paste widgets

* Paste widgets into container, support multi widget copy paste

* Replaced localstorage with clipboard while copying widget

* Fixed component name bug
This commit is contained in:
Kavin Venkatachalam 2022-07-12 16:39:02 +05:30 committed by GitHub
parent e3917a13f1
commit ae5994c6c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 207 additions and 42 deletions

View file

@ -16,6 +16,7 @@ import config from 'config';
import Spinner from '@/_ui/Spinner';
import { useHotkeys } from 'react-hotkeys-hook';
import produce from 'immer';
import { addComponents } from '@/_helpers/appUtils';
export const Container = ({
canvasWidth,
@ -61,11 +62,49 @@ export const Container = ({
const [isResizing, setIsResizing] = useState(false);
const [commentsPreviewList, setCommentsPreviewList] = useState([]);
const [newThread, addNewThread] = useState({});
const [isContainerFocused, setContainerFocus] = useState(false);
const router = useRouter();
const canvasRef = useRef(null);
const focusedParentIdRef = useRef(undefined);
useHotkeys('⌘+z, control+z', () => handleUndo());
useHotkeys('⌘+shift+z, control+shift+z', () => handleRedo());
useHotkeys(
'⌘+v, control+v',
() => {
if (isContainerFocused) {
navigator.clipboard
.readText()
.then((cliptext) =>
addComponents(appDefinition, appDefinitionChanged, focusedParentIdRef.current, JSON.parse(cliptext))
);
}
},
[isContainerFocused, appDefinition, focusedParentIdRef]
);
useEffect(() => {
const handleClick = (e) => {
if (canvasRef.current.contains(e.target)) {
const elem = e.target.closest('.real-canvas').getAttribute('id');
if (elem === 'real-canvas') {
focusedParentIdRef.current = undefined;
} else {
const parentId = elem.split('canvas-')[1];
focusedParentIdRef.current = parentId;
}
if (!isContainerFocused) {
setContainerFocus(true);
}
} else if (isContainerFocused) {
setContainerFocus(false);
}
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [isContainerFocused, canvasRef]);
useEffect(() => {
setBoxes(components);
}, [components]);
@ -403,7 +442,10 @@ export const Container = ({
return (
<div
{...(config.COMMENT_FEATURE_ENABLE && showComments && { onClick: handleAddThread })}
ref={drop}
ref={(el) => {
canvasRef.current = el;
drop(el);
}}
style={styles}
className={cx('real-canvas', {
'show-grid': isDragging || isResizing,

View file

@ -34,6 +34,7 @@ import {
setStateAsync,
computeComponentState,
getSvgIcon,
cloneComponents,
} from '@/_helpers/appUtils';
import { Confirm } from './Viewer/Confirm';
import ReactTooltip from 'react-tooltip';
@ -726,14 +727,10 @@ class Editor extends React.Component {
this.appDefinitionChanged(appDefinition);
};
cloneComponent = (newComponent) => {
const appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
copyComponents = () => cloneComponents(this, this.appDefinitionChanged, false);
newComponent.component.name = computeComponentName(newComponent.component.component, appDefinition.components);
cloneComponents = () => cloneComponents(this, this.appDefinitionChanged, true);
appDefinition.components[newComponent.id] = newComponent;
this.appDefinitionChanged(appDefinition);
};
decimalToHex = (alpha) => (alpha === 0 ? '00' : Math.round(255 * alpha).toString(16));
globalSettingsChanged = (key, value) => {
@ -1590,6 +1587,8 @@ class Editor extends React.Component {
<EditorKeyHooks
moveComponents={this.moveComponents}
cloneComponents={this.cloneComponents}
copyComponents={this.copyComponents}
handleEditorEscapeKeyPress={this.handleEditorEscapeKeyPress}
removeMultipleComponents={this.removeComponents}
/>
@ -1600,7 +1599,6 @@ class Editor extends React.Component {
!isEmpty(appDefinition.components) &&
!isEmpty(appDefinition.components[selectedComponents[0].id]) ? (
<Inspector
cloneComponent={this.cloneComponent}
moveComponents={this.moveComponents}
componentDefinitionChanged={this.componentDefinitionChanged}
dataQueries={dataQueries}

View file

@ -1,7 +1,13 @@
import React from 'react';
import useKeyHooks from '@/_hooks/useKeyHooks';
export const EditorKeyHooks = ({ moveComponents, handleEditorEscapeKeyPress, removeMultipleComponents }) => {
export const EditorKeyHooks = ({
moveComponents,
copyComponents,
cloneComponents,
handleEditorEscapeKeyPress,
removeMultipleComponents,
}) => {
const handleHotKeysCallback = (key) => {
switch (key) {
case 'Escape':
@ -10,12 +16,18 @@ export const EditorKeyHooks = ({ moveComponents, handleEditorEscapeKeyPress, rem
case 'Backspace':
removeMultipleComponents();
break;
case 'KeyD':
cloneComponents();
break;
case 'KeyC':
copyComponents();
break;
default:
moveComponents(key);
}
};
useKeyHooks(['up, down, left, right', 'esc', 'backspace'], handleHotKeysCallback);
useKeyHooks(['up, down, left, right', 'esc', 'backspace', 'cmd+d, ctrl+d, cmd+c, ctrl+c'], handleHotKeysCallback);
return <></>;
};

View file

@ -1,7 +1,6 @@
import React, { useState, useRef, useLayoutEffect } from 'react';
import Tabs from 'react-bootstrap/Tabs';
import Tab from 'react-bootstrap/Tab';
import { v4 as uuidv4 } from 'uuid';
import { componentTypes } from '../WidgetManager/components';
import { Table } from './Components/Table';
import { Chart } from './Components/Chart';
@ -17,7 +16,6 @@ import useFocus from '@/_hooks/use-focus';
import Accordion from '@/_ui/Accordion';
export const Inspector = ({
cloneComponent,
selectedComponentId,
componentDefinitionChanged,
dataQueries,
@ -45,36 +43,6 @@ export const Inspector = ({
useHotkeys('backspace', () => setWidgetDeleteConfirmation(true));
useHotkeys('escape', () => switchSidebarTab(2));
useHotkeys('cmd+d, ctrl+d', (e) => {
e.preventDefault();
let clonedComponent = JSON.parse(JSON.stringify(component));
clonedComponent.id = uuidv4();
cloneComponent(clonedComponent);
let childComponents = [];
if ((component.component.component === 'Tabs') | (component.component.component === 'Calendar')) {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent?.startsWith(component.id));
} else {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
}
childComponents.forEach((componentId) => {
let childComponent = JSON.parse(JSON.stringify(allComponents[componentId]));
childComponent.id = uuidv4();
if ((component.component.component === 'Tabs') | (component.component.component === 'Calendar')) {
const childTabId = childComponent.parent.split('-').at(-1);
childComponent.parent = `${clonedComponent.id}-${childTabId}`;
} else {
childComponent.parent = clonedComponent.id;
}
cloneComponent(childComponent);
});
toast.success(`${component.component.name} cloned succesfully`);
switchSidebarTab(2);
});
const componentMeta = componentTypes.find((comp) => component.component.component === comp.component);
useLayoutEffect(() => {

View file

@ -5,6 +5,7 @@ import {
resolveReferences,
executeMultilineJS,
serializeNestedObjectToQueryParams,
computeComponentName,
} from '@/_helpers/utils';
import { dataqueryService } from '@/_services';
import _ from 'lodash';
@ -14,6 +15,7 @@ import { componentTypes } from '@/Editor/WidgetManager/components';
import generateCSV from '@/_lib/generate-csv';
import generateFile from '@/_lib/generate-file';
import { allSvgs } from '@tooljet/plugins/client';
import { v4 as uuidv4 } from 'uuid';
export function setStateAsync(_ref, state) {
return new Promise((resolve) => {
@ -872,3 +874,146 @@ export const getSvgIcon = (key, height = 50, width = 50) => {
return <Icon style={{ height, width }} />;
};
const updateNewComponents = (appDefinition, newComponents, updateAppDefinition) => {
const newAppDefinition = JSON.parse(JSON.stringify(appDefinition));
newComponents.forEach((newComponent) => {
newComponent.component.name = computeComponentName(newComponent.component.component, newAppDefinition.components);
newAppDefinition.components[newComponent.id] = newComponent;
});
updateAppDefinition(newAppDefinition);
};
export const cloneComponents = (_ref, updateAppDefinition, isCloning = true) => {
const { selectedComponents, appDefinition } = _ref.state;
const { components: allComponents } = appDefinition;
let newComponents = [];
for (let selectedComponent of selectedComponents) {
const component = {
id: selectedComponent.id,
component: allComponents[selectedComponent.id]?.component,
layouts: allComponents[selectedComponent.id]?.layouts,
parent: allComponents[selectedComponent.id]?.parent,
};
let clonedComponent = JSON.parse(JSON.stringify(component));
clonedComponent.parent = undefined;
clonedComponent.children = [];
clonedComponent.children = [...getChildComponents(allComponents, component, clonedComponent)];
newComponents = [...newComponents, clonedComponent];
}
if (isCloning) {
addComponents(appDefinition, updateAppDefinition, undefined, newComponents, true);
toast.success('Component cloned succesfully');
} else {
navigator.clipboard.writeText(JSON.stringify(newComponents));
toast.success('Component copied succesfully');
}
_ref.setState({ currentSidebarTab: 2 });
};
const getChildComponents = (allComponents, component, parentComponent) => {
let childComponents = [],
selectedChildComponents = [];
if (component.component.component === 'Tabs' || component.component.component === 'Calendar') {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent?.startsWith(component.id));
} else {
childComponents = Object.keys(allComponents).filter((key) => allComponents[key].parent === component.id);
}
childComponents.forEach((componentId) => {
let childComponent = JSON.parse(JSON.stringify(allComponents[componentId]));
childComponent.id = componentId;
const newComponent = JSON.parse(
JSON.stringify({
id: componentId,
component: allComponents[componentId]?.component,
layouts: allComponents[componentId]?.layouts,
parent: allComponents[componentId]?.parent,
})
);
if ((component.component.component === 'Tabs') | (component.component.component === 'Calendar')) {
const childTabId = childComponent.parent.split('-').at(-1);
childComponent.parent = `${parentComponent.id}-${childTabId}`;
} else {
childComponent.parent = parentComponent.id;
}
parentComponent.children = [...(parentComponent.children || []), childComponent];
childComponent.children = [...getChildComponents(allComponents, newComponent, childComponent)];
selectedChildComponents.push(childComponent);
});
return selectedChildComponents;
};
const updateComponentLayout = (components, parentId) => {
let prevComponent;
components.forEach((component, index) => {
Object.keys(component.layouts).map((layout) => {
if (parentId !== undefined) {
if (index > 0) {
component.layouts[layout].top = prevComponent.layouts[layout].top + prevComponent.layouts[layout].height;
component.layouts[layout].left = 0;
} else {
component.layouts[layout].top = 0;
component.layouts[layout].left = 0;
}
prevComponent = component;
} else {
component.layouts[layout].top = component.layouts[layout].top + component.layouts[layout].height;
}
});
});
};
export const addComponents = (
appDefinition,
appDefinitionChanged,
parentId = undefined,
pastedComponent = [],
isCloning = false
) => {
const finalComponents = [];
let parentComponent = undefined;
if (parentId) {
const id = Object.keys(appDefinition.components).filter((key) => parentId.startsWith(key));
parentComponent = JSON.parse(JSON.stringify(appDefinition.components[id[0]]));
parentComponent.id = parentId;
}
!isCloning && updateComponentLayout(pastedComponent, parentId);
const buildComponents = (components, parentComponent = undefined, skipTabCalendarCheck = false) => {
if (Array.isArray(components) && components.length > 0) {
components.forEach((component) => {
const newComponent = {
id: uuidv4(),
component: component?.component,
layouts: component?.layouts,
};
if (parentComponent) {
if (
!skipTabCalendarCheck &&
(parentComponent.component.component === 'Tabs' || parentComponent.component.component === 'Calendar')
) {
const childTabId = component.parent.split('-').at(-1);
newComponent.parent = `${parentComponent.id}-${childTabId}`;
} else {
newComponent.parent = parentComponent.id;
}
}
finalComponents.push(newComponent);
if (component.children.length > 0) {
buildComponents(component.children, newComponent);
}
});
}
};
buildComponents(pastedComponent, parentComponent, true);
updateNewComponents(appDefinition, finalComponents, appDefinitionChanged);
!isCloning && toast.success('Component pasted succesfully');
};