chore: code changes for widget position observer and widget name on canvas (#27367)

## Description
The PR contains non integrated code changes for below new features, The
changes are not integrated to work but only contains the ground work
code changes that can be added to css based layout/ Anvil once that is
available in Release.

- **Widget Position observer-** Since we are moving to css based layout,
the positions of widgets will be unknown. To solve the issue we have
introduced the above feature that stores/updates position of widgets on
Redux state whenever a widget position updates. without manually
triggering any action
- **Widget Name on Canvas-** For the New Layout the existing widget name
is inconsistent as it would cut off or visually not visible. to solve
that the widget name will now be drawn on html canvas than it being a
dom node component

#### PR fixes following issue(s)
Fixes #26945 
Fixes #26948 


#### Type of change
- Chore (housekeeping or task changes that don't impact user perception)


## Testing
#### How Has This Been Tested?
- [ ] Manual
- [ ] JUnit
- [ ] Jest

#### Test Plan
> Add Testsmith test cases links that relate to this PR

#### Issues raised during DP testing
> Link issues raised during DP testing for better visiblity and tracking
(copy link from comments dropped on this PR)
>
>
>
## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed

---------

Co-authored-by: Ashok Kumar M <35134347+marks0351@users.noreply.github.com>
Co-authored-by: Abhinav Jha <abhinav@appsmith.com>
This commit is contained in:
rahulramesha 2023-10-06 15:37:43 +05:30 committed by GitHub
parent 09657f4cea
commit 8a35e05923
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1525 additions and 80 deletions

View File

@ -123,6 +123,7 @@
"js-sha256": "^0.9.0",
"jshint": "^2.13.4",
"klona": "^2.0.5",
"konva": "8.0.1",
"libphonenumber-js": "^1.9.44",
"linkedom": "^0.14.20",
"localforage": "^1.7.3",
@ -166,6 +167,7 @@
"react-hook-form": "^7.28.0",
"react-instantsearch-dom": "^6.4.0",
"react-json-view": "^1.21.3",
"react-konva": "17.0.2-6",
"react-masonry-css": "^1.0.16",
"react-media-recorder": "^1.6.1",
"react-modal": "^3.15.1",

View File

@ -11,6 +11,7 @@ import pageListReducer from "reducers/entityReducers/pageListReducer";
import pluginsReducer from "reducers/entityReducers/pluginsReducer";
import autoHeightLayoutTreeReducer from "reducers/entityReducers/autoHeightReducers/autoHeightLayoutTreeReducer";
import canvasLevelsReducer from "reducers/entityReducers/autoHeightReducers/canvasLevelsReducer";
import widgetPositionsReducer from "layoutSystems/anvil/integrations/reducers/widgetPositionsReducer";
export const entityReducerObject = {
canvasWidgets: canvasWidgetsReducer,
@ -26,4 +27,5 @@ export const entityReducerObject = {
jsActions: jsActionsReducer,
autoHeightLayoutTree: autoHeightLayoutTreeReducer,
canvasLevels: canvasLevelsReducer,
widgetPositions: widgetPositionsReducer,
};

View File

@ -76,6 +76,10 @@ import type { MetaWidgetsReduxState } from "reducers/entityReducers/metaWidgetsR
import type { layoutConversionReduxState } from "reducers/uiReducers/layoutConversionReducer";
import type { OneClickBindingState } from "reducers/uiReducers/oneClickBindingReducer";
/* Reducers which are integrated into the core system when registering a pluggable module
or done so by a module that is designed to be eventually pluggable */
import type { WidgetPositionsReduxState } from "layoutSystems/anvil/integrations/reducers/widgetPositionsReducer";
export const reducerObject = {
entities: entityReducer,
ui: uiReducer,
@ -152,6 +156,7 @@ export interface AppState {
jsActions: JSCollectionDataState;
autoHeightLayoutTree: AutoHeightLayoutTreeReduxState;
canvasLevels: CanvasLevelsReduxState;
widgetPositions: WidgetPositionsReduxState;
};
evaluations: {
tree: EvaluatedTreeState;

View File

@ -52,6 +52,9 @@ import oneClickBindingSaga from "sagas/OneClickBindingSaga";
import entityNavigationSaga from "sagas/NavigationSagas";
import communityTemplateSagas from "sagas/CommunityTemplatesSagas";
/* Sagas that are registered by a module that is designed to be independent of the core platform */
import WidgetPositionSaga from "layoutSystems/anvil/integrations/sagas/WidgetPositionsSaga";
export const sagas = [
initSagas,
pageSagas,
@ -105,5 +108,6 @@ export const sagas = [
snapshotSagas,
oneClickBindingSaga,
entityNavigationSaga,
WidgetPositionSaga,
communityTemplateSagas,
];

View File

@ -77,6 +77,21 @@ export const getDatasourceStructureById = (
return state.entities.datasources.structure[id];
};
/**
* Selector to indicate if the widget name should be shown/drawn on canvas
*/
export const getShouldShowWidgetName = createSelector(
(state: AppState) => state.ui.widgetDragResize.isResizing,
(state: AppState) => state.ui.widgetDragResize.isDragging,
(state: AppState) => state.ui.editor.isPreviewMode,
(state: AppState) => state.ui.widgetDragResize.isAutoCanvasResizing,
(isResizing, isDragging, isPreviewMode, isAutoCanvasResizing) => {
return (
!isResizing && !isDragging && !isPreviewMode && !isAutoCanvasResizing
);
},
);
export const getDatasourceTableColumns =
(datasourceId: string, tableName: string) => (state: AppState) => {
const structure = getDatasourceStructureById(state, datasourceId);

View File

@ -12,6 +12,8 @@ export function getBaseWidgetClassName(id?: string) {
export const CANVAS_VIEWPORT = "canvas-viewport";
export const CANVAS_ART_BOARD = "art-board";
export const POSITIONED_WIDGET = "positioned-widget";
export const WIDGET_COMPONENT_BOUNDARY_CLASS = "widget-component-boundary";

View File

@ -0,0 +1,9 @@
export type AnvilReduxAction<T> = {
type: AnvilReduxActionTypes;
payload: T;
};
export enum AnvilReduxActionTypes {
READ_WIDGET_POSITIONS = "READ_WIDGET_POSITIONS",
UPDATE_WIDGET_POSITIONS = "UPDATE_WIDGET_POSITIONS",
}

View File

@ -0,0 +1,14 @@
import { AnvilReduxActionTypes } from "./actionTypes";
export const readWidgetPositions = (
widgetsProcessQueue: {
[widgetDOMId: string]: boolean;
},
layersProcessQueue: { [canvasId: string]: number },
layoutsProcessQueue: { [layoutId: string]: boolean },
) => {
return {
type: AnvilReduxActionTypes.READ_WIDGET_POSITIONS,
payload: { widgetsProcessQueue, layersProcessQueue, layoutsProcessQueue },
};
};

View File

@ -0,0 +1,33 @@
import { AnvilReduxActionTypes } from "layoutSystems/anvil/integrations/actions/actionTypes";
import type { AnvilReduxAction } from "layoutSystems/anvil/integrations/actions/actionTypes";
import { createImmerReducer } from "utils/ReducerUtils";
import type { WidgetPositions } from "layoutSystems/common/types";
const initialState: WidgetPositions = {};
export type WidgetPositionsReduxState = typeof initialState;
/**
* Reducer used for storing Position of all widgets in the current layout
* This reducer is useful for all on canvas UI (Ex: Dropzone Highlights, Widget Name component position, etc)
*/
const widgetPositionsReducer = createImmerReducer(initialState, {
[AnvilReduxActionTypes.UPDATE_WIDGET_POSITIONS]: (
WidgetPositionState: WidgetPositions,
action: AnvilReduxAction<WidgetPositions>,
) => {
const widgetPositions = action.payload;
const widgetIds = Object.keys(widgetPositions);
for (const widgetId of widgetIds) {
const newPosition = widgetPositions[widgetId];
WidgetPositionState[widgetId].height = newPosition.height;
WidgetPositionState[widgetId].width = newPosition.width;
WidgetPositionState[widgetId].left = newPosition.left;
WidgetPositionState[widgetId].top = newPosition.top;
}
},
});
export default widgetPositionsReducer;

View File

@ -0,0 +1,78 @@
import { AnvilReduxActionTypes } from "layoutSystems/anvil/integrations/actions/actionTypes";
import type { ReduxAction } from "@appsmith/constants/ReduxActionConstants";
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import type { WidgetPositions } from "layoutSystems/common/types";
import { all, put, select, takeEvery } from "redux-saga/effects";
import { getAnvilWidgetId } from "layoutSystems/common/utils/WidgetPositionsObserver/utils";
import { getAffectedWidgetsFromLayers } from "layoutSystems/anvil/integrations/utils";
import { getCanvasWidgets } from "@appsmith/selectors/entitiesSelector";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
/**
* This saga is used to read and update widget position from the the list of widgets,
* layers and layouts received from widget positions observer
* Widgets : When triggered by the ResizeObserver wrapping the widget
* Layers: When triggered by the ResizeObserver wrapping the layer (Flex layers)
* Layouts: When triggered by the ResizeObserver wrapping the layout (Layout Components)
* @param action All the widgets, layers and layouts that have changed and currently in queue to be processed
*/
function* readAndUpdateWidgetPositions(
action: ReduxAction<{
widgetsProcessQueue: {
[widgetId: string]: boolean;
};
layersProcessQueue: { [canvasId: string]: number };
layoutsProcessQueue: { [key: string]: boolean };
}>,
) {
const widgets: CanvasWidgetsReduxState = yield select(getCanvasWidgets);
const { layersProcessQueue, widgetsProcessQueue } = action.payload;
//get additional widgets from affected layers
const affectedWidgetsFromLayers: {
[widgetDOMId: string]: boolean;
} = getAffectedWidgetsFromLayers(layersProcessQueue, widgets);
const widgetsToProcess = {
...widgetsProcessQueue,
...affectedWidgetsFromLayers,
};
const widgetDimensions: WidgetPositions = {};
const mainContainerDOMNode = document.getElementById(CANVAS_ART_BOARD);
const mainContainerDOMRect = mainContainerDOMNode?.getBoundingClientRect();
const { left = 0, top = 0 } = mainContainerDOMRect || {};
//for every affected widget get the bounding client Rect
// If they do, we don't have to update the positions here.
for (const widgetId of Object.keys(widgetsToProcess)) {
const element = document.getElementById(getAnvilWidgetId(widgetId));
if (element) {
const rect = element.getBoundingClientRect();
widgetDimensions[widgetId] = {
left: rect.left - left,
top: rect.top - top,
height: rect.height,
width: rect.width,
};
}
}
yield put({
type: AnvilReduxActionTypes.UPDATE_WIDGET_POSITIONS,
payload: widgetDimensions,
});
}
export default function* WidgetPositionSaga() {
yield all([
takeEvery(
AnvilReduxActionTypes.READ_WIDGET_POSITIONS,
readAndUpdateWidgetPositions,
),
]);
}

View File

@ -0,0 +1,151 @@
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import type { WidgetProps } from "widgets/BaseWidget";
import { getAffectedWidgetsFromLayers, getAllChildWidgets } from "./utils";
const widgets = {
"0": {
children: ["1", "3", "4"],
detachFromLayout: true,
flexLayers: [
{ children: [{ id: "1" }, { id: "3" }] },
{ children: [{ id: "4" }] },
],
},
"1": {
children: ["2"],
},
"2": {
children: ["5", "6", "7", "8"],
detachFromLayout: true,
flexLayers: [
{ children: [{ id: "5" }] },
{ children: [{ id: "6" }, { id: "7" }] },
{ children: [{ id: "8" }] },
],
},
"3": { children: [] },
"4": { children: [] },
"5": { children: [] },
"6": { children: [] },
"7": { children: [] },
"8": { children: [] },
} as unknown as CanvasWidgetsReduxState;
describe("should test getAffectedWidgetsFromLayers", () => {
const layerQueue1 = {
"0": 0,
};
const layerQueue2 = {
"0": 1,
"2": 1,
};
const layerQueue3 = {
"2": 0,
};
const layerQueue4 = {
"0": 1,
"2": 2,
};
const affectedWidgets1 = {
"1": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
};
const affectedWidgets2 = {
"4": true,
"6": true,
"7": true,
"8": true,
};
const affectedWidgets3 = {
"5": true,
"6": true,
"7": true,
"8": true,
};
const affectedWidgets4 = {
"4": true,
"8": true,
};
it("should return all the affected widgets derived from layer queue", () => {
expect(getAffectedWidgetsFromLayers(layerQueue1, widgets)).toEqual(
affectedWidgets1,
);
expect(getAffectedWidgetsFromLayers(layerQueue2, widgets)).toEqual(
affectedWidgets2,
);
expect(getAffectedWidgetsFromLayers(layerQueue3, widgets)).toEqual(
affectedWidgets3,
);
expect(getAffectedWidgetsFromLayers(layerQueue4, widgets)).toEqual(
affectedWidgets4,
);
});
});
describe("should test getAllChildWidgets", () => {
const widget1 = {
widgetId: "0",
children: ["1", "3", "4"],
} as unknown as WidgetProps;
const widget2 = {
widgetId: "1",
children: ["2"],
} as unknown as WidgetProps;
const widget3 = {
widgetId: "2",
children: ["5", "6", "7", "8"],
} as unknown as WidgetProps;
const widget4 = {
widgetId: "3",
children: [],
} as unknown as WidgetProps;
const childWidgets1 = {
"1": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
};
const childWidgets2 = {
"5": true,
"6": true,
"7": true,
"8": true,
};
const childWidgets3 = {
"5": true,
"6": true,
"7": true,
"8": true,
};
const childWidgets4 = {};
it("should return all the child widgets except canvas widgets", () => {
expect(getAllChildWidgets(widget1, widgets)).toEqual(childWidgets1);
expect(getAllChildWidgets(widget2, widgets)).toEqual(childWidgets2);
expect(getAllChildWidgets(widget3, widgets)).toEqual(childWidgets3);
expect(getAllChildWidgets(widget4, widgets)).toEqual(childWidgets4);
});
});

View File

@ -0,0 +1,90 @@
import type { CanvasWidgetsReduxState } from "reducers/entityReducers/canvasWidgetsReducer";
import type { WidgetProps } from "widgets/BaseWidget";
/**
* This method is used to determine all the affected widgets from all the layers that have changed
* @param layersProcessQueue Changed layer Ids, will have one layer id per canvas
* @param widgets all widget dsl Properties
* @returns list of all affected widgets
*/
export function getAffectedWidgetsFromLayers(
layersProcessQueue: {
[canvasId: string]: number;
},
widgets: CanvasWidgetsReduxState,
) {
let affectedWidgets: { [widgetDOMId: string]: boolean } = {};
//Even though it has many nested iterations it will go through all teh affected widgets only once
//Iterate through all the canvases and it's first layer that got affected
for (const [canvasId, layerIndex] of Object.entries(layersProcessQueue)) {
const flexLayers = widgets[canvasId]?.flexLayers || [];
//iterate through all the layers below the changed layer id inculuding the layer
for (let i = layerIndex; i < flexLayers.length; i++) {
const children = flexLayers[i]?.children || [];
//iterate through all the child widgets inside the layer
for (const child of children) {
const childWidget = widgets[child.id];
if (!childWidget) continue;
affectedWidgets[child.id] = true;
//if the widget has children get all the nested children
if (childWidget.children && childWidget.children.length > 0) {
affectedWidgets = {
...affectedWidgets,
...getAllChildWidgets(childWidget, widgets, layersProcessQueue),
};
}
}
}
}
return affectedWidgets;
}
/**
* This Method gets all the nested child widgets,
* within the given widgets ignoring the canvas type widgets
* @param widget Widget whose nested children have to be found
* @param widgets all widget dsl Properties
* @returns list of all the nested child widgets of widget
*/
export function getAllChildWidgets(
widget: WidgetProps,
widgets: CanvasWidgetsReduxState,
layersProcessQueue?: {
[canvasId: string]: number;
},
) {
let childWidgets: { [widgetDOMId: string]: boolean } = {};
const children = widget.children;
//iterate through children if widget
for (const childId of children) {
const childWidget = widgets[childId];
if (!childWidget) continue;
//if the child widget is not a canvas add it to the list
if (!childWidget.detachFromLayout) {
childWidgets[childId] = true;
} else if (layersProcessQueue) {
//If it is a canvas widget remove the widget from the layer queue to avoid processing it again
delete layersProcessQueue[childId];
}
//if the widget further has nested children call the getAllChildWidgets recursively.
if (childWidget.children && childWidget.children.length > 0) {
childWidgets = {
...childWidgets,
...getAllChildWidgets(childWidget, widgets, layersProcessQueue),
};
}
}
return childWidgets;
}

View File

@ -0,0 +1,50 @@
import { Colors } from "constants/Colors";
import type { CSSProperties } from "react";
export const WIDGET_NAME_CANVAS = "widget-name-canvas";
export const WIDGET_NAME_FONT_SIZE = 14;
export const WIDGET_NAME_LINE_HEIGHT = Math.floor(WIDGET_NAME_FONT_SIZE * 1.2);
export const WIDGET_NAME_VERTICAL_PADDING = 4;
export const WIDGET_NAME_HORIZONTAL_PADDING = 6;
export const WIDGET_NAME_ICON_PADDING = 16;
export const DEFAULT_WIDGET_NAME_CANVAS_HEIGHT = 600;
export const WIDGET_NAME_CANVAS_PADDING = 20;
export const WIDGET_NAME_HEIGHT = Math.floor(
WIDGET_NAME_LINE_HEIGHT + WIDGET_NAME_VERTICAL_PADDING * 1.5,
);
export const WIDGET_NAME_TEXT_COLOR = Colors.WHITE;
//Adding this here as Konva accepts this type of path for SVG
export const warningSVGPath =
"M 18 9 C 18 13.9706 13.9706 18 9 18 C 4.0294 18 0 13.9706 0 9 C 0 4.0294 4.0294 0 9 0 C 13.9706 0 18 4.0294 18 9 Z M 7.875 3.9375 V 10.125 H 10.125 V 3.9375 H 7.875 Z M 9 14.0625 C 9.6213 14.0625 10.125 13.5588 10.125 12.9375 C 10.125 12.3162 9.6213 11.8125 9 11.8125 C 8.3787 11.8125 7.875 12.3162 7.875 12.9375 C 7.875 13.5588 8.3787 14.0625 9 14.0625 Z";
//Indicates the state of widget name
export enum WidgetNameState {
SELECTED = "SELECTED",
ERROR = "ERROR",
FOCUSED = "FOCUSED",
}
//fill colors of widget name based on state
export const WIDGET_NAME_FILL_COLORS = {
[WidgetNameState.SELECTED]: Colors.JAFFA_DARK,
[WidgetNameState.FOCUSED]: Colors.WATUSI,
[WidgetNameState.ERROR]: Colors.DANGER_SOLID,
};
//CSS properties of the wrapper object of the html canvas
export const widgetNameWrapperStyle: CSSProperties = {
position: "absolute",
top: 0,
left: 0,
zIndex: 2,
pointerEvents: "none",
display: "flex",
flexDirection: "column",
alignItems: "center",
width: "100%",
height: "100%",
};

View File

@ -0,0 +1,33 @@
import type { WidgetPosition } from "layoutSystems/common/types";
import type { WidgetNameState } from "./WidgetNameConstants";
export type WIDGET_NAME_TYPE = "selected" | "focused";
//Contains the data of widget which are required to draw widget names on canvas
export type WidgetNameData = {
id: string;
position: WidgetPosition;
widgetName: string;
parentId: string;
nameState: WidgetNameState;
dragDisabled: boolean;
};
//Position of the widget name on canvas, required to enable interaction on canvas
export type WidgetNamePositionData = {
left: number;
top: number;
width: number;
height: number;
widgetNameData: WidgetNameData;
};
//Position of canvas with respect to client browser
export type CanvasPositions = {
top: number;
left: number;
xDiff: number;
width: number;
yDiff: number;
height: number;
};

View File

@ -0,0 +1,382 @@
import type { DragEventHandler, DragEvent } from "react";
import React, { useEffect, useRef } from "react";
import { useSelector } from "react-redux";
import { throttle } from "lodash";
import { Layer, Stage } from "react-konva/lib/ReactKonvaCore";
import { useWidgetSelection } from "utils/hooks/useWidgetSelection";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import {
useShowTableFilterPane,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import type {
CanvasPositions,
WidgetNameData,
WidgetNamePositionData,
WIDGET_NAME_TYPE,
} from "./WidgetNameTypes";
import {
DEFAULT_WIDGET_NAME_CANVAS_HEIGHT,
WIDGET_NAME_CANVAS_PADDING,
widgetNameWrapperStyle,
WIDGET_NAME_CANVAS,
} from "./WidgetNameConstants";
import {
getFocusedWidgetNameData,
getSelectedWidgetNameData,
} from "../selectors";
import type { WidgetPosition } from "layoutSystems/common/types";
import { getShouldAllowDrag } from "selectors/widgetDragSelectors";
import type { Stage as CanvasStageType } from "konva/lib/Stage";
import type { Layer as KonvaLayer } from "konva/lib/Layer";
import { getWidgetNameComponent } from "./utils";
/**
* This Component contains logic to draw widget name on canvas
* and also to make the widget name Intractable like selection of widget or dragging of widget
* @param props Object that contains
* @prop canvasWidth width of canvas in pixels
* @prop containerRef ref of PageViewWrapper component
* @prop parentRef ref of the MainContainerWrapper component i.e, the parent of the canvas component
*/
const OverlayCanvasContainer = (props: {
canvasWidth: number;
containerRef: React.RefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement>;
}) => {
//widget name data of widgets
const selectedWidgetNameData: WidgetNameData | undefined = useSelector(
getSelectedWidgetNameData,
);
const focusedWidgetNameData: WidgetNameData | undefined = useSelector(
getFocusedWidgetNameData,
);
const shouldAllowDrag = useSelector(getShouldAllowDrag);
const wrapperRef = useRef<HTMLDivElement>(null);
// used to keep track of positions of widgetName drawn on canvas to make it intractable
const widgetNamePositions = useRef<{
selected: WidgetNamePositionData | undefined;
focused: WidgetNamePositionData | undefined;
}>({ selected: undefined, focused: undefined });
const { setDraggingState } = useWidgetDragResize();
const showTableFilterPane = useShowTableFilterPane();
//Positions of canvas
const canvasPositions = useRef<CanvasPositions>({
top: 0,
left: 0,
xDiff: 0,
width: 0,
yDiff: 0,
height: 0,
});
const scrollTop = useRef<number>(0);
const isScrolling = useRef(0);
const hasScroll = useRef<boolean>(false);
const stageRef = useRef<CanvasStageType>(null);
const { selectWidget } = useWidgetSelection();
//used to set canvasPositions, which is used further to calculate the exact positions of widgets
useEffect(() => {
if (!stageRef?.current?.content || !wrapperRef?.current) return;
const HTMLCanvas: HTMLDivElement = stageRef?.current?.content;
const rect: DOMRect = HTMLCanvas.getBoundingClientRect();
const wrapper: HTMLDivElement = wrapperRef?.current as HTMLDivElement;
const wrapperRect: DOMRect = wrapper.getBoundingClientRect();
if (rect && wrapperRect) {
canvasPositions.current = {
...canvasPositions.current,
height: wrapperRect.height,
left: rect.left,
top: rect.top,
width: rect.width,
};
}
}, [wrapperRef?.current, props.canvasWidth]);
/**
* Method used to add widget name to the Konva canvas' layer
* @param layer Konva layer onto which the widget name is to be added
* @param widgetNameData widget name data contains more information regarding the widget that is used in drawing the name
* @param position position of widget in pixels
* @param type if it's either selected or focused widget name
*/
const addWidgetNameToCanvas = (
layer: KonvaLayer,
widgetNameData: WidgetNameData,
position: WidgetPosition,
type: WIDGET_NAME_TYPE,
) => {
if (!position) return;
const { id: widgetId, widgetName } = widgetNameData;
//Get Widget Name
if (widgetName) {
const {
canvasLeftOffset,
canvasTopOffset,
widgetNameComponent,
widgetNamePosition,
} = getWidgetNameComponent(
position,
widgetName,
widgetNameData,
props?.parentRef?.current,
stageRef?.current?.content,
scrollTop.current,
);
widgetNamePositions.current[type] = { ...widgetNamePosition };
canvasPositions.current = {
...canvasPositions.current,
xDiff: canvasLeftOffset,
yDiff: canvasTopOffset,
};
//Make widget name clickable
widgetNameComponent.on("click", () => {
selectWidget(SelectionRequestType.One, [widgetId]);
});
//Add widget name to canvas
layer.add(widgetNameComponent);
}
};
/**
* This method is called whenever there is a change in state of canvas,
* i.e, widget position is changed, canvas resized, selected widget changes
* @param widgetPosition
*/
const updateSelectedWidgetPositions = (widgetPosition?: WidgetPosition) => {
if (!stageRef?.current) return;
const stage = stageRef.current;
const layer = stage.getLayers()[0];
//destroy all drawings on canvas
layer.destroyChildren();
//Check and draw selected Widget
if (selectedWidgetNameData) {
const { position: selectedWidgetPosition } = selectedWidgetNameData;
const position = widgetPosition || selectedWidgetPosition;
addWidgetNameToCanvas(
layer,
selectedWidgetNameData,
position,
"selected",
);
}
//Check and draw focused Widget
if (focusedWidgetNameData) {
const { position } = focusedWidgetNameData;
addWidgetNameToCanvas(layer, focusedWidgetNameData, position, "focused");
}
layer.draw();
};
/**
* Mouse Move event function, this tracks every mouse move on canvas such that
* if the mouse position coincides with the positions of widget name, it makes the canvas intractable
* This is throttled since it tracks every single mouse move
*/
const handleMouseMove = throttle((e: MouseEvent) => {
const wrapper = wrapperRef?.current as HTMLDivElement;
if (!wrapper) return;
//check if the mouse is coinciding with the widget name drawing on canvas
const { cursor, isMouseOver } = getMouseOverDetails(e);
//if mouse over make the canvas intractable
if (isMouseOver) {
if (wrapper.style.pointerEvents === "none") {
wrapper.style.pointerEvents = "auto";
}
} // if not mouse over then keep it default
else if (wrapper.style.pointerEvents !== "none") {
wrapper.style.pointerEvents = "none";
wrapper.style.cursor = "default";
}
//set cursor based on intractability
if (!cursor) {
wrapper.style.cursor = "default";
} else if (wrapper.style.cursor !== cursor) {
wrapper.style.cursor = cursor;
}
}, 20);
/**
* on Drag Start event handler to enable drag of widget from the widget name component drawing on canvas
* @param e
*/
const handleDragStart: DragEventHandler = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
//checks if the mouse is over the widget name, if so return it's details
const { isMouseOver, widgetNameData } = getMouseOverDetails(
e as unknown as MouseEvent,
);
if (!isMouseOver || !shouldAllowDrag || widgetNameData?.dragDisabled)
return;
//set dragging state
const startPoints = {
top: 0,
left: 0,
};
showTableFilterPane();
setDraggingState({
isDragging: true,
dragGroupActualParent: widgetNameData?.parentId,
draggingGroupCenter: { widgetId: widgetNameData?.id },
startPoints,
draggedOn: widgetNameData?.parentId,
});
};
/**
* handle Scroll of the canvas, this helps in keeping track og canvas scroll
* so that the widget name remains accurately placed even when the canvas is scrolled
*/
const handleScroll = () => {
if (!props.parentRef?.current) return;
const currentScrollTop: number = props.parentRef?.current?.scrollTop;
if (!isScrolling.current) {
resetCanvas();
}
clearTimeout(isScrolling.current);
isScrolling.current = setTimeout(() => {
scrollTop.current = currentScrollTop;
//while scrolling update the widget name position
updateSelectedWidgetPositions();
isScrolling.current = 0;
if (
(props.parentRef?.current?.scrollHeight || 0) >
(props.parentRef?.current?.clientHeight || 0)
)
hasScroll.current = true;
}, 100);
};
//Add event listeners
useEffect(() => {
if (
!props.containerRef?.current ||
!props.parentRef?.current ||
!wrapperRef?.current
)
return;
const container: HTMLDivElement = props.containerRef
?.current as HTMLDivElement;
const parent: HTMLDivElement = props.parentRef?.current as HTMLDivElement;
container.addEventListener("mousemove", handleMouseMove);
parent.addEventListener("scroll", handleScroll);
return () => {
container.removeEventListener("mousemove", handleMouseMove);
parent.removeEventListener("scroll", handleScroll);
};
}, [
props.containerRef?.current,
props.parentRef?.current,
wrapperRef?.current,
widgetNamePositions.current,
canvasPositions.current,
]);
/**
* This Method verifies if the mouse position coincides with any widget name drawn on canvas
* and returns details regarding the widget
* @param e Mouse event
* @returns Mainly isMouseOver indicating if the mouse is on any one of the widget name
* if true also returns data regarding the widget
*/
const getMouseOverDetails = (e: MouseEvent) => {
const x = e.clientX - canvasPositions.current.left;
const y = e.clientY - canvasPositions.current.top;
const widgetNamePositionsArray = Object.values(widgetNamePositions.current);
//for selected and focused widget names check the widget name positions with respect to mouse positions
for (const widgetNamePosition of widgetNamePositionsArray) {
if (widgetNamePosition) {
const { height, left, top, widgetNameData, width } = widgetNamePosition;
if (x > left && x < left + width && y > top && y < top + height) {
return { isMouseOver: true, cursor: "pointer", widgetNameData };
}
}
}
return { isMouseOver: false };
};
//Used when the position of selected or focused widget changes
useEffect(() => {
if (!selectedWidgetNameData && !focusedWidgetNameData) {
resetCanvas();
} else {
updateSelectedWidgetPositions();
}
}, [selectedWidgetNameData, focusedWidgetNameData]);
/**
* Resets canvas when there is nothing to be drawn on canvas
*/
const resetCanvas = () => {
// Resets stored widget position names
widgetNamePositions.current = { selected: undefined, focused: undefined };
// clears all drawings on canvas
const stage = stageRef.current;
if (!stage) return;
const layer = stage.getLayers()[0];
if (!layer) return;
layer.destroyChildren();
layer.draw();
};
return (
<div
draggable
id={WIDGET_NAME_CANVAS}
onDragStart={handleDragStart}
ref={wrapperRef}
style={widgetNameWrapperStyle}
>
<Stage
height={
canvasPositions?.current.height || DEFAULT_WIDGET_NAME_CANVAS_HEIGHT
}
ref={stageRef}
width={props.canvasWidth + WIDGET_NAME_CANVAS_PADDING}
>
<Layer />
</Stage>
</div>
);
};
export default OverlayCanvasContainer;

View File

@ -0,0 +1,147 @@
import Konva from "konva";
import type { WidgetPosition } from "layoutSystems/common/types";
import type { WidgetNameData, WidgetNamePositionData } from "./WidgetNameTypes";
import {
warningSVGPath,
WidgetNameState,
WIDGET_NAME_FILL_COLORS,
WIDGET_NAME_FONT_SIZE,
WIDGET_NAME_HEIGHT,
WIDGET_NAME_HORIZONTAL_PADDING,
WIDGET_NAME_ICON_PADDING,
WIDGET_NAME_TEXT_COLOR,
WIDGET_NAME_VERTICAL_PADDING,
} from "./WidgetNameConstants";
/**
* used to get the Konva Group Element that is a group of all the elements
* that are to be drawn as part of widget name on canvas
* @param position Position of widget
* @param widgetName widget name
* @param widgetNameData widget name data contains more information regarding the widget that helps in determining the state of widget name
* @param parentDOM DOM element the MainContainerWrapper component i.e, the parent of the canvas component
* @param htmlCanvasDOM DOM element of the html canvas on which the widget name is drawn
* @param scrollTop amount of pixels scrolled by canvas
* @returns an object that contains
* widgetName Group on Konva, position of widgetName on canvas and canvas offsets
*/
export const getWidgetNameComponent = (
position: WidgetPosition,
widgetName: string,
widgetNameData: WidgetNameData,
parentDOM: HTMLDivElement | null,
htmlCanvasDOM: HTMLDivElement | undefined,
scrollTop: number,
) => {
let showIcon = false;
const { nameState } = widgetNameData;
if (nameState === WidgetNameState.ERROR) {
showIcon = true;
}
//Defining Text Element
const textEl = new Konva.Text({
fill: WIDGET_NAME_TEXT_COLOR,
fontFamily: "sans-serif",
fontSize: WIDGET_NAME_FONT_SIZE,
text: widgetName,
x: showIcon
? WIDGET_NAME_ICON_PADDING + WIDGET_NAME_HORIZONTAL_PADDING
: WIDGET_NAME_HORIZONTAL_PADDING,
y: WIDGET_NAME_VERTICAL_PADDING,
});
const textWidth: number = textEl.width();
const componentWidth: number =
textWidth +
WIDGET_NAME_HORIZONTAL_PADDING * 2 +
(showIcon ? WIDGET_NAME_ICON_PADDING : 0);
const {
canvasLeftOffset,
canvasTopOffset,
left: widgetLeft,
top: widgetTop,
} = getPositionsForBoundary(parentDOM, htmlCanvasDOM, position, scrollTop);
const left: number = widgetLeft + position.width - componentWidth + 0.5;
const top: number = widgetTop - WIDGET_NAME_HEIGHT;
//Store the widget name positions for future use
const widgetNamePosition: WidgetNamePositionData = {
left: left,
top: top,
width: componentWidth,
height: WIDGET_NAME_HEIGHT,
widgetNameData: widgetNameData,
};
//rectangle encompassing the widget name
const rectEl = new Konva.Rect({
cornerRadius: [4, 4, 0, 0],
fill: WIDGET_NAME_FILL_COLORS[nameState],
height: WIDGET_NAME_HEIGHT,
width: componentWidth,
x: 0,
y: 0,
});
//Icon in widget name componenet in case of error
const iconEl = new Konva.Path({
x: WIDGET_NAME_HORIZONTAL_PADDING,
y: WIDGET_NAME_VERTICAL_PADDING,
data: warningSVGPath,
fill: WIDGET_NAME_TEXT_COLOR,
scaleX: 0.7,
scaleY: 0.7,
});
//Group Containing all the elements of that particular widget name
const groupEl = new Konva.Group({
height: WIDGET_NAME_HEIGHT,
width: componentWidth,
x: left,
y: top,
});
groupEl.add(rectEl);
groupEl.add(textEl);
showIcon && groupEl.add(iconEl);
return {
widgetNameComponent: groupEl,
widgetNamePosition,
canvasLeftOffset,
canvasTopOffset,
};
};
/**
* Used to calculate the positions of the widget with respect to the HTML Canvas that is rendered by Konva
* @param parentDOM DOM element the MainContainerWrapper component i.e, the parent of the canvas component
* @param htmlCanvasDOM DOM element of the html canvas on which the widget name is drawn
* @param position position of widget with respect to client window in pixels
* @param scrollTop amount of pixels scrolled by canvas
* @returns mainly the left and top of widget with respect to the html canvas
* and also the canvas offset
*/
const getPositionsForBoundary = (
parentDOM: HTMLDivElement | null,
htmlCanvasDOM: HTMLDivElement | undefined,
position: WidgetPosition,
scrollTop: number,
) => {
const { left: parentLeft = 0, top: parentTop = 0 } =
parentDOM?.getBoundingClientRect() || {};
const { left: canvasLeft = 0, top: canvasTop = 0 } =
htmlCanvasDOM?.getBoundingClientRect() || {};
const canvasLeftOffset = parentLeft - canvasLeft;
const canvasTopOffset = parentTop - canvasTop;
const left: number = position.left + canvasLeftOffset;
const top: number = position.top + canvasTopOffset - scrollTop;
return { left, top, canvasLeftOffset, canvasTopOffset };
};

View File

@ -1,18 +0,0 @@
import { canDrag } from "./DraggableComponent";
describe("DraggableComponent", () => {
it("it tests draggable canDrag helper function", () => {
expect(
canDrag(false, false, { dragDisabled: false }, false, false, false),
).toBe(true);
expect(
canDrag(true, false, { dragDisabled: false }, false, false, false),
).toBe(false);
expect(
canDrag(false, true, { dragDisabled: false }, false, false, false),
).toBe(false);
expect(
canDrag(false, false, { dragDisabled: true }, false, false, false),
).toBe(false);
});
});

View File

@ -5,10 +5,6 @@ import type { CSSProperties, DragEventHandler, ReactNode } from "react";
import React, { useMemo, useRef } from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import {
previewModeSelector,
snipingModeSelector,
} from "selectors/editorSelectors";
import {
isCurrentWidgetFocused,
isWidgetSelected,
@ -19,7 +15,7 @@ import {
useShowTableFilterPane,
useWidgetDragResize,
} from "utils/hooks/dragResizeHooks";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
import { getShouldAllowDrag } from "selectors/widgetDragSelectors";
const DraggableWrapper = styled.div`
display: block;
@ -43,6 +39,7 @@ type DraggableComponentProps = {
parentRowSpace: number;
parentColumnSpace: number;
children: ReactNode;
dragDisabled?: boolean;
};
// Widget Boundaries which is shown to indicate the boundaries of the widget
@ -59,42 +56,11 @@ const WidgetBoundaries = styled.div`
left: 0;
`;
/**
* can drag helper function to know if drag and drop should apply
*
* @param isResizingOrDragging
* @param isDraggingDisabled
* @param props
* @param isSnipingMode
* @param isPreviewMode
* @returns
*/
export const canDrag = (
isResizingOrDragging: boolean,
isDraggingDisabled: boolean,
props: any,
isSnipingMode: boolean,
isPreviewMode: boolean,
isAppSettingsPaneWithNavigationTabOpen: boolean,
) => {
return (
!isResizingOrDragging &&
!isDraggingDisabled &&
!props?.dragDisabled &&
!isSnipingMode &&
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen
);
};
function DraggableComponent(props: DraggableComponentProps) {
// Dispatch hook handy to set a widget as focused/selected
const { focusWidget, selectWidget } = useWidgetSelection();
const isSnipingMode = useSelector(snipingModeSelector);
const isPreviewMode = useSelector(previewModeSelector);
const isAppSettingsPaneWithNavigationTabOpen = useSelector(
getIsAppSettingsPaneWithNavigationTabOpen,
);
const shouldAllowDrag = useSelector(getShouldAllowDrag);
// Dispatch hook handy to set any `DraggableComponent` as dragging/ not dragging
// The value is boolean
const { setDraggingState } = useWidgetDragResize();
@ -120,13 +86,6 @@ function DraggableComponent(props: DraggableComponentProps) {
state.ui.widgetDragResize?.dragDetails?.draggedOn === props.parentId,
);
// This state tells us to disable dragging,
// This is usually true when widgets themselves implement drag/drop
// This flag resolves conflicting drag/drop triggers.
const isDraggingDisabled: boolean = useSelector(
(state: AppState) => state.ui.widgetDragResize.isDraggingDisabled,
);
// True when any widget is dragging or resizing, including this one
const isResizingOrDragging = !!isResizing || !!isDragging;
const isCurrentWidgetDragging = isDragging && isSelected;
@ -159,14 +118,7 @@ function DraggableComponent(props: DraggableComponentProps) {
.join("")
.toLowerCase()}`;
const allowDrag = canDrag(
isResizingOrDragging,
isDraggingDisabled,
props,
isSnipingMode,
isPreviewMode,
isAppSettingsPaneWithNavigationTabOpen,
);
const allowDrag = !props?.dragDisabled && shouldAllowDrag;
const className = `${classNameForTesting}`;
const draggableRef = useRef<HTMLDivElement>(null);
const onDragStart: DragEventHandler = (e) => {

View File

@ -0,0 +1,124 @@
import type { AppState } from "@appsmith/reducers";
import { createSelector } from "reselect";
import { getFocusedWidget, getSelectedWidgets } from "selectors/ui";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getWidgets } from "sagas/selectors";
import { getShouldShowWidgetName } from "@appsmith/selectors/entitiesSelector";
import { WidgetNameState } from "./WidgetNamesCanvas/WidgetNameConstants";
import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants";
import { EVAL_ERROR_PATH } from "utils/DynamicBindingUtils";
import get from "lodash/get";
import { getErrorCount } from "layoutSystems/common/widgetName/utils";
import type { WidgetPositions } from "./types";
import type { WidgetProps } from "widgets/BaseWidget";
import type { WidgetNameData } from "./WidgetNamesCanvas/WidgetNameTypes";
import type { DataTree } from "@appsmith/entities/DataTree/types";
export const getWidgetPositions = (state: AppState) =>
state.entities.widgetPositions;
/**
* method to get the widget data required to draw widget name component on canvas
* @param widget widget whose widget name will be drawn on canvas
* @param dataTree contains evaluated widget information that is used to check of the widget has any errors
* @param positions positions of all the widgets in pixels
* @param isFocused boolean to indicate if the widget is focused
* @returns WidgetNameData object which contains information regarding the widget to draw it's widget name on canvas
*/
const getWidgetNameState = (
widget: WidgetProps,
dataTree: DataTree,
positions: WidgetPositions,
isFocused = false,
): WidgetNameData => {
let nameState = isFocused
? WidgetNameState.FOCUSED
: WidgetNameState.SELECTED;
const widgetName = widget.widgetName;
const widgetEntity = dataTree[widgetName];
const parentId = widget.parentId || MAIN_CONTAINER_WIDGET_ID;
if (widgetEntity) {
const errorObj = get(widgetEntity, EVAL_ERROR_PATH, {});
const errorCount = getErrorCount(errorObj);
if (errorCount > 0) {
nameState = WidgetNameState.ERROR;
}
}
const widgetNameData = {
id: widget.widgetId,
position: positions[widget.widgetId],
widgetName: widgetName,
parentId,
dragDisabled: widget.dragDisabled,
nameState,
};
return widgetNameData;
};
/**
* selector to get information regarding the selected widget to draw it's widget name on canvas
*/
export const getSelectedWidgetNameData = createSelector(
getWidgetPositions,
getSelectedWidgets,
getWidgets,
getDataTree,
getShouldShowWidgetName,
(
positions,
selectedWidgets,
widgets,
dataTree,
shouldShowWidgetName,
): WidgetNameData | undefined => {
if (
!selectedWidgets ||
selectedWidgets.length !== 1 ||
!shouldShowWidgetName
)
return;
const selectedWidgetId = selectedWidgets[0];
const selectedWidget = widgets[selectedWidgetId];
if (!selectedWidget) return;
return getWidgetNameState(selectedWidget, dataTree, positions);
},
);
/**
* selector to get information regarding the focused widget to draw it's widget name on canvas
*/
export const getFocusedWidgetNameData = createSelector(
getWidgetPositions,
getFocusedWidget,
getSelectedWidgets,
getWidgets,
getDataTree,
getShouldShowWidgetName,
(
positions,
focusedWidgetId,
selectedWidgets,
widgets,
dataTree,
shouldShowWidgetName,
): WidgetNameData | undefined => {
if (!focusedWidgetId || !widgets || !shouldShowWidgetName) return;
const focusedWidget = widgets[focusedWidgetId];
if (!focusedWidget || selectedWidgets.indexOf(focusedWidgetId) > -1) return;
return getWidgetNameState(focusedWidget, dataTree, positions, true);
},
);

View File

@ -0,0 +1,13 @@
/*
Hols the position of a widget in pixels from the top left of the MainContainer
*/
export type WidgetPosition = {
left: number;
top: number;
height: number;
width: number;
};
export interface WidgetPositions {
[widgetId: string]: WidgetPosition;
}

View File

@ -0,0 +1,187 @@
import { debounce } from "lodash";
import type { RefObject } from "react";
import { ANVIL_LAYER, ANVIL_WIDGET, LAYOUT } from "./utils";
import store from "store";
import { readWidgetPositions } from "layoutSystems/anvil/integrations/actions";
/**
* This Class's main function is to batch all the registered widgets, Flex layers and layout components
* and dispatch an action to process all the affected widgets to determine all the widgets' positions
*
* This inturn acts as an observer to find out update widgets positions whenever the widget position changes
*/
class WidgetPositionsObserver {
// Objects to store registered elements
private registeredWidgets: {
[widgetDOMId: string]: { ref: RefObject<HTMLDivElement>; id: string };
} = {};
private registeredLayers: {
[layerId: string]: {
ref: RefObject<HTMLDivElement>;
canvasId: string;
layerIndex: number;
};
} = {};
private registeredLayouts: {
[layoutId: string]: {
ref: RefObject<HTMLDivElement>;
layoutId: string;
canvasId: string;
};
} = {};
//Queues to process the registered elements that changed
private widgetsProcessQueue: {
[widgetId: string]: boolean;
} = {};
private layersProcessQueue: { [canvasId: string]: number } = {};
private layoutsProcessQueue: { [key: string]: boolean } = {};
private debouncedProcessBatch = debounce(this.processWidgetBatch, 200);
// All the registered elements are registered with this Resize observer
// When any of the elements changes size this observer triggers it
// Add the elements are added to queue to further batch and process
private resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
for (const entry of entries) {
if (entry?.target?.id) {
const DOMId = entry?.target?.id;
if (DOMId.indexOf(ANVIL_WIDGET) > -1) {
this.addWidgetToProcess(DOMId);
} else if (DOMId.indexOf(ANVIL_LAYER) > -1) {
this.addLayerToProcess(DOMId);
} else if (DOMId.indexOf(LAYOUT) > -1) {
this.addLayoutToProcess(DOMId);
}
}
}
},
);
//Method to register widgets for resize observer changes
public observeWidget(
widgetId: string,
widgetDOMId: string,
ref: RefObject<HTMLDivElement>,
) {
if (ref.current) {
this.registeredWidgets[widgetDOMId] = { ref, id: widgetId };
this.resizeObserver.observe(ref.current);
this.addWidgetToProcess(widgetDOMId);
}
}
//Method to de register widgets for resize observer changes
public unObserveWidget(widgetDOMId: string) {
const element = this.registeredWidgets[widgetDOMId]?.ref?.current;
if (element) {
this.resizeObserver.unobserve(element);
}
delete this.registeredWidgets[widgetDOMId];
}
//Method to register layers for resize observer changes
public observeLayer(
layerId: string,
canvasId: string,
layerIndex: number,
ref: RefObject<HTMLDivElement>,
) {
if (ref?.current) {
this.registeredLayers[layerId] = { ref, canvasId, layerIndex };
this.resizeObserver.observe(ref.current);
}
}
//Method to de register layers for resize observer changes
public unObserveLayer(layerId: string) {
const element = this.registeredLayers[layerId]?.ref?.current;
if (element) {
this.resizeObserver.unobserve(element);
}
delete this.registeredLayers[layerId];
}
//Method to register layouts for resize observer changes
public observeLayout(
layoutId: string,
ref: RefObject<HTMLDivElement>,
canvasId: string,
id: string,
) {
if (ref?.current) {
this.registeredLayouts[layoutId] = { ref, canvasId, layoutId: id };
this.resizeObserver.observe(ref.current);
}
}
//Method to de register layouts for resize observer changes
public unObserveLayout(layoutId: string) {
const element = this.registeredLayouts[layoutId]?.ref?.current;
if (element) {
this.resizeObserver.unobserve(element);
}
delete this.registeredLayouts[layoutId];
}
//This method is triggered from the resize observer to add widgets to queue
private addWidgetToProcess(widgetDOMId: string) {
if (this.registeredWidgets[widgetDOMId]) {
const widgetId = this.registeredWidgets[widgetDOMId].id;
this.widgetsProcessQueue[widgetId] = true;
this.debouncedProcessBatch();
}
}
//This method is triggered from the resize observer to add layer to queue
private addLayerToProcess(LayerId: string) {
if (this.registeredLayers[LayerId]) {
const { canvasId, layerIndex } = this.registeredLayers[LayerId];
//If the layer in canvas already exist
//and if the current layer is further higher than the previous one
//add this layer to queue
if (
this.layersProcessQueue[canvasId] === undefined ||
this.layersProcessQueue[canvasId] > layerIndex
) {
this.layersProcessQueue[canvasId] = layerIndex;
}
this.debouncedProcessBatch();
}
}
//This method is triggered from the resize observer to add layout to queue
private addLayoutToProcess(layoutId: string) {
if (this.registeredLayouts[layoutId]) {
const id = this.registeredLayouts[layoutId].layoutId;
this.layoutsProcessQueue[id] = true;
this.debouncedProcessBatch();
}
}
//Clear all process queues
private clearProcessQueue() {
this.widgetsProcessQueue = {};
this.layersProcessQueue = {};
this.layoutsProcessQueue = {};
}
//Dispatch all the changed elements to saga for further processing to update widget positions
private processWidgetBatch() {
store.dispatch(
readWidgetPositions(
{ ...this.widgetsProcessQueue },
{ ...this.layersProcessQueue },
{ ...this.layoutsProcessQueue },
),
);
this.clearProcessQueue();
}
}
export const widgetPositionsObserver = new WidgetPositionsObserver();

View File

@ -0,0 +1,31 @@
export const ANVIL_LAYER = "anvil_layer";
export const ANVIL_WIDGET = "anvil_widget";
export const LAYOUT = "layout";
/**
* Method to return Id of widget with widgetId
* @param widgetId
* @returns
*/
export const getAnvilWidgetId = (widgetId: string) => {
return ANVIL_WIDGET + "_" + widgetId;
};
/**
* Method to return Id of layer in canvasId of index layerIndex
* @param canvasId
* @param layerIndex
* @returns
*/
export const getAnvilLayerId = (canvasId: string, layerIndex: number) => {
return ANVIL_LAYER + "_" + canvasId + "_" + layerIndex;
};
/**
* Method to return Id of layout with layoutId
* @param layoutId
* @returns
*/
export const getLayoutId = (layoutId: string) => {
return LAYOUT + "_" + layoutId;
};

View File

@ -15,6 +15,7 @@ import {
useTheme,
} from "@design-system/theming";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
import { renderAppsmithCanvas } from "layoutSystems/CanvasFactory";
import type { WidgetProps } from "widgets/BaseWidget";
@ -83,8 +84,8 @@ const Canvas = (props: CanvasProps) => {
className={`relative t--canvas-artboard ${paddingBottomClass} transition-all duration-400 ${marginHorizontalClass} ${getViewportClassName(
canvasWidth,
)}`}
data-testid="t--canvas-artboard"
id="art-board"
data-testid={"t--canvas-artboard"}
id={CANVAS_ART_BOARD}
ref={focusRef}
width={canvasWidth}
>

View File

@ -0,0 +1,37 @@
import { combineReducers } from "redux";
import appReducer from "./appReducer";
import canvasWidgetsReducer from "./canvasWidgetsReducer";
import canvasWidgetsStructureReducer from "./canvasWidgetsStructureReducer";
import metaWidgetsReducer from "./metaWidgetsReducer";
import datasourceReducer from "./datasourceReducer";
import jsActionsReducer from "./jsActionsReducer";
import jsExecutionsReducer from "./jsExecutionsReducer";
import metaReducer from "./metaReducer";
import pageListReducer from "./pageListReducer";
import pluginsReducer from "reducers/entityReducers/pluginsReducer";
import autoHeightLayoutTreeReducer from "./autoHeightReducers/autoHeightLayoutTreeReducer";
import canvasLevelsReducer from "./autoHeightReducers/canvasLevelsReducer";
import actionsReducer from "@appsmith/reducers/entityReducers/actionsReducer";
/* Reducers which are integrated into the core system when registering a pluggable module
or done so by a module that is designed to be eventually pluggable */
import widgetPositionsReducer from "layoutSystems/anvil/integrations/reducers/widgetPositionsReducer";
const entityReducer = combineReducers({
canvasWidgets: canvasWidgetsReducer,
canvasWidgetsStructure: canvasWidgetsStructureReducer,
metaWidgets: metaWidgetsReducer,
actions: actionsReducer,
datasources: datasourceReducer,
pageList: pageListReducer,
jsExecutions: jsExecutionsReducer,
plugins: pluginsReducer,
meta: metaReducer,
app: appReducer,
jsActions: jsActionsReducer,
autoHeightLayoutTree: autoHeightLayoutTreeReducer,
canvasLevels: canvasLevelsReducer,
widgetPositions: widgetPositionsReducer,
});
export default entityReducer;

View File

@ -0,0 +1,42 @@
import type { AppState } from "@appsmith/reducers";
import { createSelector } from "reselect";
import { getIsAppSettingsPaneWithNavigationTabOpen } from "./appSettingsPaneSelectors";
import { previewModeSelector, snipingModeSelector } from "./editorSelectors";
export const getIsDragging = (state: AppState) =>
state.ui.widgetDragResize.isDragging;
export const getIsResizing = (state: AppState) =>
state.ui.widgetDragResize.isResizing;
export const getIsDraggingDisabledInEditor = (state: AppState) =>
state.ui.widgetDragResize.isDraggingDisabled;
/**
* getShouldAllowDrag is a Selector that indicates if the widget could be dragged on canvas based on different states
*/
export const getShouldAllowDrag = createSelector(
getIsResizing,
getIsDragging,
getIsDraggingDisabledInEditor,
previewModeSelector,
snipingModeSelector,
getIsAppSettingsPaneWithNavigationTabOpen,
(
isResizing,
isDragging,
isDraggingDisabled,
isPreviewMode,
isSnipingMode,
isAppSettingsPaneWithNavigationTabOpen,
) => {
return (
!isResizing &&
!isDragging &&
!isDraggingDisabled &&
!isSnipingMode &&
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen
);
},
);

View File

@ -47,6 +47,7 @@ import type { ThemeProp } from "WidgetProvider/constants";
import { isAirgapped } from "@appsmith/utils/airgapHelpers";
import { importSvg } from "design-system-old";
import { getVideoConstraints } from "../../utils";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
const CameraOfflineIcon = importSvg(
() => import("assets/icons/widget/camera/camera-offline.svg"),
@ -806,7 +807,9 @@ function DevicePopover(props: DevicePopoverProps) {
content={<DeviceMenu items={items} onItemClick={onItemClick} />}
disabled={disabledMenu}
minimal
portalContainer={document.getElementById("art-board") || undefined}
portalContainer={
document.getElementById(CANVAS_ART_BOARD) || undefined
}
>
<Button
disabled={disabledMenu}

View File

@ -8,6 +8,7 @@ import { Classes } from "@blueprintjs/core";
import { countryToFlag } from "./utilities";
import { Colors } from "constants/Colors";
import { lightenColor } from "widgets/WidgetUtils";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
const StyledDropdown = styled(Dropdown)`
/*
@ -256,7 +257,7 @@ export default function CurrencyTypeDropdown(props: CurrencyDropdownProps) {
optionWidth="360px"
options={props.options}
portalClassName={`country-type-filter-dropdown-${props.widgetId}`}
portalContainer={document.getElementById("art-board") || undefined}
portalContainer={document.getElementById(CANVAS_ART_BOARD) || undefined}
searchAutoFocus
searchPlaceholder="Search by currency or country"
selected={selectedOption}

View File

@ -26,6 +26,7 @@ import LabelWithTooltip, {
const DATEPICKER_POPUP_CLASSNAME = "datepickerwidget-popup";
import { required } from "utils/validation/common";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
function hasFulfilledRequiredCondition(
isRequired: boolean | undefined,
@ -381,7 +382,7 @@ class DatePickerComponent extends React.Component<
placeholder={"Select Date"}
popoverProps={{
portalContainer:
document.getElementById("art-board") || undefined,
document.getElementById(CANVAS_ART_BOARD) || undefined,
usePortal: !this.props.withoutPortal,
canEscapeKeyClose: true,
portalClassName: `${DATEPICKER_POPUP_CLASSNAME}-${this.props.widgetId}`,

View File

@ -8,6 +8,7 @@ import { ISDCodeOptions } from "constants/ISDCodes_v2";
import { Colors } from "constants/Colors";
import { Classes } from "@blueprintjs/core";
import { lightenColor } from "widgets/WidgetUtils";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
type DropdownTriggerIconWrapperProp = {
allowDialCodeChange: boolean;
@ -280,7 +281,7 @@ export default function ISDCodeDropdown(props: ISDCodeDropdownProps) {
optionWidth="360px"
options={props.options}
portalClassName={`country-type-filter-dropdown-${props.widgetId}`}
portalContainer={document.getElementById("art-board") || undefined}
portalContainer={document.getElementById(CANVAS_ART_BOARD) || undefined}
searchAutoFocus
searchPlaceholder="Search by ISD code or country"
selected={props.selected}

View File

@ -24,6 +24,7 @@ import type { LabelPosition } from "components/constants";
import SelectButton from "./SelectButton";
import { labelMargin } from "../../WidgetUtils";
import LabelWithTooltip from "widgets/components/LabelWithTooltip";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
const DEBOUNCE_TIMEOUT = 800;
const ITEM_SIZE = 40;
@ -383,7 +384,7 @@ class SelectComponent extends React.Component<
onQueryChange={this.onQueryChange}
popoverProps={{
portalContainer:
document.getElementById("art-board") || undefined,
document.getElementById(CANVAS_ART_BOARD) || undefined,
boundary: "window",
isOpen: this.state.isOpen,
minimal: true,

View File

@ -24,6 +24,7 @@ import { isColumnTypeEditable } from "widgets/TableWidgetV2/widget/utilities";
import { Popover2 } from "@blueprintjs/popover2";
import { MenuDivider } from "@design-system/widgets-old";
import { importRemixIcon, importSvg } from "@design-system/widgets-old";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
const Check = importRemixIcon(() => import("remixicon-react/CheckFillIcon"));
const ArrowDownIcon = importRemixIcon(
@ -326,7 +327,9 @@ const HeaderCellComponent = (props: HeaderProps) => {
onInteraction={setIsMenuOpen}
placement="bottom-end"
portalClassName={`${HEADER_MENU_PORTAL_CLASS}-${props.widgetId}`}
portalContainer={document.getElementById("art-board") || undefined}
portalContainer={
document.getElementById(CANVAS_ART_BOARD) || undefined
}
>
<ArrowDownIcon className="w-5 h-5" color="var(--wds-color-icon)" />
</Popover2>

View File

@ -22,6 +22,7 @@ import type { WidgetProps } from "widgets/BaseWidget";
import { selectWidgetInitAction } from "actions/widgetSelectionActions";
import { SelectionRequestType } from "sagas/WidgetSelectUtils";
import { importSvg } from "design-system-old";
import { CANVAS_ART_BOARD } from "constants/componentClassNameConstants";
const DragHandleIcon = importSvg(
() => import("assets/icons/ads/app-icons/draghandler.svg"),
@ -106,7 +107,9 @@ class TableFilterPane extends Component<Props> {
onPositionChange={this.handlePositionUpdate}
parentElement={boundaryParent}
placement="top"
portalContainer={document.getElementById("art-board") || undefined}
portalContainer={
document.getElementById(CANVAS_ART_BOARD) || undefined
}
position={get(this.props, "metaProps.position") as PositionPropsInt}
renderDragBlock={
<DragBlock>

View File

@ -8594,6 +8594,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-reconciler@npm:~0.26.2":
version: 0.26.7
resolution: "@types/react-reconciler@npm:0.26.7"
dependencies:
"@types/react": "*"
checksum: 4122d2b08580f775d0aeae9bd10b68248f894096ed14c0ebbc143ef712e21b159e89d0c628bd95dd3329947fc1ee94a0cb1d2d32b32b1d5d225e70030e91e58f
languageName: node
linkType: hard
"@types/react-redux@npm:^7.0.1, @types/react-redux@npm:^7.1.16":
version: 7.1.18
resolution: "@types/react-redux@npm:7.1.18"
@ -10369,6 +10378,7 @@ __metadata:
jshint: ^2.13.4
json5: ^2.2.3
klona: ^2.0.5
konva: 8.0.1
libphonenumber-js: ^1.9.44
linkedom: ^0.14.20
lint-staged: ^13.2.0
@ -10430,6 +10440,7 @@ __metadata:
react-instantsearch-dom: ^6.4.0
react-is: ^16.12.0
react-json-view: ^1.21.3
react-konva: 17.0.2-6
react-masonry-css: ^1.0.16
react-media-recorder: ^1.6.1
react-modal: ^3.15.1
@ -20800,6 +20811,13 @@ __metadata:
languageName: node
linkType: hard
"konva@npm:8.0.1":
version: 8.0.1
resolution: "konva@npm:8.0.1"
checksum: 989702028faed7981780d74bf4fa2f682627e726832745b4258af93bf0ef5bde766db0f5e5cd47a01839796698b01dbb4c9d6e8592c03321ba38a24e0b6dae3d
languageName: node
linkType: hard
"labeled-stream-splicer@npm:^2.0.0":
version: 2.0.2
resolution: "labeled-stream-splicer@npm:2.0.2"
@ -26225,6 +26243,21 @@ __metadata:
languageName: node
linkType: hard
"react-konva@npm:17.0.2-6":
version: 17.0.2-6
resolution: "react-konva@npm:17.0.2-6"
dependencies:
"@types/react-reconciler": ~0.26.2
react-reconciler: ~0.26.2
scheduler: ^0.20.2
peerDependencies:
konva: ^8.0.1 || ^7.2.5
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 5e868f6941090243c998f2817fbc9f031f60c83e236dc3f6328c904114582cfe281a08df9b5146b3339f6e1ccf30f3707d03de85fd534fb42fbe0ee6c6531b0c
languageName: node
linkType: hard
"react-lifecycles-compat@npm:^3.0.0, react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"
@ -26370,6 +26403,19 @@ __metadata:
languageName: node
linkType: hard
"react-reconciler@npm:~0.26.2":
version: 0.26.2
resolution: "react-reconciler@npm:0.26.2"
dependencies:
loose-envify: ^1.1.0
object-assign: ^4.1.1
scheduler: ^0.20.2
peerDependencies:
react: ^17.0.2
checksum: 2ebceace56f547f51eaf142becefef9cca980eae4f42d90ee5a966f54a375f5082d78b71b00c40bbd9bca69e0e0f698c7d4e81cc7373437caa19831fddc1d01b
languageName: node
linkType: hard
"react-redux@npm:^7.1.1, react-redux@npm:^7.2.4":
version: 7.2.4
resolution: "react-redux@npm:7.2.4"