Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Pages section redesign - IDE navigation #30212

Merged
merged 11 commits into from
Jan 11, 2024
Merged
3 changes: 3 additions & 0 deletions app/client/src/pages/Editor/Explorer/Common/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export const RelativeContainer = styled.div`
`;

export const StyledEntity = styled(Entity)<{ entitySize?: number }>`
&.page.fullWidth {
width: 100%;
}
&.pages > div:not(.t--entity-item) > div > div {
max-height: 40vh;
min-height: ${({ entitySize }) =>
Expand Down
81 changes: 81 additions & 0 deletions app/client/src/pages/Editor/IDE/EditorPane/MinimalSegment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React from "react";
import { Flex, Text, Tag } from "design-system";
import { useDispatch, useSelector } from "react-redux";

import { createMessage, EDITOR_PANE_TEXTS } from "@appsmith/constants/messages";
import {
getActionsCount,
getJsActionsCount,
getWidgetsCount,
} from "selectors/ideSelectors";
import { setIdeEditorPagesActiveStatus } from "actions/ideActions";

const MinimalSegment = () => {
const dispatch = useDispatch();
const actionsCount = useSelector(getActionsCount);
const jsActionsCount = useSelector(getJsActionsCount);
const widgetsCount = useSelector(getWidgetsCount);

const onClickHandler = () => {
dispatch(setIdeEditorPagesActiveStatus(false));
};

return (
<Flex
alignItems={"center"}
borderTop={"1px solid var(--ads-v2-color-border)"}
cursor={"pointer"}
gap={"spaces-2"}
height={"36px"}
justifyContent={"space-between"}
onClick={onClickHandler}
p={"spaces-2"}
px={"spaces-3"}
width={"100%"}
>
<Flex
alignItems={"center"}
flex={"1"}
gap={"spaces-2"}
height={"100%"}
justifyContent={"center"}
width={"100%"}
>
<Text kind={"body-m"}>
{createMessage(EDITOR_PANE_TEXTS.queries_tab)}
</Text>
<Tag isClosable={false} size="md">
{actionsCount}
</Tag>
</Flex>
<Flex
alignItems={"center"}
flex={"1"}
gap={"spaces-2"}
height={"100%"}
justifyContent={"center"}
width={"100%"}
>
<Text kind={"body-m"}>{createMessage(EDITOR_PANE_TEXTS.js_tab)}</Text>
<Tag isClosable={false} size="md">
{jsActionsCount}
</Tag>
</Flex>
<Flex
alignItems={"center"}
flex={"1"}
gap={"spaces-2"}
height={"100%"}
justifyContent={"center"}
width={"100%"}
>
<Text kind={"body-m"}>{createMessage(EDITOR_PANE_TEXTS.ui_tab)}</Text>
<Tag isClosable={false} size="md">
{widgetsCount}
</Tag>
</Flex>
</Flex>
);
};

export { MinimalSegment };
212 changes: 212 additions & 0 deletions app/client/src/pages/Editor/IDE/EditorPane/PagesSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Flex, Text } from "design-system";
import { animated, useSpring } from "react-spring";
import { useDispatch, useSelector } from "react-redux";
import { useLocation } from "react-router";

import {
getCurrentPageId,
selectAllPages,
} from "@appsmith/selectors/entitiesSelector";
import type { Page } from "@appsmith/constants/ReduxActionConstants";
import PageContextMenu from "pages/Editor/Explorer/Pages/PageContextMenu";
import { StyledEntity } from "pages/Editor/Explorer/Common/components";
import { defaultPageIcon, pageIcon } from "pages/Editor/Explorer/ExplorerIcons";
import {
getHasCreatePagePermission,
getHasManagePagePermission,
} from "@appsmith/utils/BusinessFeatures/permissionPageHelpers";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
import { getCurrentApplicationId } from "selectors/editorSelectors";
import { EntityClassNames } from "pages/Editor/Explorer/Entity";
import {
isPermitted,
PERMISSION_TYPE,
} from "@appsmith/utils/permissionHelpers";
import { getCurrentApplication } from "@appsmith/selectors/applicationSelectors";
import type { AppState } from "@appsmith/reducers";
import { builderURL, widgetListURL } from "@appsmith/RouteBuilder";
import AnalyticsUtil from "utils/AnalyticsUtil";
import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions";
import history, { NavigationMethod } from "utils/history";
import { resolveAsSpaceChar } from "utils/helpers";
import { createNewPageFromEntities, updatePage } from "actions/pageActions";
import { setIdeEditorPagesActiveStatus } from "actions/ideActions";
import AddPageContextMenu from "pages/Editor/Explorer/Pages/AddPageContextMenu";
import { getNextEntityName } from "utils/AppsmithUtils";
import { getCurrentWorkspaceId } from "@appsmith/selectors/workspaceSelectors";
import { getInstanceId } from "@appsmith/selectors/tenantSelectors";

const AnimatedFlex = animated(Flex);

const PagesSection = () => {
const dispatch = useDispatch();
const location = useLocation();
const pages: Page[] = useSelector(selectAllPages);
const currentPageId = useSelector(getCurrentPageId);
const applicationId = useSelector(getCurrentApplicationId);
const userAppPermissions = useSelector(
(state: AppState) => getCurrentApplication(state)?.userPermissions ?? [],
);
const workspaceId = useSelector(getCurrentWorkspaceId);
const instanceId = useSelector(getInstanceId);

const [isMenuOpen, setIsMenuOpen] = useState(false);

const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);

const [springs, api] = useSpring(() => ({
from: { opacity: 0, height: "0%" },
to: { opacity: 1, height: "100%" },
}));

const hasExportPermission = isPermitted(
userAppPermissions ?? [],
PERMISSION_TYPE.EXPORT_APPLICATION,
);

const canCreatePages = getHasCreatePagePermission(
isFeatureEnabled,
userAppPermissions,
);

useEffect(() => {
api.start();
}, []);

const switchPage = useCallback(
(page: Page) => {
const navigateToUrl =
currentPageId === page.pageId
? widgetListURL({})
: builderURL({
pageId: page.pageId,
});
AnalyticsUtil.logEvent("PAGE_NAME_CLICK", {
name: page.pageName,
fromUrl: location.pathname,
type: "PAGES",
toUrl: navigateToUrl,
});
dispatch(toggleInOnboardingWidgetSelection(true));
dispatch(setIdeEditorPagesActiveStatus(false));
history.push(navigateToUrl, {
invokedBy: NavigationMethod.EntityExplorer,
});
},
[location.pathname, currentPageId],
);

const createPageCallback = useCallback(() => {
const name = getNextEntityName(
"Page",
pages.map((page: Page) => page.pageName),
);
dispatch(setIdeEditorPagesActiveStatus(false));
dispatch(
createNewPageFromEntities(
applicationId,
name,
workspaceId,
false,
instanceId,
),
);
}, [dispatch, pages, applicationId]);

const onMenuClose = useCallback(() => setIsMenuOpen(false), [setIsMenuOpen]);

const pageElements = useMemo(
() =>
pages.map((page) => {
const icon = page.isDefault ? defaultPageIcon : pageIcon;
const isCurrentPage = currentPageId === page.pageId;
const pagePermissions = page.userPermissions;
const canManagePages = getHasManagePagePermission(
isFeatureEnabled,
pagePermissions,
);

const contextMenu = (
<PageContextMenu
applicationId={applicationId as string}
className={EntityClassNames.CONTEXT_MENU}
hasExportPermission={hasExportPermission}
isCurrentPage={isCurrentPage}
isDefaultPage={page.isDefault}
isHidden={!!page.isHidden}
key={page.pageId + "_context-menu"}
name={page.pageName}
pageId={page.pageId}
/>
);

return (
<StyledEntity
action={() => switchPage(page)}
active={isCurrentPage}
canEditEntityName={canManagePages}
className={`page fullWidth ${isCurrentPage && "activePage"}`}
contextMenu={contextMenu}
disabled={page.isHidden}
entityId={page.pageId}
icon={icon}
isDefaultExpanded={isCurrentPage}
key={page.pageId}
name={page.pageName}
onNameEdit={resolveAsSpaceChar}
searchKeyword={""}
step={1}
updateEntityName={(id, name) =>
updatePage({ id, name, isHidden: !!page.isHidden })
}
/>
);
}),
[pages, currentPageId, applicationId, location.pathname],
);

return (
<AnimatedFlex
flexDirection={"column"}
height={"calc(100% - 36px)"} // 36px is the height of the minimal segment
justifyContent={"center"}
overflow={"hidden"}
style={springs}
>
<Flex
alignItems={"center"}
background={"var(--ads-v2-color-bg-subtle)"}
borderBottom={"1px solid var(--ads-v2-color-border)"}
flexDirection={"row"}
justifyContent={"space-between"}
p="spaces-2"
pl="spaces-3"
width={"100%"}
>
<Text isBold kind={"body-m"}>
All Pages ({pages.length})
</Text>
{canCreatePages ? (
<AddPageContextMenu
className={`${EntityClassNames.ADD_BUTTON} group pages`}
createPageCallback={createPageCallback}
onMenuClose={onMenuClose}
openMenu={isMenuOpen}
/>
) : null}
</Flex>
<Flex
alignItems={"center"}
flex={"1"}
flexDirection={"column"}
width={"100%"}
>
{pageElements}
</Flex>
</AnimatedFlex>
);
};

export { PagesSection };
24 changes: 17 additions & 7 deletions app/client/src/pages/Editor/IDE/EditorPane/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
getPagesActiveStatus,
} from "selectors/ideSelectors";
import EntityProperties from "pages/Editor/Explorer/Entity/EntityProperties";
import { PagesSection } from "./PagesSection";
import { MinimalSegment } from "./MinimalSegment";

const EditorPane = ({ match: { path } }: RouteComponentProps) => {
const width = useEditorPaneWidth();
Expand All @@ -24,9 +26,9 @@ const EditorPane = ({ match: { path } }: RouteComponentProps) => {
const PagesRender = () => {
if (!isSideBySideEnabled) {
return <Pages />;
/* divider is inside the Pages component */
} else if (isSideBySideEnabled && pagesActive) {
/* This below pages component will get changed */
return <Pages />;
return <PagesSection />;
} else {
return null;
}
Expand All @@ -45,11 +47,19 @@ const EditorPane = ({ match: { path } }: RouteComponentProps) => {
the Bindings popover in the context menu. Will be removed eventually **/}
<EntityProperties />
<PagesRender />
{/* divider is inside the Pages component */}
<Switch>
<SentryRoute component={GlobalAdd} exact path={`${path}${ADD_PATH}`} />
<SentryRoute component={EditorPaneSegments} />
</Switch>

{pagesActive ? (
<MinimalSegment />
) : (
<Switch>
<SentryRoute
component={GlobalAdd}
exact
path={`${path}${ADD_PATH}`}
/>
<SentryRoute component={EditorPaneSegments} />
</Switch>
)}
</Flex>
);
};
Expand Down
11 changes: 11 additions & 0 deletions app/client/src/selectors/ideSelectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ export const getIDEViewMode = (state: AppState) => state.ui.ide.view;

export const getPagesActiveStatus = (state: AppState) =>
state.ui.ide.pagesActive;

export const getActionsCount = (state: AppState) =>
state.entities.actions.length || 0;
Comment on lines +26 to +27
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getActionsCount selector function correctly retrieves the length of the actions array from the state. However, the || 0 is unnecessary because the length property of an array will always return a number, and in the case of an empty array, it will return 0.

-  state.entities.actions.length || 0;
+  state.entities.actions.length;

Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
export const getActionsCount = (state: AppState) =>
state.entities.actions.length || 0;
export const getActionsCount = (state: AppState) =>
state.entities.actions.length;


export const getJsActionsCount = (state: AppState) =>
state.entities.jsActions.length || 0;
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getJsActionsCount selector function correctly retrieves the length of the jsActions array from the state. Similar to the previous comment, the || 0 is unnecessary.

-  state.entities.jsActions.length || 0;
+  state.entities.jsActions.length;

Committable suggestion

IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
export const getJsActionsCount = (state: AppState) =>
state.entities.jsActions.length || 0;
export const getJsActionsCount = (state: AppState) =>
state.entities.jsActions.length;


export const getWidgetsCount = (state: AppState) =>
Object.values(state.entities.canvasWidgets).filter(
(w) => w.type !== "CANVAS_WIDGET",
).length || 0;
Loading