Canvas: Extend root context menu (#58097)

This commit is contained in:
Adela Almasan 2022-11-03 12:30:12 -05:00 committed by GitHub
parent b799be3052
commit 9c7b6b1ce8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 239 additions and 84 deletions

View File

@ -7142,11 +7142,10 @@ exports[`better eslint`] = {
"public/app/plugins/panel/canvas/InlineEditBody.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/panel/canvas/editor/APIEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@ -7159,8 +7158,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -7176,7 +7174,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/panel/canvas/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/panel/dashlist/module.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]

View File

@ -1,5 +1,5 @@
import { css, cx } from '@emotion/css';
import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle } from 'react';
import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle, CSSProperties } from 'react';
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
@ -40,6 +40,8 @@ export interface MenuItemProps<T = any> {
tabIndex?: number;
/** List of menu items for the subMenu */
childItems?: Array<ReactElement<MenuItemProps>>;
/** Custom style for SubMenu */
customSubMenuContainerStyles?: CSSProperties;
}
/** @internal */
@ -59,6 +61,7 @@ export const MenuItem = React.memo(
childItems,
role = 'menuitem',
tabIndex = -1,
customSubMenuContainerStyles,
} = props;
const styles = useStyles2(getStyles);
const [isActive, setIsActive] = useState(active);
@ -138,6 +141,7 @@ export const MenuItem = React.memo(
openedWithArrow={openedWithArrow}
setOpenedWithArrow={setOpenedWithArrow}
close={closeSubMenu}
customStyle={customSubMenuContainerStyles}
/>
)}
</ItemElement>

View File

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import React, { ReactElement, useRef } from 'react';
import React, { CSSProperties, ReactElement, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -23,11 +23,13 @@ export interface SubMenuProps {
setOpenedWithArrow: (openedWithArrow: boolean) => void;
/** Closes the subMenu */
close: () => void;
/** Custom style */
customStyle?: CSSProperties;
}
/** @internal */
export const SubMenu: React.FC<SubMenuProps> = React.memo(
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close }) => {
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close, customStyle }) => {
const styles = useStyles2(getStyles);
const localRef = useRef<HTMLDivElement>(null);
const [handleKeys] = useMenuFocus({
@ -48,6 +50,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
ref={localRef}
className={styles.subMenu(localRef.current)}
aria-label={selectors.components.Menu.SubMenu.container}
style={customStyle}
>
<div tabIndex={-1} className={styles.itemsWrapper} role="menu" onKeyDown={handleKeys}>
{items}

View File

@ -26,8 +26,9 @@ import {
getTextDimensionFromData,
} from 'app/features/dimensions/utils';
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
import { LayerActionID } from 'app/plugins/panel/canvas/types';
import { AnchorPoint, LayerActionID } from 'app/plugins/panel/canvas/types';
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
@ -62,10 +63,12 @@ export class Scene {
shouldShowAdvancedTypes?: boolean;
skipNextSelectionBroadcast = false;
ignoreDataUpdate = false;
panel: CanvasPanel;
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
inlineEditingCallback?: () => void;
setBackgroundCallback?: (anchorPoint: AnchorPoint) => void;
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
subscription: Subscription;
@ -74,7 +77,8 @@ export class Scene {
cfg: CanvasFrameOptions,
enableEditing: boolean,
showAdvancedTypes: boolean,
public onSave: (cfg: CanvasFrameOptions) => void
public onSave: (cfg: CanvasFrameOptions) => void,
panel: CanvasPanel
) {
this.root = this.load(cfg, enableEditing, showAdvancedTypes);
@ -84,6 +88,8 @@ export class Scene {
}
this.moveable.draggable = !open;
});
this.panel = panel;
}
getNextElementName = (isFrame = false) => {
@ -566,7 +572,7 @@ export class Scene {
{this.root.render()}
{canShowContextMenu && (
<Portal>
<CanvasContextMenu scene={this} />
<CanvasContextMenu scene={this} panel={this.panel} />
</Portal>
)}
</div>

View File

@ -1,32 +1,30 @@
import { css } from '@emotion/css';
import React, { useCallback, useEffect, useState } from 'react';
import { useObservable } from 'react-use';
import { first } from 'rxjs/operators';
import { ContextMenu, MenuItem } from '@grafana/ui';
import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui';
import { Scene } from 'app/features/canvas/runtime/scene';
import { activePanelSubject } from './CanvasPanel';
import { LayerActionID } from './types';
import { FrameState } from '../../../features/canvas/runtime/frame';
import { CanvasPanel } from './CanvasPanel';
import { AnchorPoint, LayerActionID } from './types';
import { getElementTypes, onAddItem } from './utils';
type Props = {
scene: Scene;
panel: CanvasPanel;
};
type AnchorPoint = {
x: number;
y: number;
};
export const CanvasContextMenu = ({ scene }: Props) => {
const activePanel = useObservable(activePanelSubject);
const inlineEditorOpen = activePanel?.panel.state.openInlineEdit;
export const CanvasContextMenu = ({ scene, panel }: Props) => {
const inlineEditorOpen = panel.state.openInlineEdit;
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
const styles = getStyles();
const selectedElements = scene.selecto?.getSelectedTargets();
const rootLayer: FrameState | undefined = panel.context?.instanceState?.layer;
const handleContextMenu = useCallback(
(event: Event) => {
@ -35,6 +33,8 @@ export const CanvasContextMenu = ({ scene }: Props) => {
}
event.preventDefault();
panel.setActivePanel();
const shouldSelectElement = event.currentTarget !== scene.div;
if (shouldSelectElement) {
scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
@ -42,7 +42,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
setAnchorPoint({ x: event.pageX, y: event.pageY });
setIsMenuVisible(true);
},
[scene]
[scene, panel]
);
useEffect(() => {
@ -70,7 +70,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
onClick={() => {
if (scene.inlineEditingCallback) {
if (inlineEditorOpen) {
activePanel.panel.closeInlineEdit();
panel.closeInlineEdit();
} else {
scene.inlineEditingCallback();
}
@ -99,6 +99,47 @@ export const CanvasContextMenu = ({ scene }: Props) => {
return null;
};
const typeOptions = getElementTypes(scene.shouldShowAdvancedTypes).options;
const getTypeOptionsSubmenu = () => {
const submenuItems: Array<
React.ReactElement<MenuItemProps<unknown>, string | React.JSXElementConstructor<unknown>>
> = [];
typeOptions.map((option) => {
submenuItems.push(
<MenuItem
key={option.value}
label={option.label ?? 'Canvas item'}
onClick={() => onAddItem(option, rootLayer)}
/>
);
});
return submenuItems;
};
const addItemMenuItem = !scene.isPanelEditing && (
<MenuItem
label="Add item"
className={styles.menuItem}
childItems={getTypeOptionsSubmenu()}
customSubMenuContainerStyles={{ maxHeight: '150px', overflowY: 'auto' }}
/>
);
const setBackgroundMenuItem = !scene.isPanelEditing && (
<MenuItem
label={'Set background'}
onClick={() => {
if (scene.setBackgroundCallback) {
scene.setBackgroundCallback(anchorPoint);
}
closeContextMenu();
}}
className={styles.menuItem}
/>
);
if (selectedElements && selectedElements.length >= 1) {
return (
<>
@ -139,7 +180,13 @@ export const CanvasContextMenu = ({ scene }: Props) => {
</>
);
} else {
return openCloseEditorMenuItem;
return (
<>
{openCloseEditorMenuItem}
{setBackgroundMenuItem}
{addItemMenuItem}
</>
);
}
};
@ -189,7 +236,6 @@ export const CanvasContextMenu = ({ scene }: Props) => {
const getStyles = () => ({
menuItem: css`
max-width: 60ch;
overflow: hidden;
max-width: 200px;
`,
});

View File

@ -10,13 +10,17 @@ import { Scene } from 'app/features/canvas/runtime/scene';
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
import { InlineEdit } from './InlineEdit';
import { SetBackground } from './SetBackground';
import { PanelOptions } from './models.gen';
import { AnchorPoint } from './types';
interface Props extends PanelProps<PanelOptions> {}
interface State {
refresh: number;
openInlineEdit: boolean;
openSetBackground: boolean;
contextMenuAnchorPoint: AnchorPoint;
}
export interface InstanceState {
@ -31,6 +35,7 @@ export interface SelectionAction {
let canvasInstances: CanvasPanel[] = [];
let activeCanvasPanel: CanvasPanel | undefined = undefined;
let isInlineEditOpen = false;
let isSetBackgroundOpen = false;
export const activePanelSubject = new ReplaySubject<SelectionAction>(1);
@ -49,6 +54,8 @@ export class CanvasPanel extends Component<Props, State> {
this.state = {
refresh: 0,
openInlineEdit: false,
openSetBackground: false,
contextMenuAnchorPoint: { x: 0, y: 0 },
};
// Only the initial options are ever used.
@ -57,11 +64,13 @@ export class CanvasPanel extends Component<Props, State> {
this.props.options.root,
this.props.options.inlineEditing,
this.props.options.showAdvancedTypes,
this.onUpdateScene
this.onUpdateScene,
this
);
this.scene.updateSize(props.width, props.height);
this.scene.updateData(props.data);
this.scene.inlineEditingCallback = this.openInlineEdit;
this.scene.setBackgroundCallback = this.openSetBackground;
this.subs.add(
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
@ -126,6 +135,7 @@ export class CanvasPanel extends Component<Props, State> {
this.scene.subscription.unsubscribe();
this.subs.unsubscribe();
isInlineEditOpen = false;
isSetBackgroundOpen = false;
canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id);
}
@ -164,6 +174,10 @@ export class CanvasPanel extends Component<Props, State> {
changed = true;
}
if (this.state.openSetBackground !== nextState.openSetBackground) {
changed = true;
}
// After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed
const shouldUpdateSceneAndPanel = this.needsReload && this.props.options !== nextProps.options;
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
@ -197,18 +211,47 @@ export class CanvasPanel extends Component<Props, State> {
isInlineEditOpen = true;
};
openSetBackground = (anchorPoint: AnchorPoint) => {
if (isSetBackgroundOpen) {
this.forceUpdate();
this.setActivePanel();
return;
}
this.setActivePanel();
this.setState({ openSetBackground: true });
this.setState({ contextMenuAnchorPoint: anchorPoint });
isSetBackgroundOpen = true;
};
closeInlineEdit = () => {
this.setState({ openInlineEdit: false });
isInlineEditOpen = false;
};
closeSetBackground = () => {
this.setState({ openSetBackground: false });
isSetBackgroundOpen = false;
};
setActivePanel = () => {
activeCanvasPanel = this;
activePanelSubject.next({ panel: this });
};
renderInlineEdit = () => {
return <InlineEdit onClose={() => this.closeInlineEdit()} id={this.props.id} scene={activeCanvasPanel!.scene} />;
return <InlineEdit onClose={() => this.closeInlineEdit()} id={this.props.id} scene={this.scene} />;
};
renderSetBackground = () => {
return (
<SetBackground
onClose={() => this.closeSetBackground()}
scene={this.scene}
anchorPoint={this.state.contextMenuAnchorPoint}
/>
);
};
render() {
@ -216,6 +259,7 @@ export class CanvasPanel extends Component<Props, State> {
<>
{this.scene.render()}
{this.state.openInlineEdit && this.renderInlineEdit()}
{this.state.openSetBackground && this.renderSetBackground()}
</>
);
}

View File

@ -27,7 +27,7 @@ export function InlineEdit({ onClose, id, scene }: Props) {
const styles = useStyles2(getStyles);
const inlineEditKey = 'inlineEditPanel' + id.toString();
const defaultMeasurements = { width: 350, height: 400 };
const defaultMeasurements = { width: 400, height: 400 };
const widthOffset = root?.width ?? defaultMeasurements.width + OFFSET_X * 2;
const defaultX = root?.x ?? 0 + widthOffset - defaultMeasurements.width - OFFSET_X;
const defaultY = root?.y ?? 0 + OFFSET_Y;

View File

@ -3,34 +3,24 @@ import { get as lodashGet } from 'lodash';
import React, { useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import {
DataFrame,
GrafanaTheme2,
PanelOptionsEditorBuilder,
SelectableValue,
StandardEditorContext,
} from '@grafana/data';
import { DataFrame, GrafanaTheme2, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
import { useStyles2 } from '@grafana/ui/src';
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { ElementState } from 'app/features/canvas/runtime/element';
import { FrameState } from 'app/features/canvas/runtime/frame';
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
import { CanvasElementOptions, canvasElementRegistry } from '../../../features/canvas';
import { activePanelSubject, InstanceState } from './CanvasPanel';
import { TabsEditor } from './editor/TabsEditor';
import { getElementEditor } from './editor/elementEditor';
import { getLayerEditor } from './editor/layerEditor';
import { addStandardCanvasEditorOptions } from './module';
import { InlineEditTabs } from './types';
import { getElementTypes } from './utils';
import { getElementTypes, onAddItem } from './utils';
export function InlineEditBody() {
const activePanel = useObservable(activePanelSubject);
@ -90,23 +80,6 @@ export function InlineEditBody() {
const typeOptions = getElementTypes(instanceState?.scene.shouldShowAdvancedTypes).options;
const rootLayer: FrameState | undefined = instanceState?.layer;
const onAddItem = (sel: SelectableValue<string>) => {
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
newElementOptions.type = newItem.id;
if (newItem.defaultSize) {
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
}
if (rootLayer) {
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
newElement.updateData(rootLayer.scene.context);
rootLayer.elements.push(newElement);
rootLayer.scene.save();
rootLayer.reinitializeMoveable();
}
};
const noElementSelected =
instanceState && activeTab === InlineEditTabs.SelectedElement && instanceState.selected.length === 0;
@ -114,7 +87,7 @@ export function InlineEditBody() {
<>
<div style={topLevelItemsContainerStyle}>{pane.items.map((item) => item.render())}</div>
<div style={topLevelItemsContainerStyle}>
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
<AddLayerButton onChange={(sel) => onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
</div>
<div style={topLevelItemsContainerStyle}>
<TabsEditor onTabChange={onTabChange} />

View File

@ -0,0 +1,67 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Portal, useTheme2 } from '@grafana/ui';
import { Scene } from 'app/features/canvas/runtime/scene';
import { MediaType, ResourceDimensionMode, ResourceFolderName } from '../../../features/dimensions';
import { ResourcePickerPopover } from '../../../features/dimensions/editors/ResourcePickerPopover';
import { AnchorPoint } from './types';
type Props = {
onClose: () => void;
scene: Scene;
anchorPoint: AnchorPoint;
};
export function SetBackground({ onClose, scene, anchorPoint }: Props) {
const defaultValue = scene.root.options.background?.image?.fixed ?? '';
const [bgImage, setBgImage] = useState(defaultValue);
const theme = useTheme2();
const styles = getStyles(theme, anchorPoint);
const onChange = (value: string | undefined) => {
if (value) {
setBgImage(value);
if (scene.root) {
scene.root.options.background = {
...scene.root.options.background,
image: { mode: ResourceDimensionMode.Fixed, fixed: value },
};
scene.revId++;
scene.save();
scene.root.reinitializeMoveable();
}
// Force a re-render (update scene data after config update)
if (scene) {
scene.updateData(scene.data!);
}
}
onClose();
};
return (
<Portal className={styles.portalWrapper}>
<ResourcePickerPopover
onChange={onChange}
value={bgImage}
mediaType={MediaType.Image}
folderName={ResourceFolderName.IOT}
/>
</Portal>
);
}
const getStyles = (theme: GrafanaTheme2, anchorPoint: AnchorPoint) => ({
portalWrapper: css`
width: 315px;
height: 445px;
transform: translate(${anchorPoint.x}px, ${anchorPoint.y - 200}px);
`,
});

View File

@ -3,19 +3,17 @@ import { Global } from '@emotion/react';
import Tree, { TreeNodeProps } from 'rc-tree';
import React, { Key, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Button, HorizontalGroup, Icon, useStyles2, useTheme2 } from '@grafana/ui';
import { ElementState } from 'app/features/canvas/runtime/element';
import { AddLayerButton } from '../../../../core/components/Layers/AddLayerButton';
import { CanvasElementOptions, canvasElementRegistry } from '../../../../features/canvas';
import { notFoundItem } from '../../../../features/canvas/elements/notFound';
import { getGlobalStyles } from '../globalStyles';
import { PanelOptions } from '../models.gen';
import { getTreeData, onNodeDrop, TreeElement } from '../tree';
import { DragNode, DropNode } from '../types';
import { doSelect, getElementTypes } from '../utils';
import { doSelect, getElementTypes, onAddItem } from '../utils';
import { TreeNodeTitle } from './TreeNodeTitle';
import { TreeViewEditorProps } from './elementEditor';
@ -109,21 +107,6 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
allowSelection = allow;
};
const onAddItem = (sel: SelectableValue<string>) => {
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
newElementOptions.type = newItem.id;
if (newItem.defaultSize) {
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
}
const newElement = new ElementState(newItem, newElementOptions, layer);
newElement.updateData(layer.scene.context);
layer.elements.push(newElement);
layer.scene.save();
layer.reinitializeMoveable();
};
const onClearSelection = () => {
layer.scene.clearCurrentSelection();
};
@ -166,7 +149,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
<HorizontalGroup justify="space-between">
<div className={styles.addLayerButton}>
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
<AddLayerButton onChange={(sel) => onAddItem(sel, layer)} options={typeOptions} label={'Add item'} />
</div>
{selection.length > 0 && (
<Button size="sm" variant="secondary" onClick={onClearSelection}>

View File

@ -20,3 +20,8 @@ export enum InlineEditTabs {
ElementManagement = 'element-management',
SelectedElement = 'selected-element',
}
export type AnchorPoint = {
x: number;
y: number;
};

View File

@ -2,7 +2,14 @@ import { AppEvents, PluginState, SelectableValue } from '@grafana/data';
import { hasAlphaPanels } from 'app/core/config';
import appEvents from '../../../core/app_events';
import { advancedElementItems, CanvasElementItem, defaultElementItems } from '../../../features/canvas';
import {
advancedElementItems,
CanvasElementItem,
CanvasElementOptions,
canvasElementRegistry,
defaultElementItems,
} from '../../../features/canvas';
import { notFoundItem } from '../../../features/canvas/elements/notFound';
import { ElementState } from '../../../features/canvas/runtime/element';
import { FrameState } from '../../../features/canvas/runtime/frame';
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
@ -69,3 +76,21 @@ export function getElementTypesOptions(
return selectables;
}
export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState | undefined) {
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
newElementOptions.type = newItem.id;
if (newItem.defaultSize) {
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
}
if (rootLayer) {
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
newElement.updateData(rootLayer.scene.context);
rootLayer.elements.push(newElement);
rootLayer.scene.save();
rootLayer.reinitializeMoveable();
}
}