PromucFlow_constructor/app/client/src/utils/hooks/useDynamicAppLayout.tsx
Dhruvik Neharia 099859134d
feat: Improved App Navigation (#19312)
## TL;DR
A new revamped experience for navigation for Appsmith users.

## Description 
Introduces new navigation styles with better default navigation - Top
(Stacked), a variant for Top (Inline), and a collapsible Sidebar.
Configure your app's navigation by navigating to the navigation settings
tab inside the app settings pane and observe how your app with the
selected navigation settings will look side by side as you change them.

This PR pushes the v1 for EPIC #17766.

Fixes #19157
Fixes #19158
Fixes #19174
Fixes #19173
Fixes #19160
Fixes #20712
Fixes #19161
Fixes #20554
Fixes #20938
Fixes #21129

## Media
<video
src="https://user-images.githubusercontent.com/22471214/227187245-84e4e3fa-18e4-4690-8237-cfce29f432e5.mp4"></video>

## Type of change
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

## How Has This Been Tested?
- Manual
- Cypress

### Test Plan

https://www.notion.so/appsmith/Test-Plan-a7883ae4980d470690de5c62a41dd168

### Issues raised during DP testing

https://docs.google.com/spreadsheets/d/1Kocq8h1H3EXlbqDgiNruzBr9MeNPyY26zct8IWYEY40/edit#gid=0

## Checklist:
### Dev activity
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


### QA activity:
- [ ] Test plan has been approved by relevant developers
- [ ] Test plan has been peer reviewed by QA
- [ ] Cypress test cases have been added and approved by either SDET or
manual QA
- [ ] Organized project review call with relevant stakeholders after
Round 1/2 of QA
- [ ] Added Test Plan Approved label after reveiwing all Cypress test

---------

Co-authored-by: Pawan Kumar <pawan@appsmith.com>
2023-03-23 17:11:58 +05:30

330 lines
10 KiB
TypeScript

import { debounce, get } from "lodash";
import { useCallback, useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
// import { updateLayoutForMobileBreakpointAction } from "actions/autoLayoutActions";
import { updateCanvasLayoutAction } from "actions/editorActions";
import { APP_SETTINGS_PANE_WIDTH } from "constants/AppConstants";
import {
DefaultLayoutType,
layoutConfigurations,
// MAIN_CONTAINER_WIDGET_ID,
} from "constants/WidgetConstants";
import { APP_MODE } from "entities/App";
// import { AppPositioningTypes } from "reducers/entityReducers/pageListReducer";
import {
getCurrentApplicationLayout,
// getCurrentAppPositioningType,
getCurrentPageId,
getMainCanvasProps,
previewModeSelector,
} from "selectors/editorSelectors";
import { getAppMode } from "selectors/entitiesSelector";
import {
getExplorerPinned,
getExplorerWidth,
} from "selectors/explorerSelector";
import { getIsCanvasInitialized } from "selectors/mainCanvasSelectors";
import {
getIsAppSettingsPaneOpen,
getIsAppSettingsPaneWithNavigationTabOpen,
} from "selectors/appSettingsPaneSelectors";
import {
getPaneCount,
getTabsPaneWidth,
isMultiPaneActive,
} from "selectors/multiPaneSelectors";
import { SIDE_NAV_WIDTH } from "pages/common/SideNav";
import {
getAppSidebarPinned,
getCurrentApplication,
getSidebarWidth,
} from "selectors/applicationSelectors";
import { useIsMobileDevice } from "./useDeviceDetect";
import { getPropertyPaneWidth } from "selectors/propertyPaneSelectors";
import { scrollbarWidth } from "utils/helpers";
import { useWindowSizeHooks } from "./dragResizeHooks";
const BORDERS_WIDTH = 2;
const GUTTER_WIDTH = 72;
export const AUTOLAYOUT_RESIZER_WIDTH_BUFFER = 40;
export const useDynamicAppLayout = () => {
const dispatch = useDispatch();
const explorerWidth = useSelector(getExplorerWidth);
const propertyPaneWidth = useSelector(getPropertyPaneWidth);
const isExplorerPinned = useSelector(getExplorerPinned);
const appMode: APP_MODE | undefined = useSelector(getAppMode);
const { width: screenWidth } = useWindowSizeHooks();
const mainCanvasProps = useSelector(getMainCanvasProps);
const isPreviewMode = useSelector(previewModeSelector);
const currentPageId = useSelector(getCurrentPageId);
const isCanvasInitialized = useSelector(getIsCanvasInitialized);
const appLayout = useSelector(getCurrentApplicationLayout);
const isAppSettingsPaneOpen = useSelector(getIsAppSettingsPaneOpen);
const tabsPaneWidth = useSelector(getTabsPaneWidth);
const isMultiPane = useSelector(isMultiPaneActive);
const paneCount = useSelector(getPaneCount);
const isAppSidebarPinned = useSelector(getAppSidebarPinned);
const sidebarWidth = useSelector(getSidebarWidth);
const isAppSettingsPaneWithNavigationTabOpen = useSelector(
getIsAppSettingsPaneWithNavigationTabOpen,
);
const currentApplicationDetails = useSelector(getCurrentApplication);
const isMobile = useIsMobileDevice();
// const appPositioningType = useSelector(getCurrentAppPositioningType);
// /**
// * calculates min height
// */
// const calculatedMinHeight = useMemo(() => {
// return calculateDynamicHeight();
// }, [mainCanvasProps]);
/**
* app layout range i.e minWidth and maxWidth for the current layout
* if there is no config for the current layout, use default layout i.e desktop
*/
const layoutWidthRange = useMemo(() => {
let minWidth = -1;
let maxWidth = -1;
if (appLayout) {
const { type } = appLayout;
const currentLayoutConfig = get(
layoutConfigurations,
type,
layoutConfigurations[DefaultLayoutType],
);
if (currentLayoutConfig.minWidth) minWidth = currentLayoutConfig.minWidth;
if (currentLayoutConfig.maxWidth) maxWidth = currentLayoutConfig.maxWidth;
}
return { minWidth, maxWidth };
}, [appLayout]);
/**
* calculate the width for the canvas
*
* cases:
* - if max width is negative, use calculated width
* - if calculated width is in range of min/max widths of layout, use calculated width
* - if calculated width is less then min width, use min Width
* - if calculated width is larger than max width, use max width
* - by default use min width
*
* @returns
*/
const calculateCanvasWidth = () => {
const { maxWidth, minWidth } = layoutWidthRange;
let calculatedWidth = screenWidth - scrollbarWidth();
// if preview mode is not on and the app setting pane is not opened, we need to subtract the width of the property pane
if (
isPreviewMode === false &&
!isAppSettingsPaneOpen &&
appMode === APP_MODE.EDIT
) {
calculatedWidth -= propertyPaneWidth;
}
// if app setting pane is open, we need to subtract the width of app setting page width
if (isAppSettingsPaneOpen === true && appMode === APP_MODE.EDIT) {
calculatedWidth -= APP_SETTINGS_PANE_WIDTH;
}
// if explorer is closed or its preview mode, we don't need to subtract the EE width
if (
isExplorerPinned === true &&
!isPreviewMode &&
appMode === APP_MODE.EDIT
) {
calculatedWidth -= explorerWidth;
}
if (isMultiPane) {
calculatedWidth = screenWidth - scrollbarWidth() - tabsPaneWidth - 100;
if (paneCount === 3) calculatedWidth -= propertyPaneWidth;
}
/**
* If there is
* 1. a sidebar for navigation,
* 2. it is pinned,
* 3. and device is not mobile
* we need to subtract the sidebar width as well in the following modes -
* 1. Preview
* 2. App settings open with navigation tab
* 3. Published
*/
if (
(appMode === APP_MODE.PUBLISHED ||
isPreviewMode ||
isAppSettingsPaneWithNavigationTabOpen) &&
!isMobile &&
sidebarWidth
) {
calculatedWidth -= sidebarWidth;
}
// const ele: any = document.getElementById("canvas-viewport");
// if (
// appMode === "EDIT" &&
// appLayout?.type === "FLUID" &&
// ele &&
// calculatedWidth > ele.clientWidth
// ) {
// calculatedWidth = ele.clientWidth;
// }
// if (appPositioningType === AppPositioningTypes.AUTO && isPreviewMode) {
// calculatedWidth -= AUTOLAYOUT_RESIZER_WIDTH_BUFFER;
// }
switch (true) {
case maxWidth < 0:
case appLayout?.type === "FLUID":
case calculatedWidth < maxWidth && calculatedWidth > minWidth:
const totalWidthToSubtract = BORDERS_WIDTH + GUTTER_WIDTH;
// NOTE: gutter + border width will be only substracted when theme mode and preview mode are off
return (
calculatedWidth -
(appMode === APP_MODE.EDIT &&
!isPreviewMode &&
!isAppSettingsPaneWithNavigationTabOpen
? totalWidthToSubtract
: 0)
);
case calculatedWidth < minWidth:
return minWidth;
case calculatedWidth > maxWidth:
return maxWidth;
default:
return minWidth;
}
};
/**
* resizes the layout based on the layout type
*
* @param screenWidth
* @param appLayout
*/
const resizeToLayout = () => {
const calculatedWidth = calculateCanvasWidth();
const { width: rightColumn } = mainCanvasProps || {};
let scale = 1;
if (isMultiPane && appLayout?.type !== "FLUID") {
let canvasSpace =
screenWidth -
tabsPaneWidth -
SIDE_NAV_WIDTH -
GUTTER_WIDTH -
BORDERS_WIDTH;
if (paneCount === 3) canvasSpace -= propertyPaneWidth;
// Scale will always be between 0.5 to 1
scale = Math.max(
Math.min(+Math.abs(canvasSpace / calculatedWidth).toFixed(2), 1),
0.5,
);
dispatch(updateCanvasLayoutAction(calculatedWidth, scale));
} else if (rightColumn !== calculatedWidth || !isCanvasInitialized) {
dispatch(updateCanvasLayoutAction(calculatedWidth, scale));
}
};
const debouncedResize = useCallback(debounce(resizeToLayout, 250), [
mainCanvasProps,
screenWidth,
tabsPaneWidth,
paneCount,
]);
// const immediateDebouncedResize = useCallback(debounce(resizeToLayout), [
// mainCanvasProps,
// screenWidth,
// currentPageId,
// appMode,
// appLayout,
// isPreviewMode,
// ]);
// const resizeObserver = new ResizeObserver(immediateDebouncedResize);
// useEffect(() => {
// const ele: any = document.getElementById("canvas-viewport");
// if (ele) {
// if (appLayout?.type === "FLUID") {
// resizeObserver.observe(ele);
// } else {
// resizeObserver.unobserve(ele);
// }
// }
// return () => {
// ele && resizeObserver.unobserve(ele);
// };
// }, [appLayout, currentPageId, isPreviewMode]);
/**
* when screen height is changed, update canvas layout
*/
// useEffect(() => {
// if (calculatedMinHeight !== mainCanvasProps?.height) {
// // dispatch(updateCanvasLayoutAction(mainCanvasProps?.width));
// }
// }, [screenHeight, mainCanvasProps?.height]);
useEffect(() => {
if (isCanvasInitialized) debouncedResize();
}, [screenWidth, tabsPaneWidth, paneCount]);
/**
* resize the layout if any of the following thing changes:
* - app layout
* - page
* - container right column
* - preview mode
* - explorer width
* - explorer is pinned
* - theme mode is turned on
* - sidebar pin/unpin
* - app settings pane open with navigation tab
* - any of the following navigation settings changes
* - orientation
* - nav style
* - device changes to/from mobile
*/
useEffect(() => {
resizeToLayout();
}, [
appLayout,
mainCanvasProps?.width,
isPreviewMode,
isAppSettingsPaneWithNavigationTabOpen,
explorerWidth,
propertyPaneWidth,
isExplorerPinned,
propertyPaneWidth,
isAppSettingsPaneOpen,
isAppSidebarPinned,
currentApplicationDetails?.applicationDetail?.navigationSetting
?.orientation,
currentApplicationDetails?.applicationDetail?.navigationSetting?.navStyle,
isMobile,
currentPageId, //TODO: preet - remove this after first merge.
]);
// useEffect(() => {
// dispatch(
// updateLayoutForMobileBreakpointAction(
// MAIN_CONTAINER_WIDGET_ID,
// appPositioningType === AppPositioningTypes.AUTO
// ? mainCanvasProps?.isMobile
// : false,
// calculateCanvasWidth(),
// ),
// );
// }, [mainCanvasProps?.isMobile, appPositioningType]);
return isCanvasInitialized;
};