mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-23 17:08:34 +00:00
[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:
parent
e3917a13f1
commit
ae5994c6c8
5 changed files with 207 additions and 42 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 <></>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue