diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Clone.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Clone.tsx new file mode 100644 index 000000000000..a772bb7d9217 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Clone.tsx @@ -0,0 +1,23 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { CONTEXT_CLONE, createMessage } from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { clonePageInit } from "actions/pageActions"; + +interface Props { + pageId: string; + disabled?: boolean; +} + +export const Clone = ({ disabled, pageId }: Props) => { + const dispatch = useDispatch(); + const clonePage = useCallback(() => { + dispatch(clonePageInit(pageId)); + }, [dispatch, pageId]); + + return ( + + {createMessage(CONTEXT_CLONE)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/ContextMenu.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/ContextMenu.tsx new file mode 100644 index 000000000000..21f6ae9386ed --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/ContextMenu.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { EntityContextMenu, MenuSeparator } from "@appsmith/ads"; +import { Rename } from "./Rename"; +import { Clone } from "./Clone"; +import { Visibility } from "./Visibility"; +import { SetAsHomePage } from "./SetAsHomePage"; +import { Delete } from "./Delete"; +import { PartialExport } from "./PartialExport"; +import { PartialImport } from "./PartialImport"; +import { + getHasManagePagePermission, + getHasCreatePagePermission, + getHasDeletePagePermission, +} from "ee/utils/BusinessFeatures/permissionPageHelpers"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import { getPageById } from "selectors/editorSelectors"; +import { getCurrentApplication } from "ee/selectors/applicationSelectors"; +import { useSelector } from "react-redux"; +import type { AppState } from "ee/reducers"; +import { EntityClassNames } from "pages/Editor/Explorer/Entity"; + +interface Props { + pageId: string; + pageName: string; + applicationId: string; + isCurrentPage: boolean; + isDefaultPage: boolean; + isHidden: boolean; + hasExportPermission: boolean; + onItemSelected?: () => void; +} + +export const ContextMenu = (props: Props) => { + const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); + + const pagePermissions = + useSelector(getPageById(props.pageId))?.userPermissions || []; + + const userAppPermissions = useSelector( + (state: AppState) => getCurrentApplication(state)?.userPermissions ?? [], + ); + + const canManagePages = getHasManagePagePermission( + isFeatureEnabled, + pagePermissions, + ); + + const canCreatePages = getHasCreatePagePermission( + isFeatureEnabled, + userAppPermissions, + ); + + const canDeletePages = getHasDeletePagePermission( + isFeatureEnabled, + pagePermissions, + ); + + const showPartialImportExport = + props.hasExportPermission && props.isCurrentPage; + + return ( + + + + + + + {showPartialImportExport && ( + <> + + + + + )} + + + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Delete.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Delete.tsx new file mode 100644 index 000000000000..5a699701024f --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Delete.tsx @@ -0,0 +1,51 @@ +import React, { useCallback, useState } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { + CONTEXT_DELETE, + CONFIRM_CONTEXT_DELETE, + createMessage, +} from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { deletePageAction } from "actions/pageActions"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import clsx from "clsx"; + +interface Props { + pageId: string; + pageName: string; + disabled?: boolean; +} + +export const Delete = ({ disabled, pageId, pageName }: Props) => { + const dispatch = useDispatch(); + const [confirmDelete, setConfirmDelete] = useState(false); + + const deletePageCallback = useCallback(() => { + dispatch(deletePageAction(pageId)); + AnalyticsUtil.logEvent("DELETE_PAGE", { + pageName: pageName, + }); + }, [dispatch, pageId, pageName]); + + const onSelect = useCallback( + (e?: Event) => { + e?.preventDefault(); + confirmDelete ? deletePageCallback() : setConfirmDelete(true); + e?.stopPropagation(); + }, + [confirmDelete, deletePageCallback], + ); + + return ( + + {confirmDelete + ? createMessage(CONFIRM_CONTEXT_DELETE) + : createMessage(CONTEXT_DELETE)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/PageContextMenu.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/OldPageContextMenu.tsx similarity index 100% rename from app/client/src/pages/AppIDE/components/PageList/PageContextMenu.tsx rename to app/client/src/pages/AppIDE/components/PageList/ContextMenu/OldPageContextMenu.tsx diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialExport.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialExport.tsx new file mode 100644 index 000000000000..1398d756ec97 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialExport.tsx @@ -0,0 +1,30 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { CONTEXT_PARTIAL_EXPORT, createMessage } from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { openPartialExportModal } from "actions/widgetActions"; + +interface Props { + disabled?: boolean; + onItemSelected?: () => void; +} + +export const PartialExport = ({ disabled, onItemSelected }: Props) => { + const dispatch = useDispatch(); + + const handlePartialExportClick = useCallback(() => { + if (onItemSelected) onItemSelected(); + + dispatch(openPartialExportModal(true)); + }, [onItemSelected, dispatch]); + + return ( + + {createMessage(CONTEXT_PARTIAL_EXPORT)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialImport.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialImport.tsx new file mode 100644 index 000000000000..2e314d9fc4a8 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/PartialImport.tsx @@ -0,0 +1,30 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { CONTEXT_PARTIAL_IMPORT, createMessage } from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { openPartialImportModal } from "ee/actions/applicationActions"; + +interface Props { + disabled?: boolean; + onItemSelected?: () => void; +} + +export const PartialImport = ({ disabled, onItemSelected }: Props) => { + const dispatch = useDispatch(); + + const handlePartialImportClick = useCallback(() => { + if (onItemSelected) onItemSelected(); + + dispatch(openPartialImportModal(true)); + }, [onItemSelected, dispatch]); + + return ( + + {createMessage(CONTEXT_PARTIAL_IMPORT)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Rename.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Rename.tsx new file mode 100644 index 000000000000..254490f20cc7 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Rename.tsx @@ -0,0 +1,30 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { CONTEXT_RENAME, createMessage } from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { initExplorerEntityNameEdit } from "actions/explorerActions"; + +interface Props { + pageId: string; + disabled?: boolean; +} + +export const Rename = ({ disabled, pageId }: Props) => { + const dispatch = useDispatch(); + const setRename = useCallback(() => { + // We add a delay to avoid having the focus stuck in the menu trigger + setTimeout(() => { + dispatch(initExplorerEntityNameEdit(pageId)); + }, 100); + }, [dispatch, pageId]); + + return ( + + {createMessage(CONTEXT_RENAME)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/SetAsHomePage.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/SetAsHomePage.tsx new file mode 100644 index 000000000000..62f82f2842be --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/SetAsHomePage.tsx @@ -0,0 +1,28 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { CONTEXT_SET_AS_HOME_PAGE, createMessage } from "ee/constants/messages"; +import { useDispatch } from "react-redux"; +import { setPageAsDefault } from "actions/pageActions"; + +interface Props { + pageId: string; + applicationId: string; + disabled?: boolean; +} + +export const SetAsHomePage = ({ applicationId, disabled, pageId }: Props) => { + const dispatch = useDispatch(); + const setPageAsDefaultCallback = useCallback(() => { + dispatch(setPageAsDefault(pageId, applicationId)); + }, [dispatch, pageId, applicationId]); + + return ( + + {createMessage(CONTEXT_SET_AS_HOME_PAGE)} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Visibility.tsx b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Visibility.tsx new file mode 100644 index 000000000000..eda93f67a67b --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/Visibility.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from "react"; +import { MenuItem } from "@appsmith/ads"; +import { useDispatch } from "react-redux"; +import { updatePageAction } from "actions/pageActions"; + +interface Props { + pageId: string; + pageName: string; + disabled?: boolean; + isHidden?: boolean; +} + +export const Visibility = ({ disabled, isHidden, pageId, pageName }: Props) => { + const dispatch = useDispatch(); + const setHiddenField = useCallback(() => { + dispatch( + updatePageAction({ + id: pageId, + name: pageName, + isHidden: !isHidden, + }), + ); + }, [dispatch, pageId, pageName, isHidden]); + + return ( + +
+ {isHidden ? "Show" : "Hide"} +
+
+ ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/ContextMenu/index.ts b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/index.ts new file mode 100644 index 000000000000..680381adcc78 --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/ContextMenu/index.ts @@ -0,0 +1 @@ +export { ContextMenu } from "./ContextMenu"; diff --git a/app/client/src/pages/AppIDE/components/PageList/PageElement.tsx b/app/client/src/pages/AppIDE/components/PageList/OldPageEntity.tsx similarity index 98% rename from app/client/src/pages/AppIDE/components/PageList/PageElement.tsx rename to app/client/src/pages/AppIDE/components/PageList/OldPageEntity.tsx index cc01b3bc8cc4..29778794e8fd 100644 --- a/app/client/src/pages/AppIDE/components/PageList/PageElement.tsx +++ b/app/client/src/pages/AppIDE/components/PageList/OldPageEntity.tsx @@ -7,7 +7,7 @@ import { defaultPageIcon, pageIcon } from "pages/Editor/Explorer/ExplorerIcons"; import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; -import PageContextMenu from "./PageContextMenu"; +import PageContextMenu from "./ContextMenu/OldPageContextMenu"; import { getCurrentApplicationId, getCurrentPageId, diff --git a/app/client/src/pages/AppIDE/components/PageList/PageEntity.tsx b/app/client/src/pages/AppIDE/components/PageList/PageEntity.tsx new file mode 100644 index 000000000000..6645431e4f0f --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/PageEntity.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useLocation } from "react-router"; + +import type { Page } from "entities/Page"; +import { defaultPageIcon, pageIcon } from "pages/Editor/Explorer/ExplorerIcons"; +import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import { ContextMenu } from "./ContextMenu"; +import { + getCurrentApplicationId, + getCurrentPageId, +} from "selectors/editorSelectors"; +import { PERMISSION_TYPE, isPermitted } from "ee/utils/permissionHelpers"; +import { getCurrentApplication } from "ee/selectors/applicationSelectors"; +import type { AppState } from "ee/reducers"; +import { updatePageAction } from "actions/pageActions"; +import { useGetPageFocusUrl } from "./hooks/useGetPageFocusUrl"; +import AnalyticsUtil from "ee/utils/AnalyticsUtil"; +import { toggleInOnboardingWidgetSelection } from "actions/onboardingActions"; +import history, { NavigationMethod } from "utils/history"; +import { EntityItem } from "@appsmith/ads"; +import { useNameEditorState } from "IDE/hooks/useNameEditorState"; +import { useValidateEntityName } from "IDE"; +import { noop } from "lodash"; + +export const PageEntity = ({ + onClick, + page, +}: { + page: Page; + onClick?: () => void; +}) => { + const dispatch = useDispatch(); + const location = useLocation(); + const navigateToUrl = useGetPageFocusUrl(page.basePageId); + const ref = useRef(null); + + const currentPageId = useSelector(getCurrentPageId); + const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled); + const applicationId = useSelector(getCurrentApplicationId); + const userAppPermissions = useSelector( + (state: AppState) => getCurrentApplication(state)?.userPermissions ?? [], + ); + + const { editingEntity, enterEditMode, exitEditMode, updatingEntity } = + useNameEditorState(); + const validateName = useValidateEntityName({ + entityName: page.pageName, + }); + + const icon = page.isDefault ? defaultPageIcon : pageIcon; + const isCurrentPage = currentPageId === page.pageId; + const pagePermissions = page.userPermissions; + + const canManagePages = getHasManagePagePermission( + isFeatureEnabled, + pagePermissions, + ); + const hasExportPermission = isPermitted( + userAppPermissions ?? [], + PERMISSION_TYPE.EXPORT_APPLICATION, + ); + + useEffect( + function scrollPageIntoView() { + if (ref.current && isCurrentPage) { + ref.current.scrollIntoView({ + inline: "nearest", + block: "nearest", + }); + } + }, + [ref, isCurrentPage], + ); + + const handleEnterEditMode = () => { + enterEditMode(page.pageId); + }; + + const handleDoubleClick = canManagePages ? handleEnterEditMode : noop; + + const switchPage = useCallback(() => { + AnalyticsUtil.logEvent("PAGE_NAME_CLICK", { + name: page.pageName, + fromUrl: location.pathname, + type: "PAGES", + toUrl: navigateToUrl, + }); + dispatch(toggleInOnboardingWidgetSelection(true)); + history.push(navigateToUrl, { + invokedBy: NavigationMethod.EntityExplorer, + }); + + if (onClick) { + onClick(); + } + }, [location.pathname, navigateToUrl, dispatch, page.pageName, onClick]); + + const contextMenu = useMemo( + () => ( + + ), + [ + applicationId, + hasExportPermission, + isCurrentPage, + page.isDefault, + page.isHidden, + page.pageId, + page.pageName, + onClick, + ], + ); + + const nameEditorConfig = useMemo(() => { + return { + canEdit: canManagePages, + isEditing: editingEntity === page.pageId, + isLoading: updatingEntity === page.pageId, + onEditComplete: exitEditMode, + onNameSave: (newName: string) => + dispatch( + updatePageAction({ + id: page.pageId, + name: newName, + isHidden: !!page.isHidden, + }), + ), + validateName: (newName: string) => validateName(newName), + normalizeName: false, + }; + }, [ + canManagePages, + dispatch, + editingEntity, + exitEditMode, + page, + updatingEntity, + validateName, + ]); + + return ( + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/PageList.tsx b/app/client/src/pages/AppIDE/components/PageList/PageList.tsx new file mode 100644 index 000000000000..736a4e6a440e --- /dev/null +++ b/app/client/src/pages/AppIDE/components/PageList/PageList.tsx @@ -0,0 +1,38 @@ +import React, { Fragment } from "react"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import type { Page } from "entities/Page"; +import { ListItemContainer } from "@appsmith/ads"; +import { PageElement as OldPageEntity } from "./OldPageEntity"; +import { PageEntity } from "./PageEntity"; + +interface PageListProps { + pages: Page[]; + onItemSelected: () => void; +} + +export const PageList = ({ onItemSelected, pages }: PageListProps) => { + const isNewADSEnabled = useFeatureFlag( + FEATURE_FLAG.release_ads_entity_item_enabled, + ); + + if (!isNewADSEnabled) { + return ( + <> + {pages.map((page) => ( + + + + ))} + + ); + } + + return ( + <> + {pages.map((page) => ( + + ))} + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/PageList/PagesSection.tsx b/app/client/src/pages/AppIDE/components/PageList/PagesSection.tsx index 15f55b47e3d9..29faa8bc27a2 100644 --- a/app/client/src/pages/AppIDE/components/PageList/PagesSection.tsx +++ b/app/client/src/pages/AppIDE/components/PageList/PagesSection.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useMemo, useState } from "react"; -import { ListItemContainer, ListWithHeader } from "@appsmith/ads"; +import { ListWithHeader } from "@appsmith/ads"; import { useDispatch, useSelector } from "react-redux"; -import { useLocation } from "react-router"; import { selectAllPages } from "ee/selectors/entitiesSelector"; import type { Page } from "entities/Page"; @@ -17,12 +16,11 @@ import AddPageContextMenu from "./AddPageContextMenu"; import { getNextEntityName } from "utils/AppsmithUtils"; import { getCurrentWorkspaceId } from "ee/selectors/selectedWorkspaceSelectors"; import { getInstanceId } from "ee/selectors/organizationSelectors"; -import { PageElement } from "./PageElement"; +import { PageList } from "./PageList"; import { PAGE_ENTITY_NAME } from "ee/constants/messages"; const PagesSection = ({ onItemSelected }: { onItemSelected: () => void }) => { const dispatch = useDispatch(); - const location = useLocation(); const pages: Page[] = useSelector(selectAllPages); const applicationId = useSelector(getCurrentApplicationId); const userAppPermissions = useSelector( @@ -53,16 +51,6 @@ const PagesSection = ({ onItemSelected }: { onItemSelected: () => void }) => { const onMenuClose = useCallback(() => setIsMenuOpen(false), [setIsMenuOpen]); - const pageElements = useMemo( - () => - pages.map((page) => ( - - - - )), - [pages, location.pathname, onItemSelected], - ); - const createPageContextMenu = useMemo(() => { if (!canCreatePages) return null; @@ -91,7 +79,7 @@ const PagesSection = ({ onItemSelected }: { onItemSelected: () => void }) => { headerText={`All Pages (${pages.length})`} maxHeight={"300px"} > - {pageElements} + ); };